The Battle for Wesnoth  1.19.5+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 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 
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, with_prefix + "/", 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 {
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  auto 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  auto 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_("Forum authentication was requested for a user that is not registered on the forums.")
397  },
398  {
400  N_("Upload denied. Please contact the server administration for assistance.")
401  },
402  {
404  N_("Attempted to upload an update pack for a non-existent add-on.")
405  },
406 
407  //
408  // Structure errors
409  //
410 
411  {
413  N_("No add-on data was supplied by the client.")
414  },
415  {
417  N_("Invalid upload pack.")
418  },
419  {
421  N_("Invalid add-on name.")
422  },
423  {
425  N_("Formatting character in add-on name.")
426  },
427  {
429  N_("The add-on contains files or directories with illegal names.\n"
430  "\n"
431  "Names containing whitespace, control characters, or any of the following symbols are not allowed:\n"
432  "\n"
433  " \" * / : < > ? \\ | ~\n"
434  "\n"
435  "Additionally, names may not be longer than 255 characters, contain '..', or end with '.'.")
436  },
437  {
439  N_("The add-on contains files or directories with case conflicts.\n"
440  "\n"
441  "Names in the same directory may not be differently-cased versions of each other.")
442  },
443  {
445  N_("The add-on name contains an invalid UTF-8 sequence.")
446  },
447 
448  //
449  // .pbl errors
450  //
451 
452  {
454  N_("No add-on title specified.")
455  },
456  {
458  N_("No add-on author/maintainer name specified.")
459  },
460  {
462  N_("No add-on version specified.")
463  },
464  {
466  N_("No add-on description specified.")
467  },
468  {
470  N_("No add-on author/maintainer email specified.")
471  },
472  {
474  N_("Missing passphrase.")
475  },
476  {
478  N_("Formatting character in add-on title.")
479  },
480  {
482  N_("Invalid or unspecified add-on type.")
483  },
484  {
486  N_("Version number not greater than the latest uploaded version.")
487  },
488  {
490  N_("Feedback topic id is not a number.")
491  },
492  {
494  N_("Feedback topic does not exist.")
495  },
496  {
498  N_("The add-on publish information contains an invalid UTF-8 sequence.")
499  },
500  {
502  N_("The add-on’s forum_auth attribute does not match what was previously uploaded.")
503  },
504 
505  //
506  // Server errors
507  //
508 
509  {
511  N_("Unspecified server error.")
512  },
513  {
515  N_("Server is in read-only mode.")
516  },
517  {
519  N_("Corrupted server add-ons list.")
520  },
521  {
523  N_("Empty add-on version list on the server.")
524  },
525  {
527  N_("This server does not support using the forum_auth attribute in your pbl.")
528  }
529  };
530 
531  for(const auto& entry : message_table) {
532  if(static_cast<unsigned int>(entry.first) == code) {
533  return entry.second;
534  }
535  }
536 
537  return N_("Unspecified validation failure.");
538 };
539 
540 std::string translated_addon_check_status(unsigned int code)
541 {
542  return _(addon_check_status_desc(code).c_str());
543 }
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
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:784
child_itors child_range(config_key_type key)
Definition: config.cpp:272
config & add_child(config_key_type key)
Definition: config.cpp:440
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:90
std::string lowercase(const std::string &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 needs_escaping(char c)
Definition: validation.cpp:206
std::string file_hash(const config &file)
Definition: validation.cpp:258
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:236
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
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
Definition: validation.cpp:321
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:180
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:166
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:272
bool contains_hashlist(const config &from, const config &to)
Definition: validation.cpp:288
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:378
static std::string file_hash_raw(const config &file)
Definition: validation.cpp:253
std::string translated_addon_check_status(unsigned int code)
Definition: validation.cpp:540
std::string encode_binary(const std::string &str)
Definition: validation.cpp:218
bool comp_file_hash(const config &file_a, const config &file_b)
Definition: validation.cpp:267
std::string get_addon_type_string(ADDON_TYPE type)
Definition: validation.cpp:196
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
@ 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.
@ 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:101
@ ADDON_UNKNOWN
a.k.a.
Definition: validation.hpp:102
@ ADDON_TYPES_COUNT
Definition: validation.hpp:116
#define d
#define f