Browse Source

Sanitize world directory names on create. Keep original name separate (#9432)

Blacklisted characters are replaced by '_' in the path. The display name is stored in world.mt, and duplicate file names are resolved by adding an incrementing suffix (_1, _2, _3, etc).
Hugues Ross 3 years ago
parent
commit
3ce03d1c2a
6 changed files with 136 additions and 17 deletions
  1. 44 10
      src/content/subgames.cpp
  2. 4 1
      src/content/subgames.h
  3. 5 4
      src/script/lua_api/l_mainmenu.cpp
  4. 7 2
      src/server.cpp
  5. 68 0
      src/util/string.cpp
  6. 8 0
      src/util/string.h

+ 44 - 10
src/content/subgames.cpp

@@ -31,6 +31,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "client/tile.h" // getImagePath
 #endif
 
+// The maximum number of identical world names allowed
+#define MAX_WORLD_NAMES 100
+
 bool getGameMinetestConfig(const std::string &game_path, Settings &conf)
 {
 	std::string conf_path = game_path + DIR_DELIM + "minetest.conf";
@@ -213,6 +216,21 @@ bool getWorldExists(const std::string &world_path)
 			fs::PathExists(world_path + DIR_DELIM + "world.mt"));
 }
 
+//! Try to get the displayed name of a world
+std::string getWorldName(const std::string &world_path, const std::string &default_name)
+{
+	std::string conf_path = world_path + DIR_DELIM + "world.mt";
+	Settings conf;
+	bool succeeded = conf.readConfigFile(conf_path.c_str());
+	if (!succeeded) {
+		return default_name;
+	}
+
+	if (!conf.exists("world_name"))
+		return default_name;
+	return conf.get("world_name");
+}
+
 std::string getWorldGameId(const std::string &world_path, bool can_be_legacy)
 {
 	std::string conf_path = world_path + DIR_DELIM + "world.mt";
@@ -259,7 +277,7 @@ std::vector<WorldSpec> getAvailableWorlds()
 			if (!dln.dir)
 				continue;
 			std::string fullpath = worldspath + DIR_DELIM + dln.name;
-			std::string name = dln.name;
+			std::string name = getWorldName(fullpath, dln.name);
 			// Just allow filling in the gameid always for now
 			bool can_be_legacy = true;
 			std::string gameid = getWorldGameId(fullpath, can_be_legacy);
@@ -288,8 +306,24 @@ std::vector<WorldSpec> getAvailableWorlds()
 	return worlds;
 }
 
-bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamespec)
+void loadGameConfAndInitWorld(const std::string &path, const std::string &name,
+		const SubgameSpec &gamespec, bool create_world)
 {
+	std::string final_path = path;
+
+	// If we're creating a new world, ensure that the path isn't already taken
+	if (create_world) {
+		int counter = 1;
+		while (fs::PathExists(final_path) && counter < MAX_WORLD_NAMES) {
+			final_path = path + "_" + std::to_string(counter);
+			counter++;
+		}
+
+		if (fs::PathExists(final_path)) {
+			throw BaseException("Too many similar filenames");
+		}
+	}
+
 	// Override defaults with those provided by the game.
 	// We clear and reload the defaults because the defaults
 	// might have been overridden by other subgame config
@@ -300,15 +334,16 @@ bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamesp
 	getGameMinetestConfig(gamespec.path, game_defaults);
 	g_settings->overrideDefaults(&game_defaults);
 
-	infostream << "Initializing world at " << path << std::endl;
+	infostream << "Initializing world at " << final_path << std::endl;
 
-	fs::CreateAllDirs(path);
+	fs::CreateAllDirs(final_path);
 
 	// Create world.mt if does not already exist
-	std::string worldmt_path = path + DIR_DELIM "world.mt";
+	std::string worldmt_path = final_path + DIR_DELIM "world.mt";
 	if (!fs::PathExists(worldmt_path)) {
 		Settings conf;
 
+		conf.set("world_name", name);
 		conf.set("gameid", gamespec.id);
 		conf.set("backend", "sqlite3");
 		conf.set("player_backend", "sqlite3");
@@ -316,16 +351,16 @@ bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamesp
 		conf.setBool("creative_mode", g_settings->getBool("creative_mode"));
 		conf.setBool("enable_damage", g_settings->getBool("enable_damage"));
 
-		if (!conf.updateConfigFile(worldmt_path.c_str()))
-			return false;
+		if (!conf.updateConfigFile(worldmt_path.c_str())) {
+			throw BaseException("Failed to update the config file");
+		}
 	}
 
 	// Create map_meta.txt if does not already exist
-	std::string map_meta_path = path + DIR_DELIM + "map_meta.txt";
+	std::string map_meta_path = final_path + DIR_DELIM + "map_meta.txt";
 	if (!fs::PathExists(map_meta_path)) {
 		verbosestream << "Creating map_meta.txt (" << map_meta_path << ")"
 			      << std::endl;
-		fs::CreateAllDirs(path);
 		std::ostringstream oss(std::ios_base::binary);
 
 		Settings conf;
@@ -338,5 +373,4 @@ bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamesp
 
 		fs::safeWriteToFile(map_meta_path, oss.str());
 	}
-	return true;
 }

+ 4 - 1
src/content/subgames.h

@@ -63,6 +63,8 @@ std::set<std::string> getAvailableGameIds();
 std::vector<SubgameSpec> getAvailableGames();
 
 bool getWorldExists(const std::string &world_path);
