The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
validation.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
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
27const unsigned short default_campaignd_port = 15020;
28
29namespace
30{
31
32const 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
37struct 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
56bool 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
66bool 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
74bool addon_icon_too_large(const std::string& icon) {
75 return icon.size() > max_icon_size;
76}
77
78namespace {
79
80bool check_names_legal_internal(const config& dir, std::string current_prefix, std::vector<std::string>* badlist)
81{
82 if (!current_prefix.empty()) {
83 current_prefix += '/';
84 }
85
86 for(const config& path : dir.child_range("file")) {
87 const std::string& filename = path["name"];
88
90 if(badlist) {
91 badlist->push_back(current_prefix + filename);
92 } else {
93 return false;
94 }
95 }
96 }
97
98 for(const config& path : dir.child_range("dir")) {
99 const std::string& dirname = path["name"];
100 const std::string& new_prefix = current_prefix + dirname;
101
102 if(!addon_filename_legal(dirname)) {
103 if(badlist) {
104 badlist->push_back(new_prefix + "/");
105 } else {
106 return false;
107 }
108 }
109
110 // Recurse into subdir.
111 if(!check_names_legal_internal(path, new_prefix, badlist) && !badlist) {
112 return false;
113 }
114 }
115
116 return badlist ? badlist->empty() : true;
117}
118
119bool check_case_insensitive_duplicates_internal(const config& dir, const std::string& prefix, std::vector<std::string>* badlist){
120 typedef std::pair<bool, std::string> printed_and_original;
121 std::map<std::string, printed_and_original> filenames;
122 bool inserted;
123 bool printed;
124 std::string original;
125 for (const config &path : dir.child_range("file")) {
126 const config::attribute_value &filename = path["name"];
127 const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
128 const std::string with_prefix = prefix + filename.str();
129 std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
130 if (!inserted){
131 if(badlist){
132 std::tie(printed, original) = filenames[lowercase];
133 if(!printed){
134 badlist->push_back(original);
135 filenames[lowercase] = make_pair(true, std::string());
136 }
137 badlist->push_back(with_prefix);
138 } else {
139 return false;
140 }
141 }
142 }
143 for (const config &path : dir.child_range("dir")) {
144 const config::attribute_value &filename = path["name"];
145 const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
146 const std::string with_prefix = prefix + filename.str();
147 std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::pair(false, with_prefix));
148 if (!inserted) {
149 if(badlist){
150 std::tie(printed, original) = filenames[lowercase];
151 if(!printed){
152 badlist->push_back(original);
153 filenames[lowercase] = make_pair(true, std::string());
154 }
155 badlist->push_back(with_prefix);
156 } else {
157 return false;
158 }
159 }
160 if(!check_case_insensitive_duplicates_internal(path, with_prefix + "/", badlist) && !badlist) {
161 return false;
162 }
163 }
164
165 return badlist ? badlist->empty() : true;
166}
167
168} // end unnamed namespace 3
169
170bool check_names_legal(const config& dir, std::vector<std::string>* badlist)
171{
172 // Usually our caller is passing us the root [dir] for an add-on, which
173 // shall contain a single subdir named after the add-on itself, so we can
174 // start with an empty display prefix and that'll reflect the addon
175 // structure correctly (e.g. "Addon_Name/~illegalfilename1").
176 return check_names_legal_internal(dir, "", badlist);
177}
178
179bool check_case_insensitive_duplicates(const config& dir, std::vector<std::string>* badlist)
180{
181 return check_case_insensitive_duplicates_internal(dir, "", badlist);
182}
183
184ADDON_TYPE get_addon_type(const std::string& str)
185{
186 if (str.empty())
187 return ADDON_UNKNOWN;
188
189 unsigned addon_type_num = 0;
190
191 while(++addon_type_num != ADDON_TYPES_COUNT) {
192 if(str == addon_type_strings[addon_type_num]) {
193 return ADDON_TYPE(addon_type_num);
194 }
195 }
196
197 return ADDON_UNKNOWN;
198}
199
201{
202 assert(type != ADDON_TYPES_COUNT);
203 return addon_type_strings[type];
204}
205
206namespace {
207 const char escape_char = '\x01'; /**< Binary escape char. */
208} // end unnamed namespace 2
209
210bool needs_escaping(char c) {
211 switch(c) {
212 case '\x00':
213 case escape_char:
214 case '\x0D': //Windows -- carriage return
215 case '\xFE': //Parser code -- textdomain or linenumber&filename
216 return true;
217 default:
218 return false;
219 }
220}
221
222std::string encode_binary(const std::string& str)
223{
224 std::string res;
225 res.resize(str.size());
226 std::size_t n = 0;
227 for(std::string::const_iterator j = str.begin(); j != str.end(); ++j) {
228 if(needs_escaping(*j)) {
229 res.resize(res.size()+1);
230 res[n++] = escape_char;
231 res[n++] = *j + 1;
232 } else {
233 res[n++] = *j;
234 }
235 }
236
237 return res;
238}
239
240std::string unencode_binary(const std::string& str)
241{
242 std::string res(str.size(), '\0');
243
244 std::size_t n = 0;
245 for(std::string::const_iterator j = str.begin(); j != str.end(); ) {
246 char c = *j++;
247 if((c == escape_char) && (j != str.end())) {
248 c = (*j++) - 1;
249 }
250 res[n++] = c;
251 }
252
253 res.resize(n);
254 return res;
255}
256
257static std::string file_hash_raw(const config& file)
258{
259 return utils::md5(file["contents"].str()).base64_digest();
260}
261
262std::string file_hash(const config& file)
263{
264 std::string hash = file["hash"].str();
265 if(hash.empty()) {
266 hash = file_hash_raw(file);
267 }
268 return hash;
269}
270
271bool comp_file_hash(const config& file_a, const config& file_b)
272{
273 return file_a["name"] == file_b["name"] && file_hash(file_a) == file_hash(file_b);
274}
275
276void write_hashlist(config& hashlist, const config& data)
277{
278 hashlist["name"] = data["name"];
279
280 for(const config& f : data.child_range("file")) {
281 config& file = hashlist.add_child("file");
282 file["name"] = f["name"];
283 file["hash"] = file_hash_raw(f);
284 }
285
286 for(const config& d : data.child_range("dir")) {
287 config& dir = hashlist.add_child("dir");
288 write_hashlist(dir, d);
289 }
290}
291
292bool contains_hashlist(const config& from, const config& to)
293{
294 for(const config& f : to.child_range("file")) {
295 bool found = false;
296 for(const config& d : from.child_range("file")) {
297 found |= comp_file_hash(f, d);
298 if(found)
299 break;
300 }
301 if(!found) {
302 return false;
303 }
304 }
305
306 for(const config& d : to.child_range("dir")) {
307 auto origin_dir = from.find_child("dir", "name", d["name"]);
308 if(origin_dir) {
309 if(!contains_hashlist(*origin_dir, d)) {
310 return false;
311 }
312 } else {
313 // The case of empty new subdirectories
314 const config dummy_dir = config("name", d["name"]);
315 if(!contains_hashlist(dummy_dir, d)) {
316 return false;
317 }
318 }
319 }
320
321 return true;
322}
323
324/** Surround with [dir][/dir] */
325static bool write_difference(config& pack, const config& from, const config& to, bool with_content)
326{
327 pack["name"] = to["name"];
328 bool has_changes = false;
329
330 for(const config& f : to.child_range("file")) {
331 bool found = false;
332 for(const config& d : from.child_range("file")) {
333 found |= comp_file_hash(f, d);
334 if(found)
335 break;
336 }
337 if(!found) {
338 config& file = pack.add_child("file");
339 file["name"] = f["name"];
340 if(with_content) {
341 file["contents"] = f["contents"];
342 file["hash"] = file_hash(f);
343 }
344 has_changes = true;
345 }
346 }
347
348 for(const config& d : to.child_range("dir")) {
349 auto origin_dir = from.find_child("dir", "name", d["name"]);
350 config dir;
351 if(origin_dir) {
352 if(write_difference(dir, *origin_dir, d, with_content)) {
353 pack.add_child("dir", dir);
354 has_changes = true;
355 }
356 } else {
357 const config dummy_dir = config("name", d["name"]);
358 if(write_difference(dir, dummy_dir, d, with_content)) {
359 pack.add_child("dir", dir);
360 has_changes = true;
361 }
362 }
363 }
364
365 return has_changes;
366}
367
368/**
369 * &from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir]
370 *
371 * Does it worth it to archive and write the pack on the fly using config_writer?
372 * TODO: clientside verification?
373 */
374void make_updatepack(config& pack, const config& from, const config& to)
375{
376 config& removelist = pack.add_child("removelist");
377 write_difference(removelist, to, from, false);
378 config& addlist = pack.add_child("addlist");
379 write_difference(addlist, from, to, true);
380}
381
382std::string addon_check_status_desc(unsigned int code)
383{
384 static const std::map<ADDON_CHECK_STATUS, std::string> message_table = {
385
386 //
387 // General errors
388 //
389
390 {
392 N_("Success.")
393 },
394 {
396 N_("Incorrect add-on passphrase.")
397 },
398 {
400 N_("Forum authentication was requested for a user that is not registered on the forums.")
401 },
402 {
404 N_("Upload denied. Please contact the server administration for assistance.")
405 },
406 {
408 N_("Attempted to upload an update pack for a non-existent add-on.")
409 },
410
411 //
412 // Structure errors
413 //
414
415 {
417 N_("No add-on data was supplied by the client.")
418 },
419 {
421 N_("Invalid upload pack.")
422 },
423 {
425 N_("Invalid add-on name.")
426 },
427 {
429 N_("Formatting character in add-on name.")
430 },
431 {
433 N_("The add-on contains files or directories with illegal names.\n"
434 "\n"
435 "Names containing whitespace, control characters, or any of the following symbols are not allowed:\n"
436 "\n"
437 " \" * / : < > ? \\ | ~\n"
438 "\n"
439 "Additionally, names may not be longer than 255 characters, contain '..', or end with '.'.")
440 },
441 {
443 N_("The add-on contains files or directories with case conflicts.\n"
444 "\n"
445 "Names in the same directory may not be differently-cased versions of each other.")
446 },
447 {
449 N_("The add-on name contains an invalid UTF-8 sequence.")
450 },
451
452 //
453 // .pbl errors
454 //
455
456 {
458 N_("No add-on title specified.")
459 },
460 {
462 N_("No add-on author/maintainer name specified.")
463 },
464 {
466 N_("No add-on version specified.")
467 },
468 {
470 N_("No add-on description specified.")
471 },
472 {
474 N_("No add-on author/maintainer email specified.")
475 },
476 {
478 N_("Missing passphrase.")
479 },
480 {
482 N_("Formatting character in add-on title.")
483 },
484 {
486 N_("Invalid or unspecified add-on type.")
487 },
488 {
490 N_("Version number not greater than the latest uploaded version.")
491 },
492 {
494 N_("Feedback topic id is not a number.")
495 },
496 {
498 N_("Feedback topic does not exist.")
499 },
500 {
502 N_("The add-on publish information contains an invalid UTF-8 sequence.")
503 },
504 {
506 N_("The add-on’s forum_auth attribute does not match what was previously uploaded.")
507 },
508 {
510 N_("The add-on’s icon’s file size is too large.")
511 },
512
513 //
514 // Server errors
515 //
516
517 {
519 N_("Unspecified server error.")
520 },
521 {
523 N_("Server is in read-only mode.")
524 },
525 {
527 N_("Corrupted server add-ons list.")
528 },
529 {
531 N_("Empty add-on version list on the server.")
532 },
533 {
535 N_("This server does not support using the forum_auth attribute in your pbl.")
536 }
537 };
538
539 for(const auto& entry : message_table) {
540 if(static_cast<unsigned int>(entry.first) == code) {
541 return entry.second;
542 }
543 }
544
545 return N_("Unspecified validation failure.");
546};
547
548std::string translated_addon_check_status(unsigned int code)
549{
550 return _(addon_check_status_desc(code).c_str());
551}
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
child_itors child_range(std::string_view key)
Definition config.cpp:268
optional_config_impl< config > find_child(std::string_view 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:764
virtual std::string base64_digest() const override
Definition hash.cpp:124
Definitions for the interface to Wesnoth Markup Language (WML).
Declarations for File-IO.
#define N_(String)
Definition gettext.hpp:108
static std::string _(const char *str)
Definition gettext.hpp:100
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
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
Definition unicode.cpp:50
std::string_view data
Definition picture.cpp:188
std::string filename
Filename.
static map_location::direction n
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
bool addon_icon_too_large(const std::string &icon)
Checks whether an add-on icon is too large.
bool needs_escaping(char c)
std::string file_hash(const config &file)
std::string unencode_binary(const std::string &str)
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]
static bool write_difference(config &pack, const config &from, const config &to, bool with_content)
Surround with [dir][/dir].
ADDON_TYPE get_addon_type(const std::string &str)
bool addon_filename_legal(const std::string &name)
Checks whether an add-on file name is legal or not.
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
void write_hashlist(config &hashlist, const config &data)
bool contains_hashlist(const config &from, const config &to)
const unsigned short default_campaignd_port
Default port number for the addon server.
std::string addon_check_status_desc(unsigned int code)
static std::string file_hash_raw(const config &file)
std::string translated_addon_check_status(unsigned int code)
std::string encode_binary(const std::string &str)
bool comp_file_hash(const config &file_a, const config &file_b)
std::string get_addon_type_string(ADDON_TYPE type)
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
constexpr std::size_t max_icon_size
@ 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.
@ ICON_TOO_LARGE
The add-on's icon is too large (presumably a DataURI)
@ 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...
@ ADDON_UNKNOWN
a.k.a.
@ ADDON_TYPES_COUNT
#define d
#define f
#define c