The Battle for Wesnoth  1.15.0-dev
validation.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
3  2008 - 2015 by Iris Morelle <shadowm2006@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "addon/validation.hpp"
17 #include "config.hpp"
19 
20 #include <algorithm>
21 #include <array>
22 #include <boost/algorithm/string.hpp>
23 #include <set>
24 
25 const unsigned short default_campaignd_port = 15014;
26 
27 namespace {
28  const std::array<std::string, ADDON_TYPES_COUNT> addon_type_strings {{
29  "unknown", "core", "campaign", "scenario", "campaign_sp_mp", "campaign_mp",
30  "scenario_mp", "map_pack", "era", "faction", "mod_mp", /*"gui", */ "media",
31  "other"
32  }};
33 
34  // Reserved DOS device names on Windows XP and later.
35  const std::set<std::string> dos_device_names = {
36  "NUL", "CON", "AUX", "PRN",
37  // Console API devices
38  "CONIN$", "CONOUT$",
39  // Configuration-dependent devices
40  "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
41  "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
42  };
43 
44  struct addon_name_char_illegal
45  {
46  /**
47  * Returns whether the given add-on name char is not whitelisted.
48  */
49  inline bool operator()(char c) const
50  {
51  switch(c)
52  {
53  case '-': // hyphen-minus
54  case '_': // low line
55  return false;
56  default:
57  return !isalnum(c);
58  }
59  }
60  };
61 
62  struct addon_filename_ucs4char_illegal
63  {
64  inline bool operator()(char32_t c) const
65  {
66  switch(c){
67  case ' ':
68  case '"':
69  case '*':
70  case '/':
71  case ':':
72  case '<':
73  case '>':
74  case '?':
75  case '\\':
76  case '|':
77  case '~':
78  case 0x7F: // DEL
79  return true;
80  default:
81  return (
82  c < 0x20 || // C0 control characters
83  (c >= 0x80 && c < 0xA0) || // C1 control characters
84  (c >= 0xD800 && c < 0xE000) // surrogate pairs
85  );
86  }
87  }
88  };
89 }
90 
91 bool addon_name_legal(const std::string& name)
92 {
93  if(name.empty() ||
94  std::find_if(name.begin(), name.end(), addon_name_char_illegal()) != name.end()) {
95  return false;
96  } else {
97  return true;
98  }
99 }
100 
101 bool addon_filename_legal(const std::string& name)
102 {
103  if(name.empty() || name.back() == '.' ||
104  name.find("..") != std::string::npos ||
105  name.size() > 255) {
106  return false;
107  } else {
108  // NOTE: We can't use filesystem::base_name() here, since it returns
109  // the filename up to the *last* dot. "CON.foo.bar" in
110  // "CON.foo.bar.baz" is still redirected to "CON" on Windows;
111  // the base_name() approach would cause the name to not match
112  // any entries on our blacklist.
113  // Do also note that we're relying on the next check after this
114  // to flag the name as illegal if it contains a ':' -- a
115  // trailing colon is a valid way to refer to DOS device names,
116  // meaning that e.g. "CON:" is equivalent to "CON".
117  const std::string stem = boost::algorithm::to_upper_copy(name.substr(0, name.find('.')), std::locale::classic());
118  if(dos_device_names.find(stem) != dos_device_names.end()) {
119  return false;
120  }
121 
122  const std::u32string name_ucs4 = unicode_cast<std::u32string>(name);
123  const std::string name_utf8 = unicode_cast<std::string>(name_ucs4);
124  if(name != name_utf8){ // name is invalid UTF-8
125  return false;
126  }
127  return std::find_if(name_ucs4.begin(), name_ucs4.end(), addon_filename_ucs4char_illegal()) == name_ucs4.end();
128  }
129 }
130 
131 namespace {
132 
133 bool check_names_legal_internal(const config& dir, std::string current_prefix, std::vector<std::string>* badlist)
134 {
135  if (!current_prefix.empty()) {
136  current_prefix += '/';
137  }
138 
139  for(const config& path : dir.child_range("file")) {
140  const std::string& filename = path["name"];
141 
142  if(!addon_filename_legal(filename)) {
143  if(badlist) {
144  badlist->push_back(current_prefix + filename);
145  } else {
146  return false;
147  }
148  }
149  }
150 
151  for(const config& path : dir.child_range("dir")) {
152  const std::string& dirname = path["name"];
153  const std::string& new_prefix = current_prefix + dirname;
154 
155  if(!addon_filename_legal(dirname)) {
156  if(badlist) {
157  badlist->push_back(new_prefix + "/");
158  } else {
159  return false;
160  }
161  }
162 
163  // Recurse into subdir.
164  if(!check_names_legal_internal(path, new_prefix, badlist) && !badlist) {
165  return false;
166  }
167  }
168 
169  return badlist ? badlist->empty() : true;
170 }
171 
172 bool check_case_insensitive_duplicates_internal(const config& dir, std::string prefix, std::vector<std::string>* badlist){
173  typedef std::pair<bool, std::string> printed_and_original;
174  std::map<std::string, printed_and_original> filenames;
175  bool inserted;
176  bool printed;
177  std::string original;
178  for (const config &path : dir.child_range("file")) {
179  const config::attribute_value &filename = path["name"];
180  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
181  const std::string with_prefix = prefix + filename.str();
182  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::make_pair(false, with_prefix));
183  if (!inserted){
184  if(badlist){
185  std::tie(printed, original) = filenames[lowercase];
186  if(!printed){
187  badlist->push_back(original);
188  filenames[lowercase] = make_pair(true, std::string());
189  }
190  badlist->push_back(with_prefix);
191  } else {
192  return false;
193  }
194  }
195  }
196  for (const config &path : dir.child_range("dir")) {
197  const config::attribute_value &filename = path["name"];
198  const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
199  const std::string with_prefix = prefix + filename.str();
200  std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::make_pair(false, with_prefix));
201  if (!inserted) {
202  if(badlist){
203  std::tie(printed, original) = filenames[lowercase];
204  if(!printed){
205  badlist->push_back(original);
206  filenames[lowercase] = make_pair(true, std::string());
207  }
208  badlist->push_back(with_prefix);
209  } else {
210  return false;
211  }
212  }
213  if (!check_case_insensitive_duplicates_internal(path, prefix + filename + "/", badlist) && !badlist){
214  return false;
215  }
216  }
217 
218  return badlist ? badlist->empty() : true;
219 }
220 
221 } // end unnamed namespace 3
222 
223 bool check_names_legal(const config& dir, std::vector<std::string>* badlist)
224 {
225  // Usually our caller is passing us the root [dir] for an add-on, which
226  // shall contain a single subdir named after the add-on itself, so we can
227  // start with an empty display prefix and that'll reflect the addon
228  // structure correctly (e.g. "Addon_Name/~illegalfilename1").
229  return check_names_legal_internal(dir, "", badlist);
230 }
231 
232 bool check_case_insensitive_duplicates(const config& dir, std::vector<std::string>* badlist){
233  return check_case_insensitive_duplicates_internal(dir, "", badlist);
234 }
235 
236 ADDON_TYPE get_addon_type(const std::string& str)
237 {
238  if (str.empty())
239  return ADDON_UNKNOWN;
240 
241  unsigned addon_type_num = 0;
242 
243  while(++addon_type_num != ADDON_TYPES_COUNT) {
244  if(str == addon_type_strings[addon_type_num]) {
245  return ADDON_TYPE(addon_type_num);
246  }
247  }
248 
249  return ADDON_UNKNOWN;
250 }
251 
253 {
254  assert(type != ADDON_TYPES_COUNT);
255  return addon_type_strings[type];
256 }
257 
258 namespace {
259  const char escape_char = '\x01'; /**< Binary escape char. */
260 } // end unnamed namespace 2
261 
262 bool needs_escaping(char c) {
263  switch(c) {
264  case '\x00':
265  case escape_char:
266  case '\x0D': //Windows -- carriage return
267  case '\xFE': //Parser code -- textdomain or linenumber&filename
268  return true;
269  default:
270  return false;
271  }
272 }
273 
274 std::string encode_binary(const std::string& str)
275 {
276  std::string res;
277  res.resize(str.size());
278  std::size_t n = 0;
279  for(std::string::const_iterator j = str.begin(); j != str.end(); ++j) {
280  if(needs_escaping(*j)) {
281  res.resize(res.size()+1);
282  res[n++] = escape_char;
283  res[n++] = *j + 1;
284  } else {
285  res[n++] = *j;
286  }
287  }
288 
289  return res;
290 }
291 
292 std::string unencode_binary(const std::string& str)
293 {
294  std::string res(str.size(), '\0');
295 
296  std::size_t n = 0;
297  for(std::string::const_iterator j = str.begin(); j != str.end(); ) {
298  char c = *j++;
299  if((c == escape_char) && (j != str.end())) {
300  c = (*j++) - 1;
301  }
302  res[n++] = c;
303  }
304 
305  res.resize(n);
306  return res;
307 }
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:223
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:232
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:40
Variant for storing WML attributes.
std::string encode_binary(const std::string &str)
Definition: validation.cpp:274
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:292
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
child_itors child_range(config_key_type key)
Definition: config.cpp:366
bool needs_escaping(char c)
Definition: validation.cpp:262
Definitions for the interface to Wesnoth Markup Language (WML).
const t_string name
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:236
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:91
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:51
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:25
std::string get_addon_type_string(ADDON_TYPE type)
Definition: validation.cpp:252
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
mock_char c
static map_location::DIRECTION n
std::string path
File path.
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:101