+//! Try to get the displayed name of a world
+std::string getWorldName(const std::string &world_path, const std::string &default_name);
 std::string getWorldGameId(const std::string &world_path, bool can_be_legacy = false);
 
 struct WorldSpec
@@ -88,4 +90,5 @@ std::vector<WorldSpec> getAvailableWorlds();
 
 // loads the subgame's config and creates world directory
 // and world.mt if they don't exist
-bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamespec);
+void loadGameConfAndInitWorld(const std::string &path, const std::string &name,
+		const SubgameSpec &gamespec, bool create_world);

+ 5 - 4
src/script/lua_api/l_mainmenu.cpp

@@ -618,7 +618,7 @@ int ModApiMainMenu::l_create_world(lua_State *L)
 
 	std::string path = porting::path_user + DIR_DELIM
 			"worlds" + DIR_DELIM
-			+ name;
+			+ sanitizeDirName(name, "world_");
 
 	std::vector<SubgameSpec> games = getAvailableGames();
 
@@ -626,10 +626,11 @@ int ModApiMainMenu::l_create_world(lua_State *L)
 			(gameidx < (int) games.size())) {
 
 		// Create world if it doesn't exist
-		if (!loadGameConfAndInitWorld(path, games[gameidx])) {
-			lua_pushstring(L, "Failed to initialize world");
-		} else {
+		try {
+			loadGameConfAndInitWorld(path, name, games[gameidx], true);
 			lua_pushnil(L);
+		} catch (const BaseException &e) {
+			lua_pushstring(L, (std::string("Failed to initialize world: ") + e.what()).c_str());
 		}
 	} else {
 		lua_pushstring(L, "Invalid game index");

+ 7 - 2
src/server.cpp

@@ -356,8 +356,13 @@ void Server::init()
 	infostream << "- game:   " << m_gamespec.path << std::endl;
 
 	// Create world if it doesn't exist
-	if (!loadGameConfAndInitWorld(m_path_world, m_gamespec))
-		throw ServerError("Failed to initialize world");
+	try {
+		loadGameConfAndInitWorld(m_path_world,
+				fs::GetFilenameFromPath(m_path_world.c_str()),
+				m_gamespec, false);
+	} catch (const BaseException &e) {
+		throw ServerError(std::string("Failed to initialize world: ") + e.what());
+	}
 
 	// Create emerge manager
 	m_emerge = new EmergeManager(this);

+ 68 - 0
src/util/string.cpp

@@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "translation.h"
 
 #include <algorithm>
+#include <array>
 #include <sstream>
 #include <iomanip>
 #include <map>
@@ -889,3 +890,70 @@ std::wstring translate_string(const std::wstring &s)
 	return translate_string(s, g_client_translations);
 #endif
 }
+
+static const std::array<std::wstring, 22> disallowed_dir_names = {
+	// Problematic filenames from here:
+	// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#file-and-directory-names
+	L"CON",
+	L"PRN",
+	L"AUX",
+	L"NUL",
+	L"COM1",
+	L"COM2",
+	L"COM3",
+	L"COM4",
+	L"COM5",
+	L"COM6",
+	L"COM7",
+	L"COM8",
+	L"COM9",
+	L"LPT1",
+	L"LPT2",
+	L"LPT3",
+	L"LPT4",
+	L"LPT5",
+	L"LPT6",
+	L"LPT7",
+	L"LPT8",
+	L"LPT9",
+};
+
+/**
+ * List of characters that are blacklisted from created directories
+ */
+static const std::wstring disallowed_path_chars = L"<>:\"/\\|?*.";
+
+/**
+ * Sanitize the name of a new directory. This consists of two stages:
+ * 1. Check for 'reserved filenames' that can't be used on some filesystems
+ *	and add a prefix to them
+ * 2. Remove 'unsafe' characters from the name by replacing them with '_'
+ */
+std::string sanitizeDirName(const std::string &str, const std::string &optional_prefix)
+{
+	std::wstring safe_name = utf8_to_wide(str);
+
+	for (std::wstring disallowed_name : disallowed_dir_names) {
+		if (str_equal(safe_name, disallowed_name, true)) {
+			safe_name = utf8_to_wide(optional_prefix) + safe_name;
+			break;
+		}
+	}
+
+	for (unsigned long i = 0; i < safe_name.length(); i++) {
+		bool is_valid = true;
+
+		// Unlikely, but control characters should always be blacklisted
+		if (safe_name[i] < 32) {
+			is_valid = false;
+		} else if (safe_name[i] < 128) {
+			is_valid = disallowed_path_chars.find_first_of(safe_name[i])
+					== std::wstring::npos;
+		}
+
+		if (!is_valid)
+			safe_name[i] = '_';
+	}
+
+	return wide_to_utf8(safe_name);
+}

+ 8 - 0
src/util/string.h

@@ -746,3 +746,11 @@ inline irr::core::stringw utf8_to_stringw(const std::string &input)
 	std::wstring str = utf8_to_wide(input);
 	return irr::core::stringw(str.c_str());
 }
+
+/**
+ * Sanitize the name of a new directory. This consists of two stages:
+ * 1. Check for 'reserved filenames' that can't be used on some filesystems
+ *    and prefix them
+ * 2. Remove 'unsafe' characters from the name by replacing them with '_'
+ */
+std::string sanitizeDirName(const std::string &str, const std::string &optional_prefix);