From 330ff1a2151439fd18d027f0705b169dbf6a344f Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Tue, 27 Jan 2026 09:58:04 -0600 Subject: [PATCH 1/2] fix: crash when parsing invalid json config --- src/config_response.cpp | 7 +++ test/test_config_response_json.cpp | 90 ++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/config_response.cpp b/src/config_response.cpp index c9410f9..af227b6 100644 --- a/src/config_response.cpp +++ b/src/config_response.cpp @@ -627,6 +627,13 @@ ParseResult parseConfigResponse(const nlohmann::json& j) { ParseResult result; ConfigResponse cr; + // Validate that input is a JSON object + if (!j.is_object()) { + result.errors.push_back("ConfigResponse: Expected a JSON object, got " + + std::string(j.type_name())); + return result; + } + // Parse flags - required field if (!j.contains("flags")) { result.errors.push_back("ConfigResponse: Missing required field: flags"); diff --git a/test/test_config_response_json.cpp b/test/test_config_response_json.cpp index f17bf0a..ed27bea 100644 --- a/test/test_config_response_json.cpp +++ b/test/test_config_response_json.cpp @@ -172,6 +172,96 @@ TEST_CASE("ConfigResponse deserialization - empty config", "[config_response][js REQUIRE(result->flags.empty()); } +TEST_CASE("ConfigResponse deserialization - empty string input", "[config_response][json]") { + std::string emptyString = ""; + + auto result = parseConfigResponse(emptyString); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("JSON parse error") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - null JSON input", "[config_response][json]") { + json j = nullptr; + + auto result = parseConfigResponse(j); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - null JSON string input", "[config_response][json]") { + std::string nullString = "null"; + + auto result = parseConfigResponse(nullString); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - array JSON input", "[config_response][json]") { + json j = json::array(); + + auto result = parseConfigResponse(j); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - array JSON string input", "[config_response][json]") { + std::string arrayString = "[]"; + + auto result = parseConfigResponse(arrayString); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - number JSON input", "[config_response][json]") { + json j = 123; + + auto result = parseConfigResponse(j); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - string JSON input", "[config_response][json]") { + json j = "not an object"; + + auto result = parseConfigResponse(j); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - boolean JSON input", "[config_response][json]") { + json j = true; + + auto result = parseConfigResponse(j); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("Expected a JSON object") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - malformed JSON string", "[config_response][json]") { + std::string malformed = "{invalid json"; + + auto result = parseConfigResponse(malformed); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("JSON parse error") != std::string::npos); +} + +TEST_CASE("ConfigResponse deserialization - truncated JSON string", "[config_response][json]") { + std::string truncated = R"({"flags": {"test": {"key": "t)"; + + auto result = parseConfigResponse(truncated); + REQUIRE_FALSE(result.hasValue()); + REQUIRE(result.errors.size() == 1); + REQUIRE(result.errors[0].find("JSON parse error") != std::string::npos); +} + TEST_CASE("ConfigResponse deserialization - single simple flag", "[config_response][json]") { json j = R"({ "flags": { From 1afa0f3ef2049aae2fb3c4603b33a4df120db8a2 Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Tue, 27 Jan 2026 10:14:35 -0600 Subject: [PATCH 2/2] chore: bump to version 2.0.3 --- CHANGELOG.md | 4 ++++ Makefile | 2 +- src/version.hpp | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99879b7..95ed198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Note**: Semantic Versioning applies to source-level compatibility only. No guarantees are made regarding Application Binary Interface (ABI) stability. When upgrading versions, always recompile your application against the new SDK version. Avoid mixing object files or libraries compiled against different SDK versions. +## [2.0.3] - 2026-01-27 + +- fix crash in `parseConfigResponse` when receiving empty, null, or non-object JSON input (#51) + ## [2.0.2] - 2026-01-14 - fix time_utils fmodules build diff --git a/Makefile b/Makefile index c756db4..4be2dcd 100644 --- a/Makefile +++ b/Makefile @@ -173,7 +173,7 @@ help: @echo " test-eval-performance - Run flag evaluation performance tests (min/max/avg μs)" @echo " examples - Build all examples" @echo " run-bandits - Build and run the bandits example" - @echo " run-flag-assignments - Build and run the flag_assignments example" + @echo " run-flags - Build and run the flag_assignments example" @echo " run-assignment-details - Build and run the assignment_details example" @echo " run-manual-sync - Build and run the manual_sync example" @echo " format - Format all C++ source files with clang-format" diff --git a/src/version.hpp b/src/version.hpp index 3a33f8b..ff331c2 100644 --- a/src/version.hpp +++ b/src/version.hpp @@ -6,7 +6,7 @@ #define EPPOCLIENT_VERSION_MAJOR 2 #define EPPOCLIENT_VERSION_MINOR 0 -#define EPPOCLIENT_VERSION_PATCH 2 +#define EPPOCLIENT_VERSION_PATCH 3 #define EPPOCLIENT_VERSION \ (EPPO_STRINGIFY(EPPOCLIENT_VERSION_MAJOR) "." EPPO_STRINGIFY( \ EPPOCLIENT_VERSION_MINOR) "." EPPO_STRINGIFY(EPPOCLIENT_VERSION_PATCH))