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/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/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)) 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": {