diff --git a/src/addon/client.cpp b/src/addon/client.cpp
index f674f60b3c65..88861a975877 100644
--- a/src/addon/client.cpp
+++ b/src/addon/client.cpp
@@ -164,6 +164,13 @@ bool addons_client::upload_addon(const std::string& id, std::string& response_me
this->last_error_data_ = font::escape_text(utils::join(badnames, "\n"));
return false;
}
+ if(!check_case_insensitive_duplicates(addon_data, &badnames)){
+ this->last_error_ =
+ vgettext("The add-on $addon_title contains files or directories with case conflicts. "
+ "File or directory names may not be differently-cased versions of the same string.", i18n_symbols);
+ this->last_error_data_ = utils::join(badnames, "\n");
+ return false;
+ }
config request_buf, response_buf;
request_buf.add_child("upload", cfg).add_child("data", addon_data);
diff --git a/src/addon/validation.cpp b/src/addon/validation.cpp
index 311e9dedad3b..2a299b010d56 100644
--- a/src/addon/validation.cpp
+++ b/src/addon/validation.cpp
@@ -144,6 +144,55 @@ bool check_names_legal_internal(const config& dir, std::string current_prefix, s
return badlist ? badlist->empty() : true;
}
+bool check_case_insensitive_duplicates_internal(const config& dir, std::string prefix, std::vector* badlist){
+ typedef std::pair printed_and_original;
+ std::map filenames;
+ bool inserted;
+ bool printed;
+ std::string original;
+ for (const config &path : dir.child_range("file")) {
+ const config::attribute_value &filename = path["name"];
+ const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
+ const std::string with_prefix = prefix + filename.str();
+ std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::make_pair(false, with_prefix));
+ if (!inserted){
+ if(badlist){
+ std::tie(printed, original) = filenames[lowercase];
+ if(!printed){
+ badlist->push_back(original);
+ filenames[lowercase] = make_pair(true, std::string());
+ }
+ badlist->push_back(with_prefix);
+ } else {
+ return false;
+ }
+ }
+ }
+ for (const config &path : dir.child_range("dir")) {
+ const config::attribute_value &filename = path["name"];
+ const std::string lowercase = boost::algorithm::to_lower_copy(filename.str(), std::locale::classic());
+ const std::string with_prefix = prefix + filename.str();
+ std::tie(std::ignore, inserted) = filenames.emplace(lowercase, std::make_pair(false, with_prefix));
+ if (!inserted) {
+ if(badlist){
+ std::tie(printed, original) = filenames[lowercase];
+ if(!printed){
+ badlist->push_back(original);
+ filenames[lowercase] = make_pair(true, std::string());
+ }
+ badlist->push_back(with_prefix);
+ } else {
+ return false;
+ }
+ }
+ if (!check_case_insensitive_duplicates_internal(path, prefix + filename + "/", badlist) && !badlist){
+ return false;
+ }
+ }
+
+ return badlist ? badlist->empty() : true;
+}
+
} // end unnamed namespace 3
bool check_names_legal(const config& dir, std::vector* badlist)
@@ -155,21 +204,8 @@ bool check_names_legal(const config& dir, std::vector* badlist)
return check_names_legal_internal(dir, "", badlist);
}
-bool check_case_insensitive_duplicates(const config& dir){
- std::set filenames;
- bool inserted;
- for (const config &path : dir.child_range("file")) {
- const config::attribute_value &filename = path["name"];
- std::tie(std::ignore, inserted) = filenames.insert(boost::algorithm::to_lower_copy(filename.str(), std::locale::classic()));
- if (!inserted) return false;
- }
- for (const config &path : dir.child_range("dir")) {
- const config::attribute_value &filename = path["name"];
- std::tie(std::ignore, inserted) = filenames.insert(boost::algorithm::to_lower_copy(filename.str(), std::locale::classic()));
- if (!inserted) return false;
- if (!check_case_insensitive_duplicates(path)) return false;
- }
- return true;
+bool check_case_insensitive_duplicates(const config& dir, std::vector* badlist){
+ return check_case_insensitive_duplicates_internal(dir, "", badlist);
}
ADDON_TYPE get_addon_type(const std::string& str)
diff --git a/src/addon/validation.hpp b/src/addon/validation.hpp
index 4c25621ade91..009cb56e581b 100644
--- a/src/addon/validation.hpp
+++ b/src/addon/validation.hpp
@@ -77,8 +77,21 @@ bool addon_filename_legal(const std::string& name);
* @returns True if no illegal names were found.
*/
bool check_names_legal(const config& dir, std::vector* badlist = nullptr);
-/** Probes an add-on archive for case-conflicts on case-insensitive filesystems. */
-bool check_case_insensitive_duplicates(const config& dir);
+/**
+ * Scans an add-on archive for case-conflicts.
+ *
+ * Case conflicts may cause trouble on case-insensitive filesystems.
+ *
+ * @param dir The WML container for the root [dir] node where the search
+ * should begin.
+ * @param badlist If provided and not null, any case conflicts encountered will
+ * be added to this list. This also makes the archive scan more
+ * thorough instead of returning on the first conflict
+ * encountered.
+ *
+ * @returns True if no conflicts were found.
+ */
+bool check_case_insensitive_duplicates(const config& dir, std::vector* badlist = nullptr);
std::string encode_binary(const std::string& str);
std::string unencode_binary(const std::string& str);
diff --git a/src/campaign_server/campaign_server.cpp b/src/campaign_server/campaign_server.cpp
index ee2ef99773af..5622ad3a5c93 100644
--- a/src/campaign_server/campaign_server.cpp
+++ b/src/campaign_server/campaign_server.cpp
@@ -693,9 +693,13 @@ void server::handle_upload(const server::request& req)
"File or directory names may not contain whitespace, control characters or any of the following characters: '\" * / : < > ? \\ | ~'. "
"It also may not contain '..' end with '.' or be longer than 255 characters.",
filelist, req.sock);
- } else if(!check_case_insensitive_duplicates(data)) {
- LOG_CS << "Upload aborted - case conflict in add-on data.\n";
- send_error("Add-on rejected: Two files have a case conflict", req.sock);
+ } else if(!check_case_insensitive_duplicates(data, &badnames)) {
+ const std::string& filelist = utils::join(badnames, "\n");
+ LOG_CS << "Upload aborted - case conflict in add-on data (" << badnames.size() << " entries).\n";
+ send_error(
+ "Add-on rejected: The add-on contains files or directories with case conflicts. "
+ "File or directory names may not be differently-cased versions of the same string.",
+ filelist, req.sock);
} else if(campaign && !authenticate(*campaign, upload["passphrase"])) {
LOG_CS << "Upload aborted - incorrect passphrase.\n";
send_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.sock);