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