The Battle for Wesnoth  1.19.8+dev
validation.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2024
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 = 15019;
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", "media", "theme", "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 bool addon_icon_too_large(const std::string& icon) {
75  return icon.size() > max_icon_size;
76 }
77 
78 namespace {
79 
80 bool check_names_legal_internal(const config& dir, std::string current_prefix, std::vector<std::string>* badlist)
81 {
82  if (!current_prefix.empty()) {
83  current_prefix += '/';
84  }
85 
86  for(const config& path : dir.child_range("file")) {
87  const std::string& filename = path["name"];
88 
90  if(badlist) {
91  badlist->push_back(current_prefix + filename);
92  } else {
93  return false;
94  }
95  }
96  }
97 
98  for(const config& path : dir.child_range("dir")) {
99  const std::string& dirname = path["name"];
100  const std::string& new_prefix = current_prefix + dirname;
101 
102  if(!addon_filename_legal(dirname)) {
103  if(badlist) {
104  badlist->push_back(new_prefix + "/");
105  } else {
106  return false;
107  }
108  }
109 
110  // Recurse into subdir.
111  if(!check_names_legal_internal(path, new_prefix, badlist) && !badlist) {
112  return false;
113  }
114  }
115 
116  return badlist ? badlist->empty() : true;
117 }
118 
119 bool check_case_insensitive_duplicates_internal(const config& dir, const std::string& prefix, std::vector<std::string>* badlist){
120  typedef std::pair<bool, std::string> printed_and_original;
121  std::map<std::string, printed_and_original> filenames;
122  bool inserted;
123  bool printed;
124  std::string original;
125  for (const config &path : dir.child_range("file")) {
126  const config::attribute_value &filename = path["name"];
127  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
128  const std::string with_prefix = prefix + filename.str();
129  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
130  if (!inserted){
131  if(badlist){
132  std::tie(printed, original) = filenames[lowercase];
133  if(!printed){
134  badlist->push_back(original);
135  filenames[lowercase] = make_pair(true, std::string());
136  }
137  badlist->push_back(with_prefix);
138  } else {
139  return false;
140  }
141  }
142  }
143  for (const config &path : dir.child_range("dir")) {
144  const config::attribute_value &filename = path["name"];
145  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
146  const std::string with_prefix = prefix + filename.str();
147  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
148  if (!inserted) {
149  if(badlist){
150  std::tie(printed, original) = filenames[lowercase];
151  if(!printed){
152  badlist->push_back(original);
153  filenames[lowercase] = make_pair(true, std::string());
154  }
155  badlist->push_back(with_prefix);
156  } else {
157  return false;
158  }
159  }
160  if(!check_case_insensitive_duplicates_internal(path, with_prefix + "/", badlist) && !badlist) {
161  return false;
162  }
163  }
164 
165  return badlist ? badlist->empty() : true;
166 }
167 
168 } // end unnamed namespace 3
169 
170 bool check_names_legal(const config& dir, std::vector<std::string>* badlist)
171 {
172  // Usually our caller is passing us the root [dir] for an add-on, which
173  // shall contain a single subdir named after the add-on itself, so we can
174  // start with an empty display prefix and that'll reflect the addon
175  // structure correctly (e.g. "Addon_Name/~illegalfilename1").
176  return check_names_legal_internal(dir, "", badlist);
177 }
178 
179 bool check_case_insensitive_duplicates(const config& dir, std::vector<std::string>* badlist)
180 {
181  return check_case_insensitive_duplicates_internal(dir, "", badlist);
182 }
183 
184 ADDON_TYPE get_addon_type(const std::string& str)
185 {
186  if (str.empty())
187  return ADDON_UNKNOWN;
188 
189  unsigned addon_type_num = 0;
190 
191  while(++addon_type_num != ADDON_TYPES_COUNT) {
192  if(str == addon_type_strings[addon_type_num]) {
193  return ADDON_TYPE(addon_type_num);
194  }
195  }
196 
197  return ADDON_UNKNOWN;
198 }
199 
201 {
202  assert(type != ADDON_TYPES_COUNT);
203  return addon_type_strings[type];
204 }
205 
206 namespace {
207  const char escape_char = '\x01'; /**< Binary escape char. */
208 } // end unnamed namespace 2
209 
210 bool needs_escaping(char c) {
211  switch(c) {
212  case '\x00':
213  case escape_char:
214  case '\x0D': //Windows -- carriage return
215  case '\xFE': //Parser code -- textdomain or linenumber&filename
216  return true;
217  default:
218  return false;
219  }
220 }
221 
222 std::string encode_binary(const std::string& str)
223 {
224  std::string res;
225  res.resize(str.size());
226  std::size_t n = 0;
227  for(std::string::const_iterator j = str.begin(); j != str.end(); ++j) {
228  if(needs_escaping(*j)) {
229  res.resize(res.size()+1);
230  res[n++] = escape_char;
231  res[n++] = *j + 1;
232  } else {
233  res[n++] = *j;
234  }
235  }
236 
237  return res;
238 }
239 
240 std::string unencode_binary(const std::string& str)
241 {
242  std::string res(str.size(), '\0');
243 
244  std::size_t n = 0;
245  for(std::string::const_iterator j = str.begin(); j != str.end(); ) {
246  char c = *j++;
247  if((c == escape_char) && (j != str.end())) {
248  c = (*j++) - 1;
249  }
250  res[n++] = c;
251  }
252 
253  res.resize(n);
254  return res;
255 }
256 
257 static std::string file_hash_raw(const config& file)
258 {
259  return utils::md5(file["contents"].str()).base64_digest();
260 }
261 
262 std::string file_hash(const config& file)
263 {
264  std::string hash = file["hash"].str();
265  if(hash.empty()) {
266  hash = file_hash_raw(file);
267  }
268  return hash;
269 }
270 
271 bool comp_file_hash(const config& file_a, const config& file_b)
272 {
273  return file_a["name"] == file_b["name"] && file_hash(file_a) == file_hash(file_b);
274 }
275 
276 void write_hashlist(config& hashlist, const config& data)
277 {
278  hashlist["name"] = data["name"];
279 
280  for(const config& f : data.child_range("file")) {
281  config& file = hashlist.add_child("file");
282  file["name"] = f["name"];
283  file["hash"] = file_hash_raw(f);
284  }
285 
286  for(const config& d : data.child_range("dir")) {
287  config& dir = hashlist.add_child("dir");
288  write_hashlist(dir, d);
289  }
290 }
291 
292 bool contains_hashlist(const config& from, const config& to)
293 {
294  for(const config& f : to.child_range("file")) {
295  bool found = false;
296  for(const config& d : from.child_range("file")) {
297  found |= comp_file_hash(f, d);
298  if(found)
299  break;
300  }
301  if(!found) {
302  return false;
303  }
304  }
305 
306  for(const config& d : to.child_range("dir")) {
307  auto origin_dir = from.find_child("dir", "name", d["name"]);
308  if(origin_dir) {
309  if(!contains_hashlist(*origin_dir, d)) {
310  return false;
311  }
312  } else {
313  // The case of empty new subdirectories
314  const config dummy_dir = config("name", d["name"]);
315  if(!contains_hashlist(dummy_dir, d)) {
316  return false;
317  }
318  }
319  }
320 
321  return true;
322 }
323 
324 /** Surround with [dir][/dir] */
325 static bool write_difference(config& pack, const config& from, const config& to, bool with_content)
326 {
327  pack["name"] = to["name"];
328  bool has_changes = false;
329 
330  for(const config& f : to.child_range("file")) {
331  bool found = false;
332  for(const config& d : from.child_range("file")) {
333  found |= comp_file_hash(f, d);
334  if(found)
335  break;
336  }
337  if(!found) {
338  config& file = pack.add_child("file");
339  file["name"] = f["name"];
340  if(with_content) {
341  file["contents"] = f["contents"];
342  file["hash"] = file_hash(f);
343  }
344  has_changes = true;
345  }
346  }
347 
348  for(const config& d : to.child_range("dir")) {
349  auto origin_dir = from.find_child("dir", "name", d["name"]);
350  config dir;
351  if(origin_dir) {
352  if(write_difference(dir, *origin_dir, d, with_content)) {
353  pack.add_child("dir", dir);
354  has_changes = true;
355  }
356  } else {
357  const config dummy_dir = config("name", d["name"]);
358  if(write_difference(dir, dummy_dir, d, with_content)) {
359  pack.add_child("dir", dir);
360  has_changes = true;
361  }
362  }
363  }
364 
365  return has_changes;
366 }
367 
368 /**
369  * &from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir]
370  *
371  * Does it worth it to archive and write the pack on the fly using config_writer?
372  * TODO: clientside verification?
373  */
374 void make_updatepack(config& pack, const config& from, const config& to)
375 {
376  config& removelist = pack.add_child("removelist");
377  write_difference(removelist, to, from, false);
378  config& addlist = pack.add_child("addlist");
379  write_difference(addlist, from, to, true);
380 }
381 
382 std::string addon_check_status_desc(unsigned int code)
383 {
384  static const std::map<ADDON_CHECK_STATUS, std::string> message_table = {
385 
386  //
387  // General errors
388  //
389 
390  {
392  N_("Success.")
393  },
394  {
396  N_("Incorrect add-on passphrase.")
397  },
398  {
400  N_("Forum authentication was requested for a user that is not registered on the forums.")
401  },
402  {
404  N_("Upload denied. Please contact the server administration for assistance.")
405  },
406  {
408  N_("Attempted to upload an update pack for a non-existent add-on.")
409  },
410 
411  //
412  // Structure errors
413  //
414 
415  {
417  N_("No add-on data was supplied by the client.")
418  },
419  {
421  N_("Invalid upload pack.")
422  },
423  {
425  N_("Invalid add-on name.")
426  },
427  {
429  N_("Formatting character in add-on name.")
430  },
431  {
433  N_("The add-on contains files or directories with illegal names.\n"
434  "\n"
435  "Names containing whitespace, control characters, or any of the following symbols are not allowed:\n"
436  "\n"
437  " \" * / : < > ? \\ | ~\n"
438  "\n"
439  "Additionally, names may not be longer than 255 characters, contain '..', or end with '.'.")
440  },
441  {
443  N_("The add-on contains files or directories with case conflicts.\n"
444  "\n"
445  "Names in the same directory may not be differently-cased versions of each other.")
446  },
447  {
449  N_("The add-on name contains an invalid UTF-8 sequence.")
450  },
451 
452  //
453  // .pbl errors
454  //
455 
456  {
458  N_("No add-on title specified.")
459  },
460  {
462  N_("No add-on author/maintainer name specified.")
463  },
464  {
466  N_("No add-on version specified.")
467  },
468  {
470  N_("No add-on description specified.")
471  },
472  {
474  N_("No add-on author/maintainer email specified.")
475  },
476  {
478  N_("Missing passphrase.")
479  },
480  {
482  N_("Formatting character in add-on title.")
483  },
484  {
486  N_("Invalid or unspecified add-on type.")
487  },
488  {
490  N_("Version number not greater than the latest uploaded version.")
491  },
492  {
494  N_("Feedback topic id is not a number.")
495  },
496  {
498  N_("Feedback topic does not exist.")
499  },
500  {
502  N_("The add-on publish information contains an invalid UTF-8 sequence.")
503  },
504  {
506  N_("The add-on’s forum_auth attribute does not match what was previously uploaded.")
507  },
508  {
510  N_("The add-on’s icon’s file size is too large.")
511  },
512 
513  //
514  // Server errors
515  //
516 
517  {
519  N_("Unspecified server error.")
520  },
521  {
523  N_("Server is in read-only mode.")
524  },
525  {
527  N_("Corrupted server add-ons list.")
528  },
529  {
531  N_("Empty add-on version list on the server.")
532  },
533  {
535  N_("This server does not support using the forum_auth attribute in your pbl.")
536  }
537  };
538 
539  for(const auto& entry : message_table) {
540  if(static_cast<unsigned int>(entry.first) == code) {
541  return entry.second;
542  }
543  }
544 
545  return N_("Unspecified validation failure.");
546 };
547 
548 std::string translated_addon_check_status(unsigned int code)
549 {
550  return _(addon_check_status_desc(code).c_str());
551 }
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
optional_config_impl< 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:780
child_itors child_range(config_key_type key)
Definition: config.cpp:268
config & add_child(config_key_type key)
Definition: config.cpp:436
virtual std::string base64_digest() const override
Definition: hash.cpp:125
Definitions for the interface to Wesnoth Markup Language (WML).
Declarations for File-IO.
#define N_(String)
Definition: gettext.hpp:101
static std::string _(const char *str)
Definition: gettext.hpp:93
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.
std::string path
Definition: filesystem.cpp:92
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
Definition: unicode.cpp:50
std::string_view data
Definition: picture.cpp:178
std::string filename
Filename.
mock_char c
static map_location::direction n
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:56
bool addon_icon_too_large(const std::string &icon)
Checks whether an add-on icon is too large.
Definition: validation.cpp:74
bool needs_escaping(char c)
Definition: validation.cpp:210
std::string file_hash(const config &file)
Definition: validation.cpp:262
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:240
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:374
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
Definition: validation.cpp:325
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:184
bool addon_filename_legal(const std::string &name)
Checks whether an add-on file name is legal or not.
Definition: validation.cpp:66
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:170
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:276
bool contains_hashlist(const config &from, const config &to)
Definition: validation.cpp:292
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:27
std::string addon_check_status_desc(unsigned int code)
Definition: validation.cpp:382
static std::string file_hash_raw(const config &file)
Definition: validation.cpp:257
std::string translated_addon_check_status(unsigned int code)
Definition: validation.cpp:548
std::string encode_binary(const std::string &str)
Definition: validation.cpp:222
bool comp_file_hash(const config &file_a, const config &file_b)
Definition: validation.cpp:271
std::string get_addon_type_string(ADDON_TYPE type)
Definition: validation.cpp:200
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:179
constexpr std::size_t max_icon_size
Definition: validation.hpp:129
@ SERVER_ADDONS_LIST
Corrupted server add-ons list.
@ UNAUTHORIZED
Authentication failed.
@ BAD_FEEDBACK_TOPIC_ID
The provided topic ID for the addon's feedback forum thread is invalid.
@ SERVER_DELTA_NO_VERSIONS
No versions to deltify against.
@ NO_TITLE
No title specified.
@ AUTH_TYPE_MISMATCH
The addon's forum_auth value does not match its previously set value.
@ ILLEGAL_FILENAME
Bad filename.
@ ICON_TOO_LARGE
The add-on's icon is too large (presumably a DataURI)
@ NO_PASSPHRASE
No passphrase specified.
@ UNEXPECTED_DELTA
Delta for a non-existent add-on.
@ VERSION_NOT_INCREMENTED
Version number is not an increment.
@ FILENAME_CASE_CONFLICT
Filename case conflict.
@ TITLE_HAS_MARKUP
Markup in add-on title.
@ BAD_DELTA
Bad delta pack.
@ SERVER_UNSPECIFIED
Unspecified server error.
@ DENIED
Upload denied.
@ BAD_TYPE
Bad add-on type.
@ NO_AUTHOR
No author specified.
@ EMPTY_PACK
Empty pack.
@ NO_DESCRIPTION
No description specified.
@ NO_EMAIL
No email specified.
@ FEEDBACK_TOPIC_ID_NOT_FOUND
The provided topic ID for the addon's feedback forum thread wasn't found in the forum database.
@ INVALID_UTF8_NAME
Invalid UTF-8 sequence in add-on name.
@ USER_DOES_NOT_EXIST
Requested forum authentication for a user that doesn't exist on the forums.
@ INVALID_UTF8_ATTRIBUTE
Invalid UTF-8 sequence in add-on metadata.
@ SUCCESS
No error.
@ BAD_NAME
Bad add-on name.
@ NAME_HAS_MARKUP
Markup in add-on name.
@ SERVER_FORUM_AUTH_DISABLED
The remote add-ons server does not support forum authorization.
@ SERVER_READ_ONLY
Server read-only mode on.
@ NO_VERSION
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:102
@ ADDON_UNKNOWN
a.k.a.
Definition: validation.hpp:103
@ ADDON_TYPES_COUNT
Definition: validation.hpp:117
#define d
#define f