The Battle for Wesnoth  1.19.0-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 = 15018;
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 {
178  return check_case_insensitive_duplicates_internal(dir, "", badlist);
179 }
180 
181 ADDON_TYPE get_addon_type(const std::string& str)
182 {
183  if (str.empty())
184  return ADDON_UNKNOWN;
185 
186  unsigned addon_type_num = 0;
187 
188  while(++addon_type_num != ADDON_TYPES_COUNT) {
189  if(str == addon_type_strings[addon_type_num]) {
190  return ADDON_TYPE(addon_type_num);
191  }
192  }
193 
194  return ADDON_UNKNOWN;
195 }
196 
198 {
199  assert(type != ADDON_TYPES_COUNT);
200  return addon_type_strings[type];
201 }
202 
203 namespace {
204  const char escape_char = '\x01'; /**< Binary escape char. */
205 } // end unnamed namespace 2
206 
207 bool needs_escaping(char c) {
208  switch(c) {
209  case '\x00':
210  case escape_char:
211  case '\x0D': //Windows -- carriage return
212  case '\xFE': //Parser code -- textdomain or linenumber&filename
213  return true;
214  default:
215  return false;
216  }
217 }
218 
219 std::string encode_binary(const std::string& str)
220 {
221  std::string res;
222  res.resize(str.size());
223  std::size_t n = 0;
224  for(std::string::const_iterator j = str.begin(); j != str.end(); ++j) {
225  if(needs_escaping(*j)) {
226  res.resize(res.size()+1);
227  res[n++] = escape_char;
228  res[n++] = *j + 1;
229  } else {
230  res[n++] = *j;
231  }
232  }
233 
234  return res;
235 }
236 
237 std::string unencode_binary(const std::string& str)
238 {
239  std::string res(str.size(), '\0');
240 
241  std::size_t n = 0;
242  for(std::string::const_iterator j = str.begin(); j != str.end(); ) {
243  char c = *j++;
244  if((c == escape_char) && (j != str.end())) {
245  c = (*j++) - 1;
246  }
247  res[n++] = c;
248  }
249 
250  res.resize(n);
251  return res;
252 }
253 
254 static std::string file_hash_raw(const config& file)
255 {
256  return utils::md5(file["contents"].str()).base64_digest();
257 }
258 
259 std::string file_hash(const config& file)
260 {
261  std::string hash = file["hash"].str();
262  if(hash.empty()) {
263  hash = file_hash_raw(file);
264  }
265  return hash;
266 }
267 
268 bool comp_file_hash(const config& file_a, const config& file_b)
269 {
270  return file_a["name"] == file_b["name"] && file_hash(file_a) == file_hash(file_b);
271 }
272 
273 void write_hashlist(config& hashlist, const config& data)
274 {
275  hashlist["name"] = data["name"];
276 
277  for(const config& f : data.child_range("file")) {
278  config& file = hashlist.add_child("file");
279  file["name"] = f["name"];
280  file["hash"] = file_hash_raw(f);
281  }
282 
283  for(const config& d : data.child_range("dir")) {
284  config& dir = hashlist.add_child("dir");
285  write_hashlist(dir, d);
286  }
287 }
288 
289 bool contains_hashlist(const config& from, const config& to)
290 {
291  for(const config& f : to.child_range("file")) {
292  bool found = false;
293  for(const config& d : from.child_range("file")) {
294  found |= comp_file_hash(f, d);
295  if(found)
296  break;
297  }
298  if(!found) {
299  return false;
300  }
301  }
302 
303  for(const config& d : to.child_range("dir")) {
304  auto origin_dir = from.find_child("dir", "name", d["name"]);
305  if(origin_dir) {
306  if(!contains_hashlist(*origin_dir, d)) {
307  return false;
308  }
309  } else {
310  // The case of empty new subdirectories
311  const config dummy_dir = config("name", d["name"]);
312  if(!contains_hashlist(dummy_dir, d)) {
313  return false;
314  }
315  }
316  }
317 
318  return true;
319 }
320 
321 /** Surround with [dir][/dir] */
322 static bool write_difference(config& pack, const config& from, const config& to, bool with_content)
323 {
324  pack["name"] = to["name"];
325  bool has_changes = false;
326 
327  for(const config& f : to.child_range("file")) {
328  bool found = false;
329  for(const config& d : from.child_range("file")) {
330  found |= comp_file_hash(f, d);
331  if(found)
332  break;
333  }
334  if(!found) {
335  config& file = pack.add_child("file");
336  file["name"] = f["name"];
337  if(with_content) {
338  file["contents"] = f["contents"];
339  file["hash"] = file_hash(f);
340  }
341  has_changes = true;
342  }
343  }
344 
345  for(const config& d : to.child_range("dir")) {
346  auto origin_dir = from.find_child("dir", "name", d["name"]);
347  config dir;
348  if(origin_dir) {
349  if(write_difference(dir, *origin_dir, d, with_content)) {
350  pack.add_child("dir", dir);
351  has_changes = true;
352  }
353  } else {
354  const config dummy_dir = config("name", d["name"]);
355  if(write_difference(dir, dummy_dir, d, with_content)) {
356  pack.add_child("dir", dir);
357  has_changes = true;
358  }
359  }
360  }
361 
362  return has_changes;
363 }
364 
365 /**
366  * &from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir]
367  *
368  * Does it worth it to archive and write the pack on the fly using config_writer?
369  * TODO: clientside verification?
370  */
371 void make_updatepack(config& pack, const config& from, const config& to)
372 {
373  config& removelist = pack.add_child("removelist");
374  write_difference(removelist, to, from, false);
375  config& addlist = pack.add_child("addlist");
376  write_difference(addlist, from, to, true);
377 }
378 
379 std::string addon_check_status_desc(unsigned int code)
380 {
381  static const std::map<ADDON_CHECK_STATUS, std::string> message_table = {
382 
383  //
384  // General errors
385  //
386 
387  {
389  N_("Success.")
390  },
391  {
393  N_("Incorrect add-on passphrase.")
394  },
395  {
397  N_("Forum authentication was requested for a user that is not registered on the forums.")
398  },
399  {
401  N_("Upload denied. Please contact the server administration for assistance.")
402  },
403  {
405  N_("Attempted to upload an update pack for a non-existent add-on.")
406  },
407 
408  //
409  // Structure errors
410  //
411 
412  {
414  N_("No add-on data was supplied by the client.")
415  },
416  {
418  N_("Invalid upload pack.")
419  },
420  {
422  N_("Invalid add-on name.")
423  },
424  {
426  N_("Formatting character in add-on name.")
427  },
428  {
430  N_("The add-on contains files or directories with illegal names.\n"
431  "\n"
432  "Names containing whitespace, control characters, or any of the following symbols are not allowed:\n"
433  "\n"
434  " \" * / : < > ? \\ | ~\n"
435  "\n"
436  "Additionally, names may not be longer than 255 characters, contain '..', or end with '.'.")
437  },
438  {
440  N_("The add-on contains files or directories with case conflicts.\n"
441  "\n"
442  "Names in the same directory may not be differently-cased versions of each other.")
443  },
444  {
446  N_("The add-on name contains an invalid UTF-8 sequence.")
447  },
448 
449  //
450  // .pbl errors
451  //
452 
453  {
455  N_("No add-on title specified.")
456  },
457  {
459  N_("No add-on author/maintainer name specified.")
460  },
461  {
463  N_("No add-on version specified.")
464  },
465  {
467  N_("No add-on description specified.")
468  },
469  {
471  N_("No add-on author/maintainer email specified.")
472  },
473  {
475  N_("Missing passphrase.")
476  },
477  {
479  N_("Formatting character in add-on title.")
480  },
481  {
483  N_("Invalid or unspecified add-on type.")
484  },
485  {
487  N_("Version number not greater than the latest uploaded version.")
488  },
489  {
491  N_("Feedback topic id is not a number.")
492  },
493  {
495  N_("Feedback topic does not exist.")
496  },
497  {
499  N_("The add-on publish information contains an invalid UTF-8 sequence.")
500  },
501  {
503  N_("The add-on's forum_auth attribute does not match what was previously uploaded.")
504  },
505 
506  //
507  // Server errors
508  //
509 
510  {
512  N_("Unspecified server error.")
513  },
514  {
516  N_("Server is in read-only mode.")
517  },
518  {
520  N_("Corrupted server add-ons list.")
521  },
522  {
524  N_("Empty add-on version list on the server.")
525  },
526  {
528  N_("This server does not support using the forum_auth attribute in your pbl.")
529  }
530  };
531 
532  for(const auto& entry : message_table) {
533  if(static_cast<unsigned int>(entry.first) == code) {
534  return entry.second;
535  }
536  }
537 
538  return N_("Unspecified validation failure.");
539 };
540 
541 std::string translated_addon_check_status(unsigned int code)
542 {
543  return _(addon_check_status_desc(code).c_str());
544 }
Variant for storing WML attributes.
std::string str(const std::string &fallback="") const
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
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:787
child_itors child_range(config_key_type key)
Definition: config.cpp:273
config & add_child(config_key_type key)
Definition: config.cpp:441
virtual std::string base64_digest() const override
Definition: hash.cpp:125
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:83
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:194
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:57
bool needs_escaping(char c)
Definition: validation.cpp:207
std::string file_hash(const config &file)
Definition: validation.cpp:259
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:237
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:371
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
Definition: validation.cpp:322
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:181
bool addon_filename_legal(const std::string &name)
Checks whether an add-on file name is legal or not.
Definition: validation.cpp:67
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:167
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:273
bool contains_hashlist(const config &from, const config &to)
Definition: validation.cpp:289
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:379
static std::string file_hash_raw(const config &file)
Definition: validation.cpp:254
std::string translated_addon_check_status(unsigned int code)
Definition: validation.cpp:541
std::string encode_binary(const std::string &str)
Definition: validation.cpp:219
bool comp_file_hash(const config &file_a, const config &file_b)
Definition: validation.cpp:268
std::string get_addon_type_string(ADDON_TYPE type)
Definition: validation.cpp:197
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
@ 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:117
#define d
#define f