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