From 7f17fe67e136441758abbca06c01c981a46e6b5c Mon Sep 17 00:00:00 2001 From: Rohan-V Date: Tue, 11 Aug 2026 21:55:26 -0700 Subject: [PATCH] fix(config): handle malformed prep_cmd and dd_mode_remapping JSON read_json/get_child throw ptree_error (json_parser_error or ptree_bad_path), which derive from std::runtime_error rather than filesystem_error. config::parse() only guards apply_config() with filesystem_error handlers, so a malformed global_prep_cmd or dd_mode_remapping value escaped to main() and terminated the process. Because the bad value is persisted in the config, Sunshine then failed to start on every launch (crash loop). Wrap the JSON parsing in list_prep_cmd_f() and mode_remapping_from_view() so an invalid value is logged and skipped (falling back to the default) instead of aborting startup. Add tests/unit/test_config.cpp covering the malformed, missing-key, and valid parse paths for both helpers. --- src/config.cpp | 38 ++++++++++++------ tests/unit/test_config.cpp | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 tests/unit/test_config.cpp 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"); +}