diff --git a/src/test/settings_tests.cpp b/src/test/settings_tests.cpp --- a/src/test/settings_tests.cpp +++ b/src/test/settings_tests.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -14,8 +15,94 @@ #include +inline bool operator==(const util::SettingsValue &a, + const util::SettingsValue &b) { + return a.write() == b.write(); +} + +inline std::ostream &operator<<(std::ostream &os, + const util::SettingsValue &value) { + os << value.write(); + return os; +} + +inline std::ostream & +operator<<(std::ostream &os, + const std::pair &kv) { + util::SettingsValue out(util::SettingsValue::VOBJ); + out.__pushKV(kv.first, kv.second); + os << out.write(); + return os; +} + +inline void WriteText(const fs::path &path, const std::string &text) { + fsbridge::ofstream file; + file.open(path); + file << text; +} + BOOST_FIXTURE_TEST_SUITE(settings_tests, BasicTestingSetup) +BOOST_AUTO_TEST_CASE(ReadWrite) { + fs::path path = GetDataDir() / "settings.json"; + + WriteText(path, R"({ + "string": "string", + "num": 5, + "bool": true, + "null": null + })"); + + std::map expected{ + {"string", "string"}, + {"num", 5}, + {"bool", true}, + {"null", {}}, + }; + + // Check file read. + std::map values; + std::vector errors; + BOOST_CHECK(util::ReadSettings(path, values, errors)); + BOOST_CHECK_EQUAL_COLLECTIONS(values.begin(), values.end(), + expected.begin(), expected.end()); + BOOST_CHECK(errors.empty()); + + // Check no errors if file doesn't exist. + fs::remove(path); + BOOST_CHECK(util::ReadSettings(path, values, errors)); + BOOST_CHECK(values.empty()); + BOOST_CHECK(errors.empty()); + + // Check duplicate keys not allowed + WriteText(path, R"({ + "dupe": "string", + "dupe": "dupe" + })"); + BOOST_CHECK(!util::ReadSettings(path, values, errors)); + std::vector dup_keys = {strprintf( + "Found duplicate key dupe in settings file %s", path.string())}; + BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), + dup_keys.begin(), dup_keys.end()); + + // Check non-kv json files not allowed + WriteText(path, R"("non-kv")"); + BOOST_CHECK(!util::ReadSettings(path, values, errors)); + std::vector non_kv = { + strprintf("Found non-object value \"non-kv\" in settings file %s", + path.string())}; + BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), non_kv.begin(), + non_kv.end()); + + // Check invalid json not allowed + WriteText(path, R"(invalid json)"); + BOOST_CHECK(!util::ReadSettings(path, values, errors)); + std::vector fail_parse = { + strprintf("Unable to parse settings file %s", path.string())}; + BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), + fail_parse.begin(), fail_parse.end()); +} + //! Check settings struct contents against expected json strings. static void CheckValues(const util::Settings &settings, const std::string &single_val, diff --git a/src/util/settings.h b/src/util/settings.h --- a/src/util/settings.h +++ b/src/util/settings.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_UTIL_SETTINGS_H #define BITCOIN_UTIL_SETTINGS_H +#include + #include #include #include @@ -37,6 +39,16 @@ ro_config; }; +//! Read settings file. +bool ReadSettings(const fs::path &path, + std::map &values, + std::vector &errors); + +//! Write settings file. +bool WriteSettings(const fs::path &path, + const std::map &values, + std::vector &errors); + //! Get settings value from combined sources: forced settings, command line //! arguments and the read-only config file. //! diff --git a/src/util/settings.cpp b/src/util/settings.cpp --- a/src/util/settings.cpp +++ b/src/util/settings.cpp @@ -4,6 +4,7 @@ #include +#include #include namespace util { @@ -52,6 +53,75 @@ } } // namespace +bool ReadSettings(const fs::path &path, + std::map &values, + std::vector &errors) { + values.clear(); + errors.clear(); + + fsbridge::ifstream file; + file.open(path); + if (!file.is_open()) { + // Ok for file not to exist. + return true; + } + + SettingsValue in; + if (!in.read(std::string{std::istreambuf_iterator(file), + std::istreambuf_iterator()})) { + errors.emplace_back( + strprintf("Unable to parse settings file %s", path.string())); + return false; + } + + if (file.fail()) { + errors.emplace_back( + strprintf("Failed reading settings file %s", path.string())); + return false; + } + // Done with file descriptor. Release while copying data. + file.close(); + + if (!in.isObject()) { + errors.emplace_back( + strprintf("Found non-object value %s in settings file %s", + in.write(), path.string())); + return false; + } + + const std::vector &in_keys = in.getKeys(); + const std::vector &in_values = in.getValues(); + for (size_t i = 0; i < in_keys.size(); ++i) { + auto inserted = values.emplace(in_keys[i], in_values[i]); + if (!inserted.second) { + errors.emplace_back( + strprintf("Found duplicate key %s in settings file %s", + in_keys[i], path.string())); + } + } + return errors.empty(); +} + +bool WriteSettings(const fs::path &path, + const std::map &values, + std::vector &errors) { + SettingsValue out(SettingsValue::VOBJ); + for (const auto &value : values) { + out.__pushKV(value.first, value.second); + } + fsbridge::ofstream file; + file.open(path); + if (file.fail()) { + errors.emplace_back( + strprintf("Error: Unable to open settings file %s for writing", + path.string())); + return false; + } + file << out.write(/* prettyIndent= */ 1, /* indentLevel= */ 4) << std::endl; + file.close(); + return true; +} + SettingsValue GetSetting(const Settings &settings, const std::string §ion, const std::string &name, bool ignore_default_section_config,