Merge 11082 via rwconf-27+knots

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit c90495c624
12 changed files with 452 additions and 11 deletions

View File

@ -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

View File

@ -83,6 +83,7 @@ static void SetupCliArgs(ArgsManager& argsman)
argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-conf=<file>", 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=<file>", 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=<dir>", "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;
}

View File

@ -29,15 +29,18 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>
#include <variant>
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<ChainType, std::string> 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<char>::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<char>::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<std::string, std::string>& settings_to_change, std::set<std::string>& 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<std::string, std::string>& settings_to_change)
{
static const char * const ws_chars = ModifyRWConfigFile_ws_chars;
std::set<std::string> setFound;
std::string s, lineend, linebegin, key;
std::string::size_type n, n2;
bool inside_group = false, have_eof_nl = true;
std::map<std::string, std::string>::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<std::string, std::string>& 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<std::string, std::string> 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()

View File

@ -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<int64_t> SettingToInt(const common::SettingsValue&);
bool SettingToBool(const common::SettingsValue&, bool);
std::optional<bool> SettingToBool(const common::SettingsValue&);
void ModifyRWConfigStream(std::istream& stream_in, std::ostream& stream_out, const std::map<std::string, std::string>& settings_to_change);
class ArgsManager
{
public:
@ -139,11 +142,12 @@ protected:
bool m_accept_any_command GUARDED_BY(cs_args){true};
std::list<SectionInfo> m_config_sections GUARDED_BY(cs_args);
std::optional<fs::path> m_config_path GUARDED_BY(cs_args);
std::optional<fs::path> 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<std::string, std::vector<common::SettingsValue>>* 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<std::string, std::string>& 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

View File

@ -4,6 +4,7 @@
#include <common/args.h>
#include <chainparamsbase.h>
#include <common/settings.h>
#include <logging.h>
#include <sync.h>
@ -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<std::string, std::vector<common::SettingsValue>>* settings_target)
{
LOCK(cs_args);
std::vector<std::pair<std::string, std::string>> 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;
}

View File

@ -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<SettingsValue> 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.

View File

@ -34,6 +34,8 @@ struct Settings {
std::map<std::string, SettingsValue> forced_settings;
//! Map of setting name to list of command line values.
std::map<std::string, std::vector<SettingsValue>> command_line_options;
//! Map of setting name to list of r/w config file values.
std::map<std::string, std::vector<SettingsValue>> rw_config;
//! Map of setting name to read-write file setting value.
std::map<std::string, SettingsValue> rw_settings;
//! Map of config section name and setting name to list of config file values.

View File

@ -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=<file>", 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=<file>", 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=<dir>", "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=<n>", strprintf("Maximum database cache size <n> 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);

View File

@ -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();

View File

@ -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();

View File

@ -5,6 +5,7 @@
#include <qt/test/apptests.h>
#include <chainparams.h>
#include <common/args.h>
#include <key.h>
#include <logging.h>
#include <qt/bitcoin.h>
@ -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>("interfaces::BlockAndHeaderTipInfo");
m_app.parameterSetup();
QVERIFY(m_app.createOptionsModel(/*resetSettings=*/true));

View File

@ -2037,4 +2037,157 @@ BOOST_AUTO_TEST_CASE(clearshrink_test)
}
}
static std::string CheckModifyRWConfigFile(std::map<std::string, std::string>& 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<std::string, std::string> 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()