diff --git a/doc/files.md b/doc/files.md index 1bc3a1847a..dd966d8ef3 100644 --- a/doc/files.md +++ b/doc/files.md @@ -60,6 +60,7 @@ Subdirectory | File(s) | Description `./` | `anchors.dat` | Anchor IP address database, created on shutdown and deleted at startup. Anchors are last known outgoing block-relay-only peers that are tried to re-connect to on startup `./` | `banlist.json` | Stores the addresses/subnets of banned nodes. `./` | `bitcoin.conf` | User-defined [configuration settings](bitcoin-conf.md) for `bitcoind` or `bitcoin-qt`. File is not written to by the software and must be created manually. Path can be specified by `-conf` option +`./` | `bitcoin_rw.conf` | Contains [configuration settings](bitcoin-conf.md) modified by `bitcoind` or `bitcoin-qt`; can be specified by `-confrw` option `./` | `bitcoind.pid` | Stores the process ID (PID) of `bitcoind` or `bitcoin-qt` while running; created at start and deleted on shutdown; can be specified by `-pid` option `./` | `debug.log` | Contains debug information and general logging generated by `bitcoind` or `bitcoin-qt`; can be specified by `-debuglogfile` option `./` | `fee_estimates.dat` | Stores statistics used to estimate minimum transaction fees required for confirmation diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 7683801c93..f270eabba2 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -83,6 +83,7 @@ static void SetupCliArgs(ArgsManager& argsman) argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-conf=", strprintf("Specify configuration file. Relative paths will be prefixed by datadir location. (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-confrw=", strprintf("Specify read/write configuration file. Relative paths will be prefixed by the network-specific datadir location. (default: %s)", BITCOIN_RW_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-datadir=", "Specify data directory", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-generate", strprintf("Generate blocks, equivalent to RPC getnewaddress followed by RPC generatetoaddress. Optional positional integer " @@ -181,13 +182,6 @@ static int AppInitRPC(int argc, char* argv[]) tfm::format(std::cerr, "Error reading configuration file: %s\n", error); return EXIT_FAILURE; } - // Check for chain settings (BaseParams() calls are only valid after this clause) - try { - SelectBaseParams(gArgs.GetChainType()); - } catch (const std::exception& e) { - tfm::format(std::cerr, "Error: %s\n", e.what()); - return EXIT_FAILURE; - } return CONTINUE_EXECUTION; } diff --git a/src/common/args.cpp b/src/common/args.cpp index 151278a1d7..1cd4090b54 100644 --- a/src/common/args.cpp +++ b/src/common/args.cpp @@ -29,15 +29,18 @@ #include #include #include +#include #include #include #include #include +#include #include #include const char * const BITCOIN_CONF_FILENAME = "bitcoin.conf"; const char * const BITCOIN_SETTINGS_FILENAME = "settings.json"; +const char * const BITCOIN_RW_CONF_FILENAME = "bitcoin_rw.conf"; ArgsManager gArgs; @@ -772,6 +775,12 @@ void ArgsManager::SetConfigFilePath(fs::path path) m_config_path = path; } +fs::path ArgsManager::GetRWConfigFilePath() const +{ + LOCK(cs_args); + return *Assert(m_rwconf_path); +} + ChainType ArgsManager::GetChainType() const { std::variant arg = GetChainArg(); @@ -863,9 +872,231 @@ void ArgsManager::LogArgs() const for (const auto& setting : m_settings.rw_settings) { LogPrintf("Setting file arg: %s = %s\n", setting.first, setting.second.write()); } + logArgsPrefix("R/W config file arg:", "", m_settings.rw_config); logArgsPrefix("Command-line arg:", "", m_settings.command_line_options); } +namespace { + + // Like std::getline, but includes the EOL character in the result + bool getline_with_eol(std::istream& stream, std::string& result) + { + int current_char; + current_char = stream.get(); + if (current_char == std::char_traits::eof()) { + return false; + } + result.clear(); + result.push_back(char(current_char)); + while (current_char != '\n') { + current_char = stream.get(); + if (current_char == std::char_traits::eof()) { + break; + } + result.push_back(char(current_char)); + } + return true; + } + + const char * const ModifyRWConfigFile_ws_chars = " \t\r\n"; + + void ModifyRWConfigFile_SanityCheck(const std::string& s) + { + if (s.empty()) { + // Dereferencing .begin or .rbegin below is invalid unless the string has at least one character. + return; + } + + static const char * const newline_chars = "\r\n"; + static std::string ws_chars(ModifyRWConfigFile_ws_chars); + if (s.find_first_of(newline_chars) != std::string::npos) { + throw std::invalid_argument("New-line in config name/value"); + } + if (ws_chars.find(*s.begin()) != std::string::npos || ws_chars.find(*s.rbegin()) != std::string::npos) { + throw std::invalid_argument("Config name/value has leading/trailing whitespace"); + } + } + + void ModifyRWConfigFile_WriteRemaining(std::ostream& stream_out, const std::map& settings_to_change, std::set& setFound) + { + for (const auto& setting_pair : settings_to_change) { + const std::string& key = setting_pair.first; + const std::string& val = setting_pair.second; + if (setFound.find(key) != setFound.end()) { + continue; + } + setFound.insert(key); + ModifyRWConfigFile_SanityCheck(key); + ModifyRWConfigFile_SanityCheck(val); + stream_out << key << "=" << val << "\n"; + } + } +} // namespace + +void ModifyRWConfigStream(std::istream& stream_in, std::ostream& stream_out, const std::map& settings_to_change) +{ + static const char * const ws_chars = ModifyRWConfigFile_ws_chars; + std::set setFound; + std::string s, lineend, linebegin, key; + std::string::size_type n, n2; + bool inside_group = false, have_eof_nl = true; + std::map::const_iterator iterCS; + size_t lineno = 0; + while (getline_with_eol(stream_in, s)) { + ++lineno; + + have_eof_nl = (!s.empty()) && (*s.rbegin() == '\n'); + n = s.find('#'); + const bool has_comment = (n != std::string::npos); + if (!has_comment) { + n = s.size(); + } + if (n > 0) { + n2 = s.find_last_not_of(ws_chars, n - 1); + if (n2 != std::string::npos) { + n = n2 + 1; + } + } + n2 = s.find_first_not_of(ws_chars); + if (n2 == std::string::npos || n2 >= n) { + // Blank or comment-only line + stream_out << s; + continue; + } + lineend = s.substr(n); + linebegin = s.substr(0, n2); + s = s.substr(n2, n - n2); + + // It is impossible for s to be empty here, due to the blank line check above + if (*s.begin() == '[' && *s.rbegin() == ']') { + // We don't use sections, so we could possibly just write out the rest of the file - but we need to check for unparsable lines, so we just set a flag to ignore settings from here on + ModifyRWConfigFile_WriteRemaining(stream_out, settings_to_change, setFound); + inside_group = true; + key.clear(); + + stream_out << linebegin << s << lineend; + continue; + } + + n = s.find('='); + if (n == std::string::npos) { + // Bad line; this causes boost to throw an exception when parsing, so we comment out the entire file + stream_in.seekg(0, std::ios_base::beg); + stream_out.seekp(0, std::ios_base::beg); + if (!(stream_in.good() && stream_out.good())) { + throw std::ios_base::failure("Failed to rewind (to comment out existing file)"); + } + // First, write out all the settings we intend to set + setFound.clear(); + ModifyRWConfigFile_WriteRemaining(stream_out, settings_to_change, setFound); + // We then define a category to ensure new settings get added before the invalid stuff + stream_out << "[INVALID]\n"; + // Then, describe the problem in a comment + stream_out << "# Error parsing line " << lineno << ": " << s << "\n"; + // Finally, dump the rest of the file commented out + while (getline_with_eol(stream_in, s)) { + stream_out << "#" << s; + } + return; + } + + if (!inside_group) { + // We don't support/use groups, so once we're inside key is always null to avoid setting anything + n2 = s.find_last_not_of(ws_chars, n - 1); + if (n2 == std::string::npos) { + n2 = n - 1; + } else { + ++n2; + } + key = s.substr(0, n2); + } + if ((!key.empty()) && (iterCS = settings_to_change.find(key)) != settings_to_change.end() && setFound.find(key) == setFound.end()) { + // This is the key we want to change + const std::string& val = iterCS->second; + setFound.insert(key); + ModifyRWConfigFile_SanityCheck(val); + if (has_comment) { + // Rather than change a commented line, comment it out entirely (the existing comment may relate to the value) and replace it + stream_out << key << "=" << val << "\n"; + linebegin.insert(linebegin.begin(), '#'); + } else { + // Just modify the value in-line otherwise + n2 = s.find_first_not_of(ws_chars, n + 1); + if (n2 == std::string::npos) { + n2 = n + 1; + } + s = s.substr(0, n2) + val; + } + } + stream_out << linebegin << s << lineend; + } + if (setFound.size() < settings_to_change.size()) { + if (!have_eof_nl) { + stream_out << "\n"; + } + ModifyRWConfigFile_WriteRemaining(stream_out, settings_to_change, setFound); + } +} + +void ArgsManager::ModifyRWConfigFile(const std::map& settings_to_change) +{ + LOCK(cs_args); + fs::path rwconf_path{GetRWConfigFilePath()}; + fs::path rwconf_new_path{rwconf_path}; + rwconf_new_path += ".new"; + try { + fs::remove(rwconf_new_path); + std::ofstream streamRWConfigOut(rwconf_new_path, std::ios_base::out | std::ios_base::trunc); + if (fs::exists(rwconf_path)) { + std::ifstream streamRWConfig(rwconf_path); + ::ModifyRWConfigStream(streamRWConfig, streamRWConfigOut, settings_to_change); + } else { + std::istringstream streamIn; + ::ModifyRWConfigStream(streamIn, streamRWConfigOut, settings_to_change); + } + } catch (...) { + fs::remove(rwconf_new_path); + throw; + } + if (!RenameOver(rwconf_new_path, rwconf_path)) { + fs::remove(rwconf_new_path); + throw std::ios_base::failure(strprintf("Failed to replace %s", fs::PathToString(rwconf_new_path))); + } + for (const auto& setting_change : settings_to_change) { + m_settings.rw_config[setting_change.first] = {setting_change.second}; + } + if (!IsArgNegated("-settings")) { + // Also save to settings.json for Core (0.21+) compatibility + for (const auto& setting_change : settings_to_change) { + m_settings.rw_settings[setting_change.first] = setting_change.second; + } + WriteSettingsFile(); + } +} + +void ArgsManager::ModifyRWConfigFile(const std::string& setting_to_change, const std::string& new_value) +{ + std::map settings_to_change; + settings_to_change[setting_to_change] = new_value; + ModifyRWConfigFile(settings_to_change); +} + +void ArgsManager::EraseRWConfigFile() +{ + LOCK(cs_args); + fs::path rwconf_path{GetRWConfigFilePath()}; + if (!fs::exists(rwconf_path)) { + return; + } + fs::path rwconf_reset_path = rwconf_path; + rwconf_reset_path += ".reset"; + if (!RenameOver(rwconf_path, rwconf_reset_path)) { + if (fs::remove(rwconf_path)) { + throw std::ios_base::failure(strprintf("Failed to remove %s", fs::PathToString(rwconf_path))); + } + } +} + namespace common { #ifdef WIN32 WinCmdLineArgs::WinCmdLineArgs() diff --git a/src/common/args.h b/src/common/args.h index c782d4245c..fda7be39ad 100644 --- a/src/common/args.h +++ b/src/common/args.h @@ -25,6 +25,7 @@ class ArgsManager; extern const char * const BITCOIN_CONF_FILENAME; extern const char * const BITCOIN_SETTINGS_FILENAME; +extern const char * const BITCOIN_RW_CONF_FILENAME; // Return true if -datadir option points to a valid directory or is not specified. bool CheckDataDirOption(const ArgsManager& args); @@ -94,6 +95,8 @@ std::optional SettingToInt(const common::SettingsValue&); bool SettingToBool(const common::SettingsValue&, bool); std::optional SettingToBool(const common::SettingsValue&); +void ModifyRWConfigStream(std::istream& stream_in, std::ostream& stream_out, const std::map& settings_to_change); + class ArgsManager { public: @@ -139,11 +142,12 @@ protected: bool m_accept_any_command GUARDED_BY(cs_args){true}; std::list m_config_sections GUARDED_BY(cs_args); std::optional m_config_path GUARDED_BY(cs_args); + std::optional m_rwconf_path GUARDED_BY(cs_args); mutable fs::path m_cached_blocks_path GUARDED_BY(cs_args); mutable fs::path m_cached_datadir_path GUARDED_BY(cs_args); mutable fs::path m_cached_network_datadir_path GUARDED_BY(cs_args); - [[nodiscard]] bool ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys = false); + [[nodiscard]] bool ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys = false, std::map>* settings_target = nullptr); /** * Returns true if settings values from the default section should be used, @@ -182,8 +186,13 @@ protected: */ fs::path GetConfigFilePath() const; void SetConfigFilePath(fs::path); + fs::path GetRWConfigFilePath() const; [[nodiscard]] bool ReadConfigFiles(std::string& error, bool ignore_invalid_keys = false); + void ModifyRWConfigFile(const std::map& settings_to_change); + void ModifyRWConfigFile(const std::string& setting_to_change, const std::string& new_value); + void EraseRWConfigFile(); + /** * Log warnings for options in m_section_only_args when * they are specified in the default section but not overridden diff --git a/src/common/config.cpp b/src/common/config.cpp index fac4aa314c..42b2eee71f 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -90,7 +91,7 @@ bool IsConfSupported(KeyInfo& key, std::string& error) { return true; } -bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys) +bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& filepath, std::string& error, bool ignore_invalid_keys, std::map>* settings_target) { LOCK(cs_args); std::vector> options; @@ -106,6 +107,9 @@ bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& file if (!value) { return false; } + if (settings_target) { + (*settings_target)[key.name].push_back(*value); + } else m_settings.ro_config[key.section][key.name].push_back(*value); } else { if (ignore_invalid_keys) { @@ -124,6 +128,7 @@ bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys) { LOCK(cs_args); m_settings.ro_config.clear(); + m_settings.rw_config.clear(); m_config_sections.clear(); m_config_path = AbsPathForConfigVal(*this, GetPathArg("-conf", BITCOIN_CONF_FILENAME), /*net_specific=*/false); } @@ -214,12 +219,31 @@ bool ArgsManager::ReadConfigFiles(std::string& error, bool ignore_invalid_keys) } } + // Check for chain settings (BaseParams() calls are only valid after this clause) + try { + SelectBaseParams(gArgs.GetChainType()); + } catch (const std::exception& e) { + error = e.what(); + return false; + } + // If datadir is changed in .conf file: ClearPathCache(); if (!CheckDataDirOption(*this)) { error = strprintf("specified data directory \"%s\" does not exist.", GetArg("-datadir", "")); return false; } + + LOCK(cs_args); + m_rwconf_path = AbsPathForConfigVal(*this, GetPathArg("-confrw", BITCOIN_RW_CONF_FILENAME)); + const auto rwconf_path{GetRWConfigFilePath()}; + std::ifstream rwconf_stream(rwconf_path); + if (rwconf_stream.good()) { + if (!ReadConfigStream(rwconf_stream, fs::PathToString(rwconf_path), error, ignore_invalid_keys, &m_settings.rw_config)) { + return false; + } + } + return true; } diff --git a/src/common/settings.cpp b/src/common/settings.cpp index c1520dacd2..576ef285ac 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -24,6 +24,7 @@ namespace { enum class Source { FORCED, COMMAND_LINE, + CONFIG_FILE_RW, RW_SETTINGS, CONFIG_FILE_NETWORK_SECTION, CONFIG_FILE_DEFAULT_SECTION @@ -48,6 +49,10 @@ static void MergeSettings(const Settings& settings, const std::string& section, if (auto* values = FindKey(settings.command_line_options, name)) { fn(SettingsSpan(*values), Source::COMMAND_LINE); } + // Merge in the rw config file + if (auto* values = FindKey(settings.rw_config, name)) { + fn(SettingsSpan(*values), Source::CONFIG_FILE_RW); + } // Merge in the read-write settings if (const SettingsValue* value = FindKey(settings.rw_settings, name)) { fn(SettingsSpan(*value), Source::RW_SETTINGS); @@ -165,7 +170,7 @@ SettingsValue GetSetting(const Settings& settings, // the config file the precedence is reversed for all settings except // chain type settings. const bool reverse_precedence = - (source == Source::CONFIG_FILE_NETWORK_SECTION || source == Source::CONFIG_FILE_DEFAULT_SECTION) && + (source == Source::CONFIG_FILE_RW || source == Source::CONFIG_FILE_NETWORK_SECTION || source == Source::CONFIG_FILE_DEFAULT_SECTION) && !get_chain_type; // Weird behavior preserved for backwards compatibility: Negated @@ -217,7 +222,7 @@ std::vector GetSettingsList(const Settings& settings, // settings will be brought back from the dead (but earlier command // line settings will still be ignored). const bool add_zombie_config_values = - (source == Source::CONFIG_FILE_NETWORK_SECTION || source == Source::CONFIG_FILE_DEFAULT_SECTION) && + (source == Source::CONFIG_FILE_RW || source == Source::CONFIG_FILE_NETWORK_SECTION || source == Source::CONFIG_FILE_DEFAULT_SECTION) && !prev_negated_empty; // Ignore settings in default config section if requested. diff --git a/src/common/settings.h b/src/common/settings.h index 0e9d376e23..e00f580314 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -34,6 +34,8 @@ struct Settings { std::map forced_settings; //! Map of setting name to list of command line values. std::map> command_line_options; + //! Map of setting name to list of r/w config file values. + std::map> rw_config; //! Map of setting name to read-write file setting value. std::map rw_settings; //! Map of config section name and setting name to list of config file values. diff --git a/src/init.cpp b/src/init.cpp index d95a166117..bf92f2a1e8 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -493,6 +493,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-blocksonly", strprintf("Whether to reject transactions from network peers. Disables automatic broadcast and rebroadcast of transactions, unless the source peer has the 'forcerelay' permission. RPC transactions are not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-coinstatsindex", strprintf("Maintain coinstats index used by the gettxoutsetinfo RPC (default: %u)", DEFAULT_COINSTATSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-conf=", strprintf("Specify path to read-only configuration file. Relative paths will be prefixed by datadir location (only useable from command line, not configuration file) (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-confrw=", strprintf("Specify read/write configuration file. Relative paths will be prefixed by the network-specific datadir location (default: %s)", BITCOIN_RW_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-datadir=", "Specify data directory", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); argsman.AddArg("-dbcache=", strprintf("Maximum database cache size MiB (%d to %d, default: %d). In addition, unused mempool memory is shared for this cache (see -maxmempool).", nMinDbCache, nMaxDbCache, nDefaultDbCache), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); diff --git a/src/init/common.cpp b/src/init/common.cpp index 1d246241c1..fefb29c9ac 100644 --- a/src/init/common.cpp +++ b/src/init/common.cpp @@ -136,6 +136,15 @@ bool StartLogging(const ArgsManager& args) LogPrintf("Config file: %s (not found, skipping)\n", fs::PathToString(config_file_path)); } + fs::path rwconfig_file_path = args.GetRWConfigFilePath(); + if (fs::exists(rwconfig_file_path)) { + LogPrintf("R/W Config file: %s\n", fs::PathToString(rwconfig_file_path)); + } else if (gArgs.IsArgSet("-confrw")) { + InitWarning(strprintf(_("The specified R/W config file %s does not exist"), fs::PathToString(rwconfig_file_path))); + } else { + LogPrintf("R/W Config file: %s (not found, skipping)\n", fs::PathToString(rwconfig_file_path)); + } + // Log the config arguments to debug.log args.LogArgs(); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 774210455f..0d2c7576c3 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -312,6 +312,9 @@ void OptionsModel::Reset() QString dataDir = GUIUtil::getDefaultDataDirectory(); dataDir = settings.value("strDataDir", dataDir).toString(); + // Remove rw config file + gArgs.EraseRWConfigFile(); + // Remove all entries from our QSettings object settings.clear(); diff --git a/src/qt/test/apptests.cpp b/src/qt/test/apptests.cpp index 10abcb00eb..d176ea5ae9 100644 --- a/src/qt/test/apptests.cpp +++ b/src/qt/test/apptests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -66,6 +67,14 @@ void AppTests::appTests() } #endif + { + // Need to ensure datadir is setup so resetting settings can delete the non-existent bitcoin_rw.conf + std::string error; + if (!gArgs.ReadConfigFiles(error, true)) { + QWARN("Error in readConfigFiles"); + } + } + qRegisterMetaType("interfaces::BlockAndHeaderTipInfo"); m_app.parameterSetup(); QVERIFY(m_app.createOptionsModel(/*resetSettings=*/true)); diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 3d5d29f0e5..7369c77f7b 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -2037,4 +2037,157 @@ BOOST_AUTO_TEST_CASE(clearshrink_test) } } +static std::string CheckModifyRWConfigFile(std::map& settings_to_change, const std::string& current_config_file) +{ + std::istringstream stream_in(current_config_file); + std::ostringstream stream_out; + try { + ModifyRWConfigStream(stream_in, stream_out, settings_to_change); + } catch (...) { + settings_to_change.clear(); + throw; + } + settings_to_change.clear(); + return stream_out.str(); +} + +BOOST_AUTO_TEST_CASE(test_ModifyRWConfigFile) +{ + std::map cs; + + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b"), "a=b"); + + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b"), "a=c"); + BOOST_CHECK(cs.empty()); + + // Multi-char name/value + cs["ab"] = "cd"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "ab=bc"), "ab=cd"); + + // Preserved final newline + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n"), "a=b\n"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n"), "a=c\n"); + + // Preserved final tab + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\t"), "a=b\t"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\t"), "a=c\t"); + + // Preserved final space + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b "), "a=b "); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b "), "a=c "); + + // Preserved final crnl + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\r\n"), "a=b\r\n"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\r\n"), "a=c\r\n"); + + // Empty file + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, ""), "a=c\n"); + + // Ignore k=v in comment + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "#a=b"), "#a=b\na=c\n"); + + // Preserved comment + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\t# c"), "a=b\t# c"); + + // Commented out commented value + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\t# c"), "a=c\n#a=b\t# c"); + + // Preserved whitespace before name + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, " \t \ta=b"), " \t \ta=b"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, " \t \ta=b"), " \t \ta=c"); + + // Preserved whitespace after name + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a \t \t=b"), "a \t \t=b"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a \t \t=b"), "a \t \t=c"); + + // Preserved whitespace before value + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a= \t \tb"), "a= \t \tb"); + cs["a"] = "c"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a= \t \tb"), "a= \t \tc"); + + // Modifying value between others + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nd=e"), "a=b\nab=bc\nd=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nd=e"), "a=b\nab=x\nd=e"); + + // Blank key/value + cs["ab"] = ""; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nd=e"), "a=b\nab=\nd=e"); + cs[""] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nd=e"), "a=b\nab=bc\nd=e\n=x\n"); + + // Blank line in source + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n\nab=bc\n\nd=e"), "a=b\n\nab=bc\n\nd=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n\nab=bc\n\nd=e"), "a=b\n\nab=x\n\nd=e"); + + // Duplicate keys in the source + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nf=x\nab=zx\nd=e"), "a=b\nab=bc\nf=x\nab=zx\nd=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nf=x\nab=zx\nd=e"), "a=b\nab=x\nf=x\nab=zx\nd=e"); + + // Comment out entire file if invalid input line + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nGARBAGE\nd=e"), "[INVALID]\n# Error parsing line 3: GARBAGE\n#a=b\n#ab=bc\n#GARBAGE\n#d=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nGARBAGE\nd=e"), "ab=x\n[INVALID]\n# Error parsing line 3: GARBAGE\n#a=b\n#ab=bc\n#GARBAGE\n#d=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=bc\nGARBAGE\nd=e\n"), "ab=x\n[INVALID]\n# Error parsing line 3: GARBAGE\n#a=b\n#ab=bc\n#GARBAGE\n#d=e\n"); + + // Whitespace inside values + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=b\t \t c\nd=e"), "a=b\nab=b\t \t c\nd=e"); + cs["ab"] = "x \t \tx"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\nab=b\t \t c\nd=e"), "a=b\nab=x \t \tx\nd=e"); + + // Newline inside name/value + cs["a"] = "x\nx"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a"] = "x\rx"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a\nb"] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a\rb"] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + + // Whitespace leading/trailing name/value + cs["a"] = " x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a"] = "\tx"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs[" a"] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["\ta"] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a"] = "x "; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a"] = "x\t"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a "] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + cs["a\t"] = "x"; + BOOST_REQUIRE_THROW(CheckModifyRWConfigFile(cs, ""), std::invalid_argument); + + // Ignore groups + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n[group]\nab=bc\nd=e"), "a=b\n[group]\nab=bc\nd=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n[group]\nab=bc\nd=e"), "a=b\nab=x\n[group]\nab=bc\nd=e"); + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n\t [group] \t#c\nab=bc\nd=e"), "a=b\n\t [group] \t#c\nab=bc\nd=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n\t [group] \t#c\nab=bc\nd=e"), "a=b\nab=x\n\t [group] \t#c\nab=bc\nd=e"); + + // Comment out entire file if invalid input line, even after a group + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n[group]\nab=bc\nGARBAGE\nd=e"), "[INVALID]\n# Error parsing line 4: GARBAGE\n#a=b\n#[group]\n#ab=bc\n#GARBAGE\n#d=e"); + cs["ab"] = "x"; + BOOST_CHECK_EQUAL(CheckModifyRWConfigFile(cs, "a=b\n[group]\nab=bc\nGARBAGE\nd=e"), "ab=x\n[INVALID]\n# Error parsing line 4: GARBAGE\n#a=b\n#[group]\n#ab=bc\n#GARBAGE\n#d=e"); +} + BOOST_AUTO_TEST_SUITE_END()