From 25d0a4bafc62364949130084cb7133379761389f Mon Sep 17 00:00:00 2001 From: "Morten V. Pedersen" Date: Wed, 25 Mar 2026 13:24:29 +0100 Subject: [PATCH 1/3] Update NEWS for strict parsing changes --- NEWS.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index a650fe7..e1fc1f4 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -6,7 +6,10 @@ every change, see the Git log. Latest ------ -* tbd +* Minor: Added ``json::parse_options`` and new ``json::parse`` overloads for + strict parsing. +* Minor: Added ``bourne::error::parse_object_duplicate_key`` when strict + parsing detects duplicate object keys. 11.0.0 ------ From 45609354c0ed6001ae2c9d09191c768fd4fd9e11 Mon Sep 17 00:00:00 2001 From: "Morten V. Pedersen" Date: Wed, 25 Mar 2026 13:25:27 +0100 Subject: [PATCH 2/3] Add strict parsing mode for duplicate object keys --- README.rst | 23 ++++++++++++++ src/bourne/detail/error_tags.hpp | 1 + src/bourne/detail/parser.cpp | 53 +++++++++++++++++++++++++------- src/bourne/detail/parser.hpp | 14 +++++++-- src/bourne/json.cpp | 16 ++++++++-- src/bourne/json.hpp | 16 ++++++++++ test/src/test_parser.cpp | 42 +++++++++++++++++++++++++ 7 files changed, 149 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index fbb48e4..a57b486 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,29 @@ Usage Usage example is located in the examples folder. +Strict Parsing +============== + +By default, parsing is permissive and later values overwrite earlier ones if +an object contains duplicate keys. + +To enable strict parsing, pass ``parse_options`` with ``strict = true``. In +strict mode, parsing fails with +``bourne::error::parse_object_duplicate_key`` when duplicate keys are found. + +:: + + bourne::json::parse_options options; + options.strict = true; + + std::error_code error; + auto result = bourne::json::parse("{\"a\":1,\"a\":2}", options, error); + + if (error == bourne::error::parse_object_duplicate_key) + { + // duplicate key found + } + Build ===== diff --git a/src/bourne/detail/error_tags.hpp b/src/bourne/detail/error_tags.hpp index 8e196ad..9a0ed5b 100644 --- a/src/bourne/detail/error_tags.hpp +++ b/src/bourne/detail/error_tags.hpp @@ -8,6 +8,7 @@ BOURNE_ERROR_TAG(parse_found_multiple_unstructured_elements, "Found multiple unstructured elements.") BOURNE_ERROR_TAG(parse_object_expected_colon, "Expected \":\"") BOURNE_ERROR_TAG(parse_object_expected_comma, "Expected \",\"") +BOURNE_ERROR_TAG(parse_object_duplicate_key, "Duplicate key in object") BOURNE_ERROR_TAG(parse_array_expected_comma_or_closing_bracket, "Expected \",\" or \"]\"") BOURNE_ERROR_TAG(parse_string_expected_unicode_escape_hex_char, diff --git a/src/bourne/detail/parser.cpp b/src/bourne/detail/parser.cpp index 2683c33..13a2942 100644 --- a/src/bourne/detail/parser.cpp +++ b/src/bourne/detail/parser.cpp @@ -22,18 +22,40 @@ inline namespace STEINWURF_BOURNE_VERSION namespace detail { json parser::parse(const std::string& input) +{ + return parse(input, json::parse_options{}); +} + +json parser::parse(const std::string& input, const json::parse_options& options) { std::error_code error; std::size_t offset = 0; - auto result = parse_next(input, offset, error); + auto result = parse_next(input, offset, error, options); + if (error) + { + throw_if_error(error); + } + consume_white_space(input, offset); + if (offset != input.size()) + { + error = bourne::error::parse_found_multiple_unstructured_elements; + throw_if_error(error); + } throw_if_error(error); return result; } + json parser::parse(const std::string& input, std::error_code& error) +{ + return parse(input, json::parse_options{}, error); +} + +json parser::parse(const std::string& input, const json::parse_options& options, + std::error_code& error) { assert(!error); std::size_t offset = 0; - auto result = parse_next(input, offset, error); + auto result = parse_next(input, offset, error, options); if (error) return result; consume_white_space(input, offset); @@ -55,7 +77,8 @@ void parser::consume_white_space(const std::string& input, size_t& offset) } json parser::parse_object(const std::string& input, size_t& offset, - std::error_code& error) + std::error_code& error, + const json::parse_options& options) { assert(!error); json object = json(class_type::object); @@ -70,7 +93,7 @@ json parser::parse_object(const std::string& input, size_t& offset, while (true) { - json key = parse_next(input, offset, error); + json key = parse_next(input, offset, error, options); if (error) return json(class_type::null); @@ -82,11 +105,17 @@ json parser::parse_object(const std::string& input, size_t& offset, } offset++; consume_white_space(input, offset); - json value = parse_next(input, offset, error); + json value = parse_next(input, offset, error, options); if (error) return json(class_type::null); - object[key.to_string()] = value; + std::string key_string = key.to_string(); + if (options.strict && object.has_key(key_string)) + { + error = bourne::error::parse_object_duplicate_key; + return json(class_type::null); + } + object[key_string] = value; consume_white_space(input, offset); if (input[offset] == ',') @@ -110,7 +139,8 @@ json parser::parse_object(const std::string& input, size_t& offset, } json parser::parse_array(const std::string& input, size_t& offset, - std::error_code& error) + std::error_code& error, + const json::parse_options& options) { assert(!error); json array = json(class_type::array); @@ -126,7 +156,7 @@ json parser::parse_array(const std::string& input, size_t& offset, while (true) { - array[index++] = parse_next(input, offset, error); + array[index++] = parse_next(input, offset, error, options); if (error) return json(class_type::null); consume_white_space(input, offset); @@ -335,7 +365,8 @@ json parser::parse_null(const std::string& input, size_t& offset, } json parser::parse_next(const std::string& input, size_t& offset, - std::error_code& error) + std::error_code& error, + const json::parse_options& options) { assert(!error); char value; @@ -344,9 +375,9 @@ json parser::parse_next(const std::string& input, size_t& offset, switch (value) { case '[': - return parse_array(input, offset, error); + return parse_array(input, offset, error, options); case '{': - return parse_object(input, offset, error); + return parse_object(input, offset, error, options); case '\"': return parse_string(input, offset, error); case 't': diff --git a/src/bourne/detail/parser.hpp b/src/bourne/detail/parser.hpp index e8c2881..673f597 100644 --- a/src/bourne/detail/parser.hpp +++ b/src/bourne/detail/parser.hpp @@ -23,13 +23,20 @@ class parser public: static json parse(const std::string& input); static json parse(const std::string& input, std::error_code& error); + static json parse(const std::string& input, + const json::parse_options& options); + static json parse(const std::string& input, + const json::parse_options& options, + std::error_code& error); private: static void consume_white_space(const std::string& input, size_t& offset); static json parse_object(const std::string& input, size_t& offset, - std::error_code& error); + std::error_code& error, + const json::parse_options& options); static json parse_array(const std::string& input, size_t& offset, - std::error_code& error); + std::error_code& error, + const json::parse_options& options); static json parse_string(const std::string& input, size_t& offset, std::error_code& error); static json parse_number(const std::string& input, size_t& offset, @@ -39,7 +46,8 @@ class parser static json parse_null(const std::string& input, size_t& offset, std::error_code& error); static json parse_next(const std::string& input, size_t& offset, - std::error_code& error); + std::error_code& error, + const json::parse_options& options); }; } } diff --git a/src/bourne/json.cpp b/src/bourne/json.cpp index 32e69ab..4396bd0 100644 --- a/src/bourne/json.cpp +++ b/src/bourne/json.cpp @@ -537,12 +537,24 @@ bool json::contains(const json& other) const json json::parse(const std::string& input, std::error_code& error) { assert(!error); - return detail::parser::parse(input, error); + return detail::parser::parse(input, json::parse_options{}, error); +} + +json json::parse(const std::string& input, const json::parse_options& options, + std::error_code& error) +{ + assert(!error); + return detail::parser::parse(input, options, error); } json json::parse(const std::string& input) { - return detail::parser::parse(input); + return detail::parser::parse(input, json::parse_options{}); +} + +json json::parse(const std::string& input, const json::parse_options& options) +{ + return detail::parser::parse(input, options); } json json::array() diff --git a/src/bourne/json.hpp b/src/bourne/json.hpp index a159e94..4ffbff9 100644 --- a/src/bourne/json.hpp +++ b/src/bourne/json.hpp @@ -49,6 +49,15 @@ class json using array_type = detail::backing_data::array_type; public: + struct parse_options + { + /// Enable strict JSON parsing checks. + /// + /// Currently this enables: + /// - bourne::error::parse_object_duplicate_key + bool strict = false; + }; + /// Default constructor, creates a null value. json(); @@ -343,9 +352,16 @@ class json /// Parse a string as a json object. static json parse(const std::string& input, std::error_code& error); + /// Parse a string as a json object with options. + static json parse(const std::string& input, const parse_options& options, + std::error_code& error); + /// Parse a string as a json object. static json parse(const std::string& input); + /// Parse a string as a json object with options. + static json parse(const std::string& input, const parse_options& options); + /// Create a json array static json array(); template diff --git a/test/src/test_parser.cpp b/test/src/test_parser.cpp index dd2e289..e4f85ac 100644 --- a/test/src/test_parser.cpp +++ b/test/src/test_parser.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace @@ -212,3 +213,44 @@ TEST(test_parser, test_parse_unicode) ASSERT_FALSE((bool)error); ASSERT_EQ(json_string, result.dump_min()); } + +TEST(test_parser, test_parse_object_duplicate_key_non_strict) +{ + std::error_code error; + std::string json_string = "{\"bourne\":1,\"bourne\":2}"; + auto result = bourne::detail::parser::parse(json_string, error); + + ASSERT_FALSE((bool)error); + ASSERT_TRUE(result.is_object()); + ASSERT_TRUE(result.has_key("bourne")); + EXPECT_EQ(2, result["bourne"].to_int()); +} + +TEST(test_parser, test_parse_object_duplicate_key_strict) +{ + std::error_code error; + std::string json_string = "{\"bourne\":1,\"bourne\":2}"; + bourne::json::parse_options options; + options.strict = true; + + auto result = bourne::detail::parser::parse(json_string, options, error); + + EXPECT_EQ(bourne::error::parse_object_duplicate_key, error) + << error.message(); + ASSERT_EQ(bourne::json::null(), result); +} + +TEST(test_parser, test_parse_nested_object_duplicate_key_strict) +{ + std::error_code error; + std::string json_string = + "{\"outer\":{\"bourne\":1,\"bourne\":2},\"ok\":true}"; + bourne::json::parse_options options; + options.strict = true; + + auto result = bourne::json::parse(json_string, options, error); + + EXPECT_EQ(bourne::error::parse_object_duplicate_key, error) + << error.message(); + ASSERT_EQ(bourne::json::null(), result); +} From 8fc2081c8f21d672f69b5a8912eae9b5469298fc Mon Sep 17 00:00:00 2001 From: "Morten V. Pedersen" Date: Wed, 25 Mar 2026 13:27:34 +0100 Subject: [PATCH 3/3] Update workflow --- .github/workflows/cpp-internal.yml | 70 ++++++++++++------------------ 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/.github/workflows/cpp-internal.yml b/.github/workflows/cpp-internal.yml index 71c94dd..c03450d 100644 --- a/.github/workflows/cpp-internal.yml +++ b/.github/workflows/cpp-internal.yml @@ -1,18 +1,11 @@ name: C++ Internal - on: schedule: - - cron: 0 1 * * * # Nightly at 01:00 UTC + - cron: 0 1 * * * # Nightly at 01:00 UTC push: branches: - master pull_request: - workflow_dispatch: - inputs: - extra_resolve_options: - description: Extra Resolve Options - required: false - jobs: linux_cmake: timeout-minutes: 45 @@ -47,18 +40,16 @@ jobs: - ${{ matrix.config.runner }} name: ${{ matrix.config.name }} steps: - # This is sometimes needed when running docker builds since these - # sometimes produce files with root ownership - name: Ensure correct owner of repository run: sudo chown -R actions-runner:actions-runner . - name: Checkout source code uses: actions/checkout@v3 - - name: Waf Clean - run: python3 waf clean --no_resolve - name: Waf Configure run: python3 waf configure --git_protocol=git@ --cmake_toolchain=${{ matrix.config.toolchain }} --cmake_verbose - name: Waf Build - run: python3 waf build --run_tests + run: python3 waf build + - name: Waf Run Tests + run: python3 waf --run_tests valgrind: timeout-minutes: 45 @@ -70,41 +61,40 @@ jobs: steps: - name: Ensure correct owner of repository run: sudo chown -R actions-runner:actions-runner . - - name: Checkout source code uses: actions/checkout@v3 - - - name: Waf Clean - run: python3 waf clean --no_resolve - - name: Waf Configure run: python3 waf configure --git_protocol=git@ --cmake_toolchain=./resolve_symlinks/toolchains/gcc-toolchain.cmake --cmake_verbose - - name: Waf Build - run: python3 waf build --run_tests --ctest_valgrind - + run: python3 waf build + - name: Waf Run Tests + run: python3 waf --run_tests --ctest_valgrind zig_toolchain_build: name: Zig Toolchain Build (Docker) runs-on: [self-hosted, vm, ubuntu-current] container: - image: kassany/bookworm-ziglang + image: ghcr.io/steinwurf/build-images/zig-cpp options: --user 0:0 - volumes: - - /root/.ssh:/root/.ssh steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Install dependencies + with: + persist-credentials: false + - name: Configure Github Authentication run: | - apt-get update - apt-get install -y python3 python3-pip git cmake build-essential - - name: Waf Clean - run: python3 waf clean --no_resolve + git config --global credential.helper 'store' + git credential approve <