diff --git a/docs/binary.md b/docs/binary.md index 00dfb749e9..5c92ecface 100644 --- a/docs/binary.md +++ b/docs/binary.md @@ -438,6 +438,23 @@ struct glz::meta { }; ``` +### Keys that are not numeric or string + +A BEVE object (map) header encodes a *single shared key type*, so the compact map encoding requires keys that reduce to a numeric or string type. When a key does not, such as a multi-field struct, the map/pair serializers automatically fall back to a **generic array of `[key, value]` entries**, where each key is written as a full BEVE value. The container still round-trips with no extra code: + +```c++ +struct CompositeKey { + int32_t id{}; + std::string name{}; + auto operator<=>(const CompositeKey&) const = default; +}; + +std::unordered_map m{{{1, "a"}, 10}, {{2, "b"}, 20}}; +glz::write_beve(m, beve); // a generic array of [key, value] entries +``` + +This applies to `std::map`, `std::unordered_map`, `std::vector>`, and a standalone `std::pair`. The fallback is portable and self-describing, and each entry is encoded identically to `std::tuple` (so a non-native `std::pair` and the corresponding `std::tuple` produce the same bytes). Native (numeric/string) keys are unaffected and keep the compact shared-header encoding. + ## Partial Objects It is sometimes desirable to write out only a portion of an object. This is permitted via an array of JSON pointers, which indicate which parts of the object should be written out. diff --git a/include/glaze/beve/key_traits.hpp b/include/glaze/beve/key_traits.hpp index d6682e4805..957a79056c 100644 --- a/include/glaze/beve/key_traits.hpp +++ b/include/glaze/beve/key_traits.hpp @@ -56,6 +56,10 @@ namespace glz using numeric_type = typename beve_numeric_type::type; static constexpr bool numeric = std::is_arithmetic_v; + // A BEVE object header encodes a single shared key type, which must be numeric or string-like. + // Native keys use the compact object/map encoding; keys that reduce to neither (e.g. multi-field + // structs) fall back to a generic array of [key, value] entries in the map/pair serializers. + static constexpr bool native = numeric || str_t; static constexpr bool as_string = str_t || !numeric; static constexpr bool as_number = !as_string; diff --git a/include/glaze/beve/read.hpp b/include/glaze/beve/read.hpp index d98d440fa1..1b36819c71 100644 --- a/include/glaze/beve/read.hpp +++ b/include/glaze/beve/read.hpp @@ -1470,47 +1470,86 @@ namespace glz using Element = typename T::value_type; using Key = typename Element::first_type; - constexpr uint8_t header = beve_key_traits::header; + if constexpr (beve_key_traits::native) { + constexpr uint8_t header = beve_key_traits::header; - if (invalid_end(ctx, it, end)) { - return; - } - const auto tag = uint8_t(*it); - if (tag != header) [[unlikely]] { - if constexpr (check_allow_conversions(Opts)) { - const auto key_type = tag & 0b000'11'000; - if constexpr (beve_key_traits::as_string) { - if (key_type != 0) { - ctx.error = error_code::syntax_error; - return; + if (invalid_end(ctx, it, end)) { + return; + } + const auto tag = uint8_t(*it); + if (tag != header) [[unlikely]] { + if constexpr (check_allow_conversions(Opts)) { + const auto key_type = tag & 0b000'11'000; + if constexpr (beve_key_traits::as_string) { + if (key_type != 0) { + ctx.error = error_code::syntax_error; + return; + } + } + else { + if (key_type == 0) { + ctx.error = error_code::syntax_error; + return; + } } } else { - if (key_type == 0) { - ctx.error = error_code::syntax_error; - return; - } + ctx.error = error_code::syntax_error; + return; } } - else { - ctx.error = error_code::syntax_error; + + ++it; + const size_t n = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { return; } - } - ++it; - const size_t n = int_from_compressed(ctx, it, end); - if (bool(ctx.error)) [[unlikely]] { - return; + // Validate count against remaining buffer size (minimum 1 byte per key-value pair) + if (n > size_t(end - it)) [[unlikely]] { + ctx.error = error_code::unexpected_end; + return; + } + + value.clear(); + + constexpr uint8_t key_tag = beve_key_traits::key_tag; + for (size_t i = 0; i < n; ++i) { + auto& item = value.emplace_back(); + parse::op()>(item.first, key_tag, ctx, it, end); + parse::op(item.second, ctx, it, end); + } } + else { + // Non-native key: read the generic-array fallback (an array of [key, value] entries; see + // the writer). Each entry is itself a generic array, identical to std::tuple. + if (invalid_end(ctx, it, end)) { + return; + } + if (uint8_t(*it) != tag::generic_array) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + ++it; - value.clear(); + const size_t n = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + // Validate count against remaining buffer size (minimum 1 byte per entry) + if (n > size_t(end - it)) [[unlikely]] { + ctx.error = error_code::unexpected_end; + return; + } - constexpr uint8_t key_tag = beve_key_traits::key_tag; - for (size_t i = 0; i < n; ++i) { - auto& item = value.emplace_back(); - parse::op()>(item.first, key_tag, ctx, it, end); - parse::op(item.second, ctx, it, end); + value.clear(); + for (size_t i = 0; i < n; ++i) { + auto& item = value.emplace_back(); + parse::op(item, ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + } } } }; @@ -1523,31 +1562,60 @@ namespace glz { using Key = typename T::first_type; - constexpr uint8_t header = beve_key_traits::header; + if constexpr (beve_key_traits::native) { + constexpr uint8_t header = beve_key_traits::header; - if (invalid_end(ctx, it, end)) { - return; - } - const auto tag = uint8_t(*it); - if (tag != header) [[unlikely]] { - ctx.error = error_code::syntax_error; - return; - } + if (invalid_end(ctx, it, end)) { + return; + } + const auto tag = uint8_t(*it); + if (tag != header) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } - ++it; - const auto n = int_from_compressed(ctx, it, end); - if (bool(ctx.error)) [[unlikely]] { - return; - } + ++it; + const auto n = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } - if (n != 1) [[unlikely]] { - ctx.error = error_code::syntax_error; - return; + if (n != 1) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + + constexpr uint8_t key_tag = beve_key_traits::key_tag; + parse::op()>(value.first, key_tag, ctx, it, end); + parse::op(value.second, ctx, it, end); } + else { + // Non-native key: read the generic-array fallback [key, value], identical to + // std::tuple. Each element carries its own header. + if (invalid_end(ctx, it, end)) { + return; + } + if (uint8_t(*it) != tag::generic_array) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + ++it; + + const auto n = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + if (n != 2) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } - constexpr uint8_t key_tag = beve_key_traits::key_tag; - parse::op()>(value.first, key_tag, ctx, it, end); - parse::op(value.second, ctx, it, end); + parse::op(value.first, ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + parse::op(value.second, ctx, it, end); + } } }; @@ -1559,6 +1627,84 @@ namespace glz { using Key = typename T::key_type; + // Non-native key (e.g. a multi-field struct): a BEVE object header can't carry a struct key, + // so the writer emits a generic array of [key, value] entries. Read that fallback form here; + // native keys use the compact object/map encoding below. + if constexpr (not beve_key_traits::native) { + if (invalid_end(ctx, it, end)) { + return; + } + if (uint8_t(*it) != tag::generic_array) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + ++it; + + const size_t n = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + // Validate count against remaining buffer size (minimum 1 byte per entry) + if (n > size_t(end - it)) [[unlikely]] { + ctx.error = error_code::unexpected_end; + return; + } + if constexpr (check_max_map_size(Opts) > 0) { + if (n > check_max_map_size(Opts)) [[unlikely]] { + ctx.error = error_code::invalid_length; + return; + } + } + if constexpr (has_runtime_max_map_size>) { + if (ctx.max_map_size > 0 && n > ctx.max_map_size) [[unlikely]] { + ctx.error = error_code::invalid_length; + return; + } + } + + for (size_t i = 0; i < n; ++i) { + // each entry is a generic array [key, value] + if (invalid_end(ctx, it, end)) { + return; + } + if (uint8_t(*it) != tag::generic_array) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + ++it; + const auto m = int_from_compressed(ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + if (m != 2) [[unlikely]] { + ctx.error = error_code::syntax_error; + return; + } + + Key key{}; + parse::op(key, ctx, it, end); + if (bool(ctx.error)) [[unlikely]] { + return; + } + if constexpr (Opts.partial_read) { + if (auto element = value.find(key); element != value.end()) { + parse::op(element->second, ctx, it, end); + } + else { + typename T::mapped_type discard{}; + parse::op(discard, ctx, it, end); + } + } + else { + parse::op(value[key], ctx, it, end); + } + if (bool(ctx.error)) [[unlikely]] { + return; + } + } + return; + } + constexpr uint8_t header = beve_key_traits::header; if (invalid_end(ctx, it, end)) { @@ -1618,7 +1764,7 @@ namespace glz } } - constexpr uint8_t key_tag = beve_key_traits::key_tag; + [[maybe_unused]] constexpr uint8_t key_tag = beve_key_traits::key_tag; if constexpr (beve_key_traits::as_number) { Key key{}; // Value-initialize to silence false positive -Wmaybe-uninitialized @@ -1651,7 +1797,7 @@ namespace glz } } } - else { + else if constexpr (beve_key_traits::native) { Key key; for (size_t i = 0; i < n; ++i) { if constexpr (Opts.partial_read) { diff --git a/include/glaze/beve/size.hpp b/include/glaze/beve/size.hpp index dcc103ebc5..f438e5b858 100644 --- a/include/glaze/beve/size.hpp +++ b/include/glaze/beve/size.hpp @@ -332,15 +332,30 @@ namespace glz requires(map_like_array && check_concatenate(Opts) == true) [[nodiscard]] static size_t op(auto&& value, size_t offset = 0) { - size_t result = 1; // tag byte - result += compressed_int_size(value.size()); // element count + using Key = typename range_value_t>::first_type; + if constexpr (beve_key_traits::native) { + size_t result = 1; // tag byte + result += compressed_int_size(value.size()); // element count + + for (auto&& [k, v] : value) { + result += calculate_size::template no_header(k, offset + result); + result += calculate_size::template op(v, offset + result); + } - for (auto&& [k, v] : value) { - result += calculate_size::template no_header(k, offset + result); - result += calculate_size::template op(v, offset + result); + return result; } + else { + // Non-native key: sized as a generic array of [key, value] entries (each entry matches + // std::tuple). + size_t result = 1; // generic_array tag + result += compressed_int_size(value.size()); // element count - return result; + for (auto&& entry : value) { + result += calculate_size::template op(entry, offset + result); + } + + return result; + } } }; @@ -350,14 +365,28 @@ namespace glz template [[nodiscard]] GLZ_ALWAYS_INLINE static size_t op(auto&& value, size_t offset = 0) { - size_t result = 1; // tag byte - result += compressed_int_size<1>(); // count = 1 + using Key = typename T::first_type; + if constexpr (beve_key_traits::native) { + size_t result = 1; // tag byte + result += compressed_int_size<1>(); // count = 1 - const auto& [k, v] = value; - result += calculate_size::template no_header(k, offset + result); - result += calculate_size::template op(v, offset + result); + const auto& [k, v] = value; + result += calculate_size::template no_header(k, offset + result); + result += calculate_size::template op(v, offset + result); - return result; + return result; + } + else { + // Non-native key: sized as a generic array [key, value] (matches std::tuple). + size_t result = 1; // generic_array tag + result += compressed_int_size<2>(); // count = 2 + + const auto& [k, v] = value; + result += calculate_size::template op(k, offset + result); + result += calculate_size::template op(v, offset + result); + + return result; + } } }; @@ -367,15 +396,30 @@ namespace glz template [[nodiscard]] static size_t op(auto&& value, size_t offset = 0) { - size_t result = 1; // tag byte - result += compressed_int_size(value.size()); // element count + using Key = typename T::key_type; + if constexpr (beve_key_traits::native) { + size_t result = 1; // tag byte + result += compressed_int_size(value.size()); // element count - for (auto&& [k, v] : value) { - result += calculate_size::template no_header(k, offset + result); - result += calculate_size::template op(v, offset + result); + for (auto&& [k, v] : value) { + result += calculate_size::template no_header(k, offset + result); + result += calculate_size::template op(v, offset + result); + } + + return result; } + else { + // Non-native key: sized as a generic array of [key, value] entries (each entry matches + // std::tuple). + size_t result = 1; // generic_array tag + result += compressed_int_size(value.size()); // element count - return result; + for (auto&& entry : value) { + result += calculate_size::template op(entry, offset + result); + } + + return result; + } } }; diff --git a/include/glaze/beve/write.hpp b/include/glaze/beve/write.hpp index 3206e9cbad..8d7c033b94 100644 --- a/include/glaze/beve/write.hpp +++ b/include/glaze/beve/write.hpp @@ -958,25 +958,45 @@ namespace glz using Element = typename T::value_type; using Key = typename Element::first_type; - constexpr uint8_t tag = beve_key_traits::header; - dump_type(ctx, tag, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; - } + if constexpr (beve_key_traits::native) { + constexpr uint8_t tag = beve_key_traits::header; + dump_type(ctx, tag, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } - dump_compressed_int(ctx, value.size(), b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; - } - for (auto&& [k, v] : value) { - serialize::no_header(k, ctx, b, ix); + dump_compressed_int(ctx, value.size(), b, ix); if (bool(ctx.error)) [[unlikely]] { return; } - serialize::op(v, ctx, b, ix); + for (auto&& [k, v] : value) { + serialize::no_header(k, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + serialize::op(v, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + } + } + else { + // Non-native key: a BEVE object header can't carry a struct key, so write a generic array + // of [key, value] entries. This matches std::vector> on the wire. + if (!ensure_space(ctx, b, ix + 1 + write_padding_bytes)) [[unlikely]] { + return; + } + dump(b, ix); + dump_compressed_int(ctx, value.size(), b, ix); if (bool(ctx.error)) [[unlikely]] { return; } + for (auto&& entry : value) { + serialize::op(entry, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + } } } }; @@ -989,22 +1009,42 @@ namespace glz { using Key = typename T::first_type; - constexpr uint8_t tag = beve_key_traits::header; - dump_type(ctx, tag, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; - } + if constexpr (beve_key_traits::native) { + constexpr uint8_t tag = beve_key_traits::header; + dump_type(ctx, tag, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } - dump_compressed_int<1>(ctx, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; + dump_compressed_int<1>(ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + const auto& [k, v] = value; + serialize::no_header(k, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + serialize::op(v, ctx, b, ix); } - const auto& [k, v] = value; - serialize::no_header(k, ctx, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; + else { + // Non-native key: write the pair as a generic array [key, value], identical to + // std::tuple. Each element carries its own header. + if (!ensure_space(ctx, b, ix + 1 + write_padding_bytes)) [[unlikely]] { + return; + } + dump(b, ix); + dump_compressed_int<2>(ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + const auto& [k, v] = value; + serialize::op(k, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + serialize::op(v, ctx, b, ix); } - serialize::op(v, ctx, b, ix); } }; @@ -1018,39 +1058,75 @@ namespace glz using val_t = std::remove_cvref_t>; constexpr bool may_skip = null_t && Opts.skip_null_members; - constexpr uint8_t tag = beve_key_traits::header; - dump_type(ctx, tag, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; - } - - size_t count = value.size(); - if constexpr (may_skip) { - count = 0; - for (auto&& [k, v] : value) { - (void)k; - if (!skip_member(v)) ++count; + if constexpr (beve_key_traits::native) { + constexpr uint8_t tag = beve_key_traits::header; + dump_type(ctx, tag, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; } - } - dump_compressed_int(ctx, count, b, ix); - if (bool(ctx.error)) [[unlikely]] { - return; - } - for (auto&& [k, v] : value) { + size_t count = value.size(); if constexpr (may_skip) { - if (skip_member(v)) continue; + count = 0; + for (auto&& [k, v] : value) { + (void)k; + if (!skip_member(v)) ++count; + } } - serialize::no_header(k, ctx, b, ix); + + dump_compressed_int(ctx, count, b, ix); if (bool(ctx.error)) [[unlikely]] { return; } - serialize::op(v, ctx, b, ix); + for (auto&& [k, v] : value) { + if constexpr (may_skip) { + if (skip_member(v)) continue; + } + serialize::no_header(k, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + serialize::op(v, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + if constexpr (is_output_streaming>) { + flush_buffer(b, ix); + } + } + } + else { + // Non-native key: write a generic array of [key, value] entries (each entry identical to + // std::tuple), since a BEVE object header can't carry a struct key. + if (!ensure_space(ctx, b, ix + 1 + write_padding_bytes)) [[unlikely]] { + return; + } + dump(b, ix); + + size_t count = value.size(); + if constexpr (may_skip) { + count = 0; + for (auto&& [k, v] : value) { + (void)k; + if (!skip_member(v)) ++count; + } + } + + dump_compressed_int(ctx, count, b, ix); if (bool(ctx.error)) [[unlikely]] { return; } - if constexpr (is_output_streaming>) { - flush_buffer(b, ix); + for (auto&& entry : value) { + if constexpr (may_skip) { + if (skip_member(entry.second)) continue; + } + serialize::op(entry, ctx, b, ix); + if (bool(ctx.error)) [[unlikely]] { + return; + } + if constexpr (is_output_streaming>) { + flush_buffer(b, ix); + } } } } diff --git a/tests/beve_test/beve_test.cpp b/tests/beve_test/beve_test.cpp index 3c94fa0abc..a6424188b1 100644 --- a/tests/beve_test/beve_test.cpp +++ b/tests/beve_test/beve_test.cpp @@ -77,6 +77,35 @@ struct std::hash size_t operator()(const CastModuleID& id) const noexcept { return std::hash{}(id.value); } }; +// A multi-field key that reduces to neither a numeric nor a string type. A BEVE object header encodes +// a single shared key type, so this key can't use the compact map encoding; the serializers fall back +// to a generic array of [key, value] entries instead. See issue #2666. +struct CompositeKey +{ + int32_t id{}; + std::string name{}; + + auto operator<=>(const CompositeKey&) const = default; +}; + +// An explicit entry struct, equivalent on the wire to the [key, value] fallback above. +struct CompositeEntry +{ + CompositeKey key{}; + int value{}; + + auto operator<=>(const CompositeEntry&) const = default; +}; + +template <> +struct std::hash +{ + size_t operator()(const CompositeKey& k) const noexcept + { + return std::hash{}(k.id) ^ (std::hash{}(k.name) << 1); + } +}; + namespace { template @@ -2712,6 +2741,111 @@ suite beve_custom_key_tests = [] { "vector pair CastModuleID"_test = [] { verify_vector_pair_roundtrip(); }; }; +// A BEVE object header encodes a single shared key type. Native (numeric/string-like) keys use the +// compact map encoding; non-native keys (e.g. multi-field structs) fall back to a generic array of +// [key, value] entries so they still round-trip. See issue #2666. +suite beve_key_classification_tests = [] { + // Native keys reduce to a numeric or string type. + static_assert(glz::beve_key_traits::native); + static_assert(glz::beve_key_traits::native); + static_assert(glz::beve_key_traits::native); + static_assert(glz::beve_key_traits::native); + static_assert(glz::beve_key_traits::native); + static_assert(glz::beve_key_traits::native); // wraps a uint64_t via glz::meta + static_assert(glz::beve_key_traits::native); + + // Multi-field structs are not native; they use the generic-array fallback rather than a map header. + static_assert(not glz::beve_key_traits::native); + + // The exact case from issue #2666: a std::unordered_map keyed by a multi-field struct now serializes. + "unordered_map with struct key"_test = [] { + const std::unordered_map src{{{1, "a"}, 10}, {{2, "b"}, 20}, {{3, "c"}, 30}}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::unordered_map dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + }; + + "map with struct key"_test = [] { + const std::map src{{{1, "a"}, 10}, {{2, "b"}, 20}, {{3, "c"}, 30}}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::map dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + + std::string untagged{}; + expect(not glz::write_beve_untagged(src, untagged)); + std::map dst2{}; + expect(!glz::read_beve_untagged(dst2, untagged)); + expect(dst2 == src); + }; + + "vector of pairs with struct key"_test = [] { + const std::vector> src{{{1, "a"}, 10}, {{2, "b"}, 20}, {{3, "c"}, 30}}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::vector> dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + }; + + "standalone pair with struct key"_test = [] { + const std::pair src{{9, "i"}, 42}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::pair dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + }; + + // The fallback encodes each entry exactly like std::tuple: a non-native pair and the + // corresponding tuple must produce identical bytes. + "pair fallback matches tuple bytes"_test = [] { + const std::pair p{{9, "i"}, 42}; + const std::tuple t{{9, "i"}, 42}; + + std::string pair_buffer{}; + std::string tuple_buffer{}; + expect(not glz::write_beve(p, pair_buffer)); + expect(not glz::write_beve(t, tuple_buffer)); + expect(pair_buffer == tuple_buffer); + }; + + // An explicit entry struct still works and is equivalent to the [key, value] fallback. + "vector of explicit entries"_test = [] { + const std::vector src{{{1, "a"}, 10}, {{2, "b"}, 20}, {{3, "c"}, 30}}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::vector dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + + std::string untagged{}; + expect(not glz::write_beve_untagged(src, untagged)); + std::vector dst2{}; + expect(!glz::read_beve_untagged(dst2, untagged)); + expect(dst2 == src); + }; + + // std::tuple already round-trips a non-native first element. + "tuple with struct first element"_test = [] { + const std::tuple src{{9, "i"}, 42}; + + std::string buffer{}; + expect(not glz::write_beve(src, buffer)); + std::tuple dst{}; + expect(!glz::read_beve(dst, buffer)); + expect(dst == src); + }; +}; + suite beve_to_json_tests = [] { "beve_to_json bool"_test = [] { bool b = true; @@ -4904,6 +5038,29 @@ suite dos_prevention = [] { auto ec = glz::read_beve(result, truncated); expect(bool(ec)) << "Should fail on truncated map"; }; + + "vector-of-pairs with huge key count protection"_test = [] { + // std::vector> shares the concatenated-map encoding, but is read by a separate + // path than std::map. A buffer claiming a huge element count must be rejected before allocating. + std::vector> sample = {{"one", 1}, {"two", 2}}; + std::string valid_buffer; + expect(not glz::write_beve(sample, valid_buffer)); + + // Reuse the object-header byte, then claim ~2^40 entries with no element data following. The + // count stays below the compressed-int safety limit (2^48) so it reaches the per-element bound. + std::string malicious; + malicious.push_back(valid_buffer.at(0)); // object header for a string-keyed map + const uint64_t huge_count = uint64_t(1) << 40; + const uint64_t encoded = (huge_count << 2) | 0b11; // 8-byte compressed int (config 3) + for (int i = 0; i < 8; ++i) { + malicious.push_back(char(uint8_t(encoded >> (8 * i)))); + } + + std::vector> result; + auto ec = glz::read_beve(result, malicious); + expect(bool(ec)) << "Should reject huge vector-of-pairs count, not allocate"; + expect(result.empty()); + }; }; // Custom opts for max_string_length and max_array_size tests