The Battle for Wesnoth  1.17.10+dev
validation.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2022
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 = 15017;
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  const config& 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  const config& 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 }
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
The remote add-ons server does not support forum authorization.
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:101
Invalid UTF-8 sequence in add-on name.
bool contains_hashlist(const config &from, const config &to)
Definition: validation.cpp:289
virtual std::string base64_digest() const override
Definition: hash.cpp:125
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:219
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:237
child_itors child_range(config_key_type key)
Definition: config.cpp:344
Delta for a non-existent add-on.
std::string_view data
Definition: picture.cpp:206
bool needs_escaping(char c)
Definition: validation.cpp:207
#define d
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:273
static std::string _(const char *str)
Definition: gettext.hpp:93
std::string addon_check_status_desc(unsigned int code)
Definition: validation.cpp:379
The addon&#39;s forum_auth value does not match its previously set value.
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:181
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:254
std::string file_hash(const config &file)
Definition: validation.cpp:259
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
Definition: validation.cpp:322
Markup in add-on title.
std::string translated_addon_check_status(unsigned int code)
Definition: validation.cpp:541
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
Requested forum authentication for a user that doesn&#39;t exist on the forums.
Corrupted server add-ons list.
bool comp_file_hash(const config &file_a, const config &file_b)
Definition: validation.cpp:268
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:371
#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:197
No author specified.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:60
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