diff --git a/src/config.cpp b/src/config.cpp index 5392bca2ee0..e18abacd458 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -693,13 +693,19 @@ namespace config { std::stringstream json_stream; json_stream << "{\"dd_mode_remapping\":" << value << "}"; - boost::property_tree::ptree json_tree; - boost::property_tree::read_json(json_stream, json_tree); - video_t::dd_t::mode_remapping_t output; - parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed); - parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only); - parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only); + try { + boost::property_tree::ptree json_tree; + boost::property_tree::read_json(json_stream, json_tree); + + parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed); + parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only); + parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only); + } catch (const boost::property_tree::ptree_error &err) { + // don't let bad json kill startup, skip remapping + BOOST_LOG(warning) << "Ignoring invalid dd_mode_remapping configuration: "sv << err.what(); + return {}; + } return output; } @@ -1426,15 +1432,21 @@ namespace config { // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. jsonStream << "{\"prep_cmd\":" << string << "}"; - boost::property_tree::ptree jsonTree; - boost::property_tree::read_json(jsonStream, jsonTree); + try { + boost::property_tree::ptree jsonTree; + boost::property_tree::read_json(jsonStream, jsonTree); - for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { - auto do_cmd = prep_cmd.get_optional("do"s); - auto undo_cmd = prep_cmd.get_optional("undo"s); - auto elevated = prep_cmd.get_optional("elevated"s); + for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { + auto do_cmd = prep_cmd.get_optional("do"s); + auto undo_cmd = prep_cmd.get_optional("undo"s); + auto elevated = prep_cmd.get_optional("elevated"s); - input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); + input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); + } + } catch (const boost::property_tree::ptree_error &err) { + // don't let bad json kill startup, skip this setting + BOOST_LOG(warning) << "Ignoring invalid "sv << name << " configuration: "sv << err.what(); + input.clear(); } } diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp new file mode 100644 index 00000000000..2a5b7b1b52a --- /dev/null +++ b/tests/unit/test_config.cpp @@ -0,0 +1,81 @@ +/** + * @file tests/unit/test_config.cpp + * @brief Test src/config.* configuration parsing helpers. + */ +// test imports +#include "../tests_common.h" + +// standard imports +#include +#include +#include +#include + +// local imports +#include + +// internal helpers, not exposed in config.h +namespace config { + void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input); + + namespace dd { + video_t::dd_t::mode_remapping_t mode_remapping_from_view(std::string_view value); + } // namespace dd +} // namespace config + +struct ConfigParsingTest: BaseTest {}; + +TEST_F(ConfigParsingTest, PrepCmdMalformedJsonDoesNotThrow) { + // bad json here must not kill startup + std::unordered_map vars {{"global_prep_cmd", "["}}; + std::vector input; + + EXPECT_NO_THROW(config::list_prep_cmd_f(vars, "global_prep_cmd", input)); + EXPECT_TRUE(input.empty()); +} + +TEST_F(ConfigParsingTest, PrepCmdValidJsonIsParsed) { + std::unordered_map vars { + {"global_prep_cmd", R"([{"do":"echo start","undo":"echo stop","elevated":false}])"} + }; + std::vector input; + + ASSERT_NO_THROW(config::list_prep_cmd_f(vars, "global_prep_cmd", input)); + ASSERT_EQ(input.size(), 1u); + EXPECT_EQ(input[0].do_cmd, "echo start"); + EXPECT_EQ(input[0].undo_cmd, "echo stop"); + EXPECT_FALSE(input[0].elevated); +} + +TEST_F(ConfigParsingTest, ModeRemappingMalformedJsonReturnsEmpty) { + config::video_t::dd_t::mode_remapping_t result; + EXPECT_NO_THROW(result = config::dd::mode_remapping_from_view("{ not valid json")); + EXPECT_TRUE(result.mixed.empty()); + EXPECT_TRUE(result.resolution_only.empty()); + EXPECT_TRUE(result.refresh_rate_only.empty()); +} + +TEST_F(ConfigParsingTest, ModeRemappingValidJsonMissingChildKeysReturnsEmpty) { + // valid json but missing keys also throws (ptree_bad_path) + config::video_t::dd_t::mode_remapping_t result; + EXPECT_NO_THROW(result = config::dd::mode_remapping_from_view("[]")); + EXPECT_TRUE(result.mixed.empty()); + EXPECT_TRUE(result.resolution_only.empty()); + EXPECT_TRUE(result.refresh_rate_only.empty()); +} + +TEST_F(ConfigParsingTest, ModeRemappingValidJsonIsParsed) { + constexpr std::string_view value = R"({ + "mixed": [{"requested_resolution":"1920x1080","requested_fps":"60","final_resolution":"2560x1440","final_refresh_rate":"120"}], + "resolution_only": [], + "refresh_rate_only": [] + })"; + + config::video_t::dd_t::mode_remapping_t result; + ASSERT_NO_THROW(result = config::dd::mode_remapping_from_view(value)); + ASSERT_EQ(result.mixed.size(), 1u); + EXPECT_EQ(result.mixed[0].requested_resolution, "1920x1080"); + EXPECT_EQ(result.mixed[0].requested_fps, "60"); + EXPECT_EQ(result.mixed[0].final_resolution, "2560x1440"); + EXPECT_EQ(result.mixed[0].final_refresh_rate, "120"); +}