From bee55ce5bb7fb157bb178df252185c46c2f193a9 Mon Sep 17 00:00:00 2001 From: gibbed Date: Sun, 4 Aug 2019 07:09:05 -0500 Subject: [PATCH] [Base/Core] Work on config/cvars. Properly escape string values when saving config. --- src/xenia/base/cvar.cc | 118 +++++++++++++++++++++++++++++++++++++++++ src/xenia/base/cvar.h | 64 +++++++++++----------- src/xenia/config.cc | 52 +++++++++++------- 3 files changed, 185 insertions(+), 49 deletions(-) diff --git a/src/xenia/base/cvar.cc b/src/xenia/base/cvar.cc index 3300a0b7c..7ac498219 100644 --- a/src/xenia/base/cvar.cc +++ b/src/xenia/base/cvar.cc @@ -1,6 +1,7 @@ #include "cvar.h" namespace cvar { + cxxopts::Options options("xenia", "Xbox 360 Emulator"); std::map* CmdVars; std::map* ConfigVars; @@ -53,4 +54,121 @@ void ParseLaunchArguments(int argc, char** argv) { } } +namespace toml { + +std::string EscapeBasicString(const std::string& str) { + std::string result; + for (auto c : str) { + if (c == '\b') { + result += "\\b"; + } else if (c == '\t') { + result += "\\t"; + } else if (c == '\n') { + result += "\\n"; + } else if (c == '\f') { + result += "\\f"; + } else if (c == '\r') { + result += "\\r"; + } else if (c == '"') { + result += "\\\""; + } else if (c == '\\') { + result += "\\\\"; + } else if (static_cast(c) < 0x20 || + static_cast(c) == 0x7F) { + auto v = static_cast(c); + int w; + if (v <= 0xFFFF) { + result += "\\u"; + w = 4; + } else { + result += "\\U"; + w = 8; + } + std::stringstream ss; + ss << std::hex << std::setw(w) << std::setfill('0') << v; + result += ss.str(); + } else { + result += c; + } + } + return result; +} + +std::string EscapeMultilineBasicString(const std::string& str) { + const std::string three_escaped_quotes("\\\"\\\"\\\""); + std::string result; + char lc = '\0'; + int quote_run = 0; + for (char c : str) { + if (c == '\b') { + result += "\\b"; + } else if (c == '\t' || c == '\n') { + result += c; + } else if (c == '\f') { + result += "\\f"; + } else if (c == '\r') { + // Silently drop \r. + // result += c; + } else if (c == '"') { + result += '"'; + if (lc == '"') { + ++quote_run; + if (quote_run >= 3) { + result.resize(result.size() - 3); + result += three_escaped_quotes; + quote_run = 0; + } + } + } else if (c == '\\') { + result += "\\\\"; + } else if (static_cast(c) < 0x20 || + static_cast(c) == 0x7F) { + auto v = static_cast(c); + int w; + if (v <= 0xFFFF) { + result += "\\u"; + w = 4; + } else { + result += "\\U"; + w = 8; + } + std::stringstream ss; + ss << std::hex << std::setw(w) << std::setfill('0') << v; + result += ss.str(); + } else { + result += c; + } + lc = c; + } + return result; +} + +std::string EscapeString(const std::string& val) { + const char multiline_chars[] = "\r\n"; + const char escape_chars[] = + "\0\b\v\f" + "\x01\x02\x03\x04\x05\x06\x07\x0E\x0F" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" + "'" + "\x7F"; + if (val.find_first_of("\r\n") == std::string::npos) { + // single line + if (val.find_first_of(multiline_chars) == std::string::npos) { + return "'" + val + "'"; + } else { + return "\"" + toml::EscapeBasicString(val) + "\""; + } + } else { + // multi line + if (val.find_first_of(escape_chars) == std::string::npos && + val.find("'''") == std::string::npos) { + return "'''\n" + val + "'''"; + } else { + return "\"\"\"\n" + toml::EscapeMultilineBasicString(val) + "\"\"\""; + } + } +} + +} // namespace toml + } // namespace cvar diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index deeec67f0..ce3a96af2 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -7,6 +7,10 @@ #include "xenia/base/string_util.h" namespace cvar { +namespace toml { +std::string EscapeString(const std::string& str); +} + class ICommandVar { public: virtual ~ICommandVar() = default; @@ -148,7 +152,7 @@ inline std::string CommandVar::ToString(bool val) { } template <> inline std::string CommandVar::ToString(std::string val) { - return "\"" + val + "\""; + return toml::EscapeString(val); } template @@ -203,52 +207,52 @@ inline void AddCommandVar(ICommandVar* cv) { void ParseLaunchArguments(int argc, char** argv); template -T* define_configvar(const char* name, T* defaultValue, const char* description, +T* define_configvar(const char* name, T* default_value, const char* description, const char* category, bool is_transient) { - IConfigVar* cfgVar = - new ConfigVar(name, defaultValue, description, category, is_transient); + IConfigVar* cfgVar = new ConfigVar(name, default_value, description, + category, is_transient); AddConfigVar(cfgVar); - return defaultValue; + return default_value; } template -T* define_cmdvar(const char* name, T* defaultValue, const char* description) { - ICommandVar* cmdVar = new CommandVar(name, defaultValue, description); +T* define_cmdvar(const char* name, T* default_value, const char* description) { + ICommandVar* cmdVar = new CommandVar(name, default_value, description); AddCommandVar(cmdVar); - return defaultValue; + return default_value; } -#define DEFINE_double(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, false, double) +#define DEFINE_double(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, double) -#define DEFINE_int32(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, false, int32_t) +#define DEFINE_int32(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, int32_t) -#define DEFINE_uint64(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, false, uint64_t) +#define DEFINE_uint64(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, uint64_t) -#define DEFINE_string(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, false, std::string) +#define DEFINE_string(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, std::string) -#define DEFINE_transient_string(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, true, std::string) +#define DEFINE_transient_string(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, true, std::string) -#define DEFINE_bool(name, defaultValue, description, category) \ - DEFINE_CVar(name, defaultValue, description, category, false, bool) +#define DEFINE_bool(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, bool) -#define DEFINE_CVar(name, defaultValue, description, category, is_transient, \ - type) \ - namespace cvars { \ - type name = defaultValue; \ - } \ - namespace cv { \ - static auto cv_##name = cvar::define_configvar( \ - #name, &cvars::name, description, category, is_transient); \ +#define DEFINE_CVar(name, default_value, description, category, is_transient, \ + type) \ + namespace cvars { \ + type name = default_value; \ + } \ + namespace cv { \ + static auto cv_##name = cvar::define_configvar( \ + #name, &cvars::name, description, category, is_transient); \ } // CmdVars can only be strings for now, we don't need any others -#define CmdVar(name, defaultValue, description) \ +#define CmdVar(name, default_value, description) \ namespace cvars { \ - std::string name = defaultValue; \ + std::string name = default_value; \ } \ namespace cv { \ static auto cv_##name = \ diff --git a/src/xenia/config.cc b/src/xenia/config.cc index 899170014..d9a58f67f 100644 --- a/src/xenia/config.cc +++ b/src/xenia/config.cc @@ -43,10 +43,10 @@ std::shared_ptr ParseConfig(const std::wstring& config_path) { void ReadConfig(const std::wstring& file_path) { const auto config = ParseConfig(file_path); for (auto& it : *cvar::ConfigVars) { - auto configVar = static_cast(it.second); - auto configKey = configVar->category() + "." + configVar->name(); - if (config->contains_qualified(configKey)) { - configVar->LoadConfigValue(config->get_qualified(configKey)); + auto config_var = static_cast(it.second); + auto config_key = config_var->category() + "." + config_var->name(); + if (config->contains_qualified(config_key)) { + config_var->LoadConfigValue(config->get_qualified(config_key)); } } XELOGI("Loaded config: %S", file_path.c_str()); @@ -55,10 +55,10 @@ void ReadConfig(const std::wstring& file_path) { void ReadGameConfig(std::wstring file_path) { const auto config = ParseConfig(file_path); for (auto& it : *cvar::ConfigVars) { - auto configVar = static_cast(it.second); - auto configKey = configVar->category() + "." + configVar->name(); - if (config->contains_qualified(configKey)) { - configVar->LoadGameConfigValue(config->get_qualified(configKey)); + auto config_var = static_cast(it.second); + auto config_key = config_var->category() + "." + config_var->name(); + if (config->contains_qualified(config_key)) { + config_var->LoadGameConfigValue(config->get_qualified(config_key)); } } XELOGI("Loaded game config: %S", file_path.c_str()); @@ -71,22 +71,36 @@ void SaveConfig() { // we use our own write logic because cpptoml doesn't // allow us to specify comments :( std::ostringstream output; - std::string lastCategory; - for (auto configVar : vars) { - if (configVar->is_transient()) { + std::string last_category; + for (auto config_var : vars) { + if (config_var->is_transient()) { continue; } - if (lastCategory != configVar->category()) { - if (!lastCategory.empty()) { + if (last_category != config_var->category()) { + if (!last_category.empty()) { output << std::endl; } - lastCategory = configVar->category(); - output << xe::format_string("[%s]\n", lastCategory.c_str()); + last_category = config_var->category(); + output << xe::format_string("[%s]\n", last_category.c_str()); } - output << std::left << std::setw(40) << std::setfill(' ') - << xe::format_string("%s = %s", configVar->name().c_str(), - configVar->config_value().c_str()); - output << xe::format_string("\t# %s\n", configVar->description().c_str()); + + auto value = config_var->config_value(); + if (value.find('\n') == std::string::npos) { + output << std::left << std::setw(40) << std::setfill(' ') + << xe::format_string("%s = %s", config_var->name().c_str(), + config_var->config_value().c_str()); + } else { + auto lines = xe::split_string(value, "\n"); + auto first_it = lines.cbegin(); + output << xe::format_string("%s = %s\n", config_var->name().c_str(), + *first_it); + auto last_it = std::prev(lines.cend()); + for (auto it = std::next(first_it); it != last_it; ++it) { + output << *it << "\n"; + } + output << std::left << std::setw(40) << std::setfill(' ') << *last_it; + } + output << xe::format_string("\t# %s\n", config_var->description().c_str()); } if (xe::filesystem::PathExists(config_path)) {