The Battle for Wesnoth  1.15.12+dev
validation.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
3  2008 - 2015 by Iris Morelle <shadowm2006@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "addon/validation.hpp"
17 #include "config.hpp"
18 #include "filesystem.hpp"
19 #include "gettext.hpp"
20 #include "hash.hpp"
21 
22 #include <algorithm>
23 #include <array>
24 #include <boost/algorithm/string.hpp>
25 
26 const unsigned short default_campaignd_port = 15015;
27 
28 namespace
29 {
30 
31 const std::array<std::string, ADDON_TYPES_COUNT> addon_type_strings {{
32  "unknown", "core", "campaign", "scenario", "campaign_sp_mp", "campaign_mp",
33  "scenario_mp", "map_pack", "era", "faction", "mod_mp", /*"gui", */ "media",
34  "other"
35 }};
36 
37 struct addon_name_char_illegal
38 {
39  /**
40  * Returns whether the given add-on name char is not whitelisted.
41  */
42  inline bool operator()(char c) const
43  {
44  switch(c) {
45  case '-': // hyphen-minus
46  case '_': // low line
47  return false;
48  default:
49  return !isalnum(c);
50  }
51  }
52 };
53 
54 } // end unnamed namespace
55 
56 bool addon_name_legal(const std::string& name)
57 {
58  if(name.empty() ||
59  std::find_if(name.begin(), name.end(), addon_name_char_illegal()) != name.end()) {
60  return false;
61  } else {
62  return true;
63  }
64 }
65 
66 bool addon_filename_legal(const std::string& name)
67 {
68  // Currently just a wrapper for filesystem::is_legal_user_file_name().
69  // This is allowed to change in the future. Do not remove this wrapper.
70  // I will hunt you down if you do.
71  return filesystem::is_legal_user_file_name(name, false);
72 }
73 
74 namespace {
75 
76 bool check_names_legal_internal(const config& dir, std::string current_prefix, std::vector<std::string>* badlist)
77 {
78  if (!current_prefix.empty()) {
79  current_prefix += '/';
80  }
81 
82  for(const config& path : dir.child_range("file")) {
83  const std::string& filename = path["name"];
84 
85  if(!addon_filename_legal(filename)) {
86  if(badlist) {
87  badlist->push_back(current_prefix + filename);
88  } else {
89  return false;
90  }
91  }
92  }
93 
94  for(const config& path : dir.child_range("dir")) {
95  const std::string& dirname = path["name"];
96  const std::string& new_prefix = current_prefix + dirname;
97 
98  if(!addon_filename_legal(dirname)) {
99  if(badlist) {
100  badlist->push_back(new_prefix + "/");
101  } else {
102  return false;
103  }
104  }
105 
106  // Recurse into subdir.
107  if(!check_names_legal_internal(path, new_prefix, badlist) && !badlist) {
108  return false;
109  }
110  }
111 
112  return badlist ? badlist->empty() : true;
113 }
114 
115 bool check_case_insensitive_duplicates_internal(const config& dir, const std::string& prefix, std::vector<std::string>* badlist){
116  typedef std::pair<bool, std::string> printed_and_original;
117  std::map<std::string, printed_and_original> filenames;
118  bool inserted;
119  bool printed;
120  std::string original;
121  for (const config &path : dir.child_range("file")) {
122  const config::attribute_value &filename = path["name"];
123  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
124  const std::string with_prefix = prefix + filename.str();
125  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
126  if (!inserted){
127  if(badlist){
128  std::tie(printed, original) = filenames[lowercase];
129  if(!printed){
130  badlist->push_back(original);
131  filenames[lowercase] = make_pair(true, std::string());
132  }
133  badlist->push_back(with_prefix);
134  } else {
135  return false;
136  }
137  }
138  }
139  for (const config &path : dir.child_range("dir")) {
140  const config::attribute_value &filename = path["name"];
141  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
142  const std::string with_prefix = prefix + filename.str();
143  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
144  if (!inserted) {
145  if(badlist){
146  std::tie(printed, original) = filenames[lowercase];
147  if(!printed){
148  badlist->push_back(original);
149  filenames[lowercase] = make_pair(true, std::string());
150  }
151  badlist->push_back(with_prefix);
152  } else {
153  return false;
154  }
155  }
156  if (!check_case_insensitive_duplicates_internal(path, prefix + filename + "/", badlist) && !badlist){
157  return false;
158  }
159  }
160 
161  return badlist ? badlist->empty() : true;
162 }
163 
164 } // end unnamed namespace 3
165 
166 bool check_names_legal(const config& dir, std::vector<std::string>* badlist)
167 {
168  // Usually our caller is passing us the root [dir] for an add-on, which
169  // shall contain a single subdir named after the add-on itself, so we can
170  // start with an empty display prefix and that'll reflect the addon
171  // structure correctly (e.g. "Addon_Name/~illegalfilename1").
172  return check_names_legal_internal(dir, "", badlist);
173 }
174 
175 bool check_case_insensitive_duplicates(const config& dir, std::vector<std::string>* badlist){
176  return check_case_insensitive_duplicates_internal(dir, "", badlist);
177 }
178 
179 ADDON_TYPE get_addon_type(const std::string& str)
180 {
181  if (str.empty())
182  return ADDON_UNKNOWN;
183 
184  unsigned addon_type_num = 0;
185 
186  while(++addon_type_num != ADDON_TYPES_COUNT) {
187  if(str == addon_type_strings[addon_type_num]) {
188  return ADDON_TYPE(addon_type_num);
189  }
190  }
191 
192  return ADDON_UNKNOWN;
193 }
194 
196 {
197  assert(type != ADDON_TYPES_COUNT);
198  return addon_type_strings[type];
199 }
200 
201 namespace {
202  const char escape_char = '\x01'; /**< Binary escape char. */
203 } // end unnamed namespace 2
204 
205 bool needs_escaping(char c) {
206  switch(c) {
207  case '\x00':
208  case escape_char:
209  case '\x0D': //Windows -- carriage return
210  case '\xFE': //Parser code -- textdomain or linenumber&filename
211  return true;
212  default:
213  return false;
214  }
215 }
216 
217 std::string encode_binary(const std::string& str)
218 {
219  std::string res;
220  res.resize(str.size());
221  std::size_t n = 0;
222  for(std::string::const_iterator j = str.begin(); j != str.end(); ++j) {
223  if(needs_escaping(*j)) {
224  res.resize(res.size()+1);
225  res[n++] = escape_char;
226  res[n++] = *j + 1;
227  } else {
228  res[n++] = *j;
229  }
230  }
231 
232  return res;
233 }
234 
235 std::string unencode_binary(const std::string& str)
236 {
237  std::string res(str.size(), '\0');
238 
239  std::size_t n = 0;
240  for(std::string::const_iterator j = str.begin(); j != str.end(); ) {
241  char c = *j++;
242  if((c == escape_char) && (j != str.end())) {
243  c = (*j++) - 1;
244  }
245  res[n++] = c;
246  }
247 
248  res.resize(n);
249  return res;
250 }
251 
252 static std::string file_hash_raw(const config& file)
253 {
254  return utils::md5(file["contents"].str()).base64_digest();
255 }
256 
257 std::string file_hash(const config& file)
258 {
259  std::string hash = file["hash"].str();
260  if(hash.empty()) {
261  hash = file_hash_raw(file);
262  }
263  return hash;
264 }
265 
266 bool comp_file_hash(const config& file_a, const config& file_b)
267 {
268  return file_a["name"] == file_b["name"] && file_hash(file_a) == file_hash(file_b);
269 }
270 
271 void write_hashlist(config& hashlist, const config& data)
272 {
273  hashlist["name"] = data["name"];
274 
275  for(const config& f : data.child_range("file")) {
276  config& file = hashlist.add_child("file");
277  file["name"] = f["name"];
278  file["hash"] = file_hash_raw(f);
279  }
280 
281  for(const config& d : data.child_range("dir")) {
282  config& dir = hashlist.add_child("dir");
283  write_hashlist(dir, d);
284  }
285 }
286 
287 bool contains_hashlist(const config& from, const config& to)
288 {
289  for(const config& f : to.child_range("file")) {
290  bool found = false;
291  for(const config& d : from.child_range("file")) {
292  found |= comp_file_hash(f, d);
293  if(found)
294  break;
295  }
296  if(!found) {
297  return false;
298  }
299  }
300 
301  for(const config& d : to.child_range("dir")) {
302  const config& origin_dir = from.find_child("dir", "name", d["name"]);
303  if(origin_dir) {
304  if(!contains_hashlist(origin_dir, d)) {
305  return false;
306  }
307  } else {
308  // The case of empty new subdirectories
309  const config dummy_dir = config("name", d["name"]);
310  if(!contains_hashlist(dummy_dir, d)) {
311  return false;
312  }
313  }
314  }
315 
316  return true;
317 }
318 
319 /** Surround with [dir][/dir] */
320 static bool write_difference(config& pack, const config& from, const config& to, bool with_content)
321 {
322  pack["name"] = to["name"];
323  bool has_changes = false;
324 
325  for(const config& f : to.child_range("file")) {
326  bool found = false;
327  for(const config& d : from.child_range("file")) {
328  found |= comp_file_hash(f, d);
329  if(found)
330  break;
331  }
332  if(!found) {
333  config& file = pack.add_child("file");
334  file["name"] = f["name"];
335  if(with_content) {
336  file["contents"] = f["contents"];
337  file["hash"] = file_hash(f);
338  }
339  has_changes = true;
340  }
341  }
342 
343  for(const config& d : to.child_range("dir")) {
344  const config& origin_dir = from.find_child("dir", "name", d["name"]);
345  config dir;
346  if(origin_dir) {
347  if(write_difference(dir, origin_dir, d, with_content)) {
348  pack.add_child("dir", dir);
349  has_changes = true;
350  }
351  } else {
352  const config dummy_dir = config("name", d["name"]);
353  if(write_difference(dir, dummy_dir, d, with_content)) {
354  pack.add_child("dir", dir);
355  has_changes = true;
356  }
357  }
358  }
359 
360  return has_changes;
361 }
362 
363 /**
364  * &from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir]
365  *
366  * Does it worth it to archive and write the pack on the fly using config_writer?
367  * TODO: clientside verification?
368  */
369 void make_updatepack(config& pack, const config& from, const config& to)
370 {
371  config& removelist = pack.add_child("removelist");
372  write_difference(removelist, to, from, false);
373  config& addlist = pack.add_child("addlist");
374  write_difference(addlist, from, to, true);
375 }
376 
377 std::string addon_check_status_desc(unsigned int code)
378 {
379  static const std::map<ADDON_CHECK_STATUS, std::string> message_table = {
380 
381  //
382  // General errors
383  //
384 
385  {
387  N_("Success.")
388  },
389  {
391  N_("Incorrect add-on passphrase.")
392  },
393  {
395  N_("Upload denied. Please contact the server administration for assistance.")
396  },
397  {
399  N_("Attempted to upload an update pack for a non-existent add-on.")
400  },
401 
402  //
403  // Structure errors
404  //
405 
406  {
408  N_("No add-on data was supplied by the client.")
409  },
410  {
412  N_("Invalid upload pack.")
413  },
414  {
416  N_("Invalid add-on name.")
417  },
418  {
420  N_("Formatting character in add-on name.")
421  },
422  {
424  N_("The add-on contains files or directories with illegal names.\n"
425  "\n"
426  "Names containing whitespace, control characters, or any of the following symbols are not allowed:\n"
427  "\n"
428  " \" * / : < > ? \\ | ~\n"
429  "\n"
430  "Additionally, names may not be longer than 255 characters, contain '..', or end with '.'.")
431  },
432  {
434  N_("The add-on contains files or directories with case conflicts.\n"
435  "\n"
436  "Names in the same directory may not be differently-cased versions of each other.")
437  },
438  {
440  N_("The add-on name contains an invalid UTF-8 sequence.")
441  },
442 
443  //
444  // .pbl errors
445  //
446 
447  {
449  N_("No add-on title specified.")
450  },
451  {
453  N_("No add-on author/maintainer name specified.")
454  },
455  {
457  N_("No add-on version specified.")
458  },
459  {
461  N_("No add-on description specified.")
462  },
463  {
465  N_("No add-on author/maintainer email specified.")
466  },
467  {
469  N_("Missing passphrase.")
470  },
471  {
473  N_("Formatting character in add-on title.")
474  },
475  {
477  N_("Invalid or unspecified add-on type.")
478  },
479  {
481  N_("Version number not greater than the latest uploaded version.")
482  },
483  {
485  N_("Feedback topic id is not a number.")
486  },
487  {
489  N_("Feedback topic does not exist.")
490  },
491  {
493  N_("The add-on publish information contains an invalid UTF-8 sequence.")
494  },
495 
496  //
497  // Server errors
498  //
499 
500  {
502  N_("Unspecified server error.")
503  },
504  {
506  N_("Server is in read-only mode.")
507  },
508  {
510  N_("Corrupted server add-ons list.")
511  },
512  {
514  N_("Empty add-on version list on the server.")
515  }
516  };
517 
518  for(const auto& entry : message_table) {
519  if(static_cast<unsigned int>(entry.first) == code) {
520  return entry.second;
521  }
522  }
523 
524  return N_("Unspecified validation failure.");
525 };
526 
527 std::string translated_addon_check_status(unsigned int code)
528 {
529  return _(addon_check_status_desc(code).c_str());
530 }
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:166
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:175
bool is_legal_user_file_name(const std::string &name, bool allow_whitespace=true)
Returns whether the given filename is a legal name for a user-created file.
No version specified.
ADDON_TYPE
Values used for add-on classification; UI-only at the moment, in the future it could be used for dire...
Definition: validation.hpp:97
Invalid UTF-8 sequence in add-on name.
bool contains_hashlist(const config &from, const config &to)
Definition: validation.cpp:287
virtual std::string base64_digest() const override
Definition: hash.cpp:121
config & find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:860
Variant for storing WML attributes.
No versions to deltify against.
std::string encode_binary(const std::string &str)
Definition: validation.cpp:217
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:235
child_itors child_range(config_key_type key)
Definition: config.cpp:356
Delta for a non-existent add-on.
bool needs_escaping(char c)
Definition: validation.cpp:205
#define d
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:271
static std::string _(const char *str)
Definition: gettext.hpp:92
std::string addon_check_status_desc(unsigned int code)
Definition: validation.cpp:377
Definitions for the interface to Wesnoth Markup Language (WML).
No description specified.
Invalid UTF-8 sequence in add-on metadata.
No email specified.
Authentication failed.
Server read-only mode on.
std::string path
Definition: game_config.cpp:38
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:179
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:56
The provided topic ID for the addon&#39;s feedback forum thread wasn&#39;t found in the forum database...
Unspecified server error.
static std::string file_hash_raw(const config &file)
Definition: validation.cpp:252
std::string file_hash(const config &file)
Definition: validation.cpp:257
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
Definition: validation.cpp:320
Markup in add-on title.
std::string translated_addon_check_status(unsigned int code)
Definition: validation.cpp:527
Declarations for File-IO.
#define N_(String)
Definition: gettext.hpp:100
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:51
No title specified.
config & add_child(config_key_type key)
Definition: config.cpp:500
Corrupted server add-ons list.
bool comp_file_hash(const config &file_a, const config &file_b)
Definition: validation.cpp:266
No passphrase specified.
void make_updatepack(config &pack, const config &from, const config &to)
&from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir] ...
Definition: validation.cpp:369
#define f
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:26
The provided topic ID for the addon&#39;s feedback forum thread is invalid.
std::string get_addon_type_string(ADDON_TYPE type)
Definition: validation.cpp:195
No author specified.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:59
mock_char c
static map_location::DIRECTION n
Version number is not an increment.
std::string str(const std::string &fallback="") const
bool addon_filename_legal(const std::string &name)
Checks whether an add-on file name is legal or not.
Definition: validation.cpp:66