From 09b6b6b5ba0610851fba2a79d66fbd801452a107 Mon Sep 17 00:00:00 2001 From: Qatadaha Bin Matloob <79017227+qatcod@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:30:22 +0500 Subject: [PATCH] Fix to_bjdata() emitting unparsable output when _ArraySize_ is not an array (#5455) * Fix to_bjdata() emitting unparsable output when _ArraySize_ is not an array write_bjdata_ndarray() never checked that _ArraySize_ is an array. The shape is written verbatim as the header length, so a null shape emitted 'Z' and an object shape emitted '{' after the '#', neither of which from_bjdata() accepts, and the round-trip guarantee in the BJData docs was broken. Both slipped through the existing validation: for null, empty() is true so the element count starts at 0 and the per-dimension loop never runs, and for an object the loop walks its values, which can satisfy the non-negative integer check. When _ArrayData_ then matched that count, the writer took the ndarray path. Require the shape to be an array, so anything else falls back to a plain object encoding that round-trips, as the fallback rule in the docs already specifies. Signed-off-by: qatcod <79017227+qatcod@users.noreply.github.com> * Document that _ArraySize_ must be an array in the ndarray requirements The list at bjdata.md is the exhaustive set of conditions for the ndarray encoding, but it only implied this one through 'every entry of'. Signed-off-by: qatcod <79017227+qatcod@users.noreply.github.com> --------- Signed-off-by: qatcod <79017227+qatcod@users.noreply.github.com> --- .../docs/features/binary_formats/bjdata.md | 1 + .../nlohmann/detail/output/binary_writer.hpp | 9 +++++++ single_include/nlohmann/json.hpp | 9 +++++++ tests/src/unit-bjdata.cpp | 25 +++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/docs/mkdocs/docs/features/binary_formats/bjdata.md b/docs/mkdocs/docs/features/binary_formats/bjdata.md index faff64f16..d3f63a9b8 100644 --- a/docs/mkdocs/docs/features/binary_formats/bjdata.md +++ b/docs/mkdocs/docs/features/binary_formats/bjdata.md @@ -125,6 +125,7 @@ The library uses the following mapping from JSON values types to BJData types ac - `"_ArrayType_"` is one of `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`, `single`, `double`, `char`, or `byte`, + - `"_ArraySize_"` is an array, since the dimensions are written as the ND-array header's length, - every entry of `"_ArraySize_"` is a non-negative integer, and their product is representable as a `std::size_t`, - `"_ArrayData_"` holds exactly that many elements, and - every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"` (a floating-point number for diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index e636c6d84..b8e9efa45 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1668,6 +1668,15 @@ class binary_writer CharType dtype = it->second; key = "_ArraySize_"; + // the dimensions are written verbatim as the header length below, so a + // value that is not an array cannot produce a valid one: null emits 'Z' + // and an object emits '{', neither of which a reader accepts after '#'. + // Such an object is not a valid ndarray and falls back to a plain object. + if (!value.at(key).is_array()) + { + return true; + } + std::size_t len = (value.at(key).empty() ? 0 : 1); for (const auto& el : value.at(key)) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9c6ed1335..31bfb869d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -18676,6 +18676,15 @@ class binary_writer CharType dtype = it->second; key = "_ArraySize_"; + // the dimensions are written verbatim as the header length below, so a + // value that is not an array cannot produce a valid one: null emits 'Z' + // and an object emits '{', neither of which a reader accepts after '#'. + // Such an object is not a valid ndarray and falls back to a plain object. + if (!value.at(key).is_array()) + { + return true; + } + std::size_t len = (value.at(key).empty() ? 0 : 1); for (const auto& el : value.at(key)) { diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 41feb5288..7d0dd5ff2 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -2751,6 +2751,31 @@ TEST_CASE("BJData") CHECK(json::to_bjdata(j_ok) == std::vector({'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 1, 2, 3, 4, 5, 6})); CHECK(json::from_bjdata(json::to_bjdata(j_ok), true, true) == j_ok); } + + SECTION("ndarray whose _ArraySize_ is not an array stays as object") + { + // the shape is written verbatim as the header length, so a + // value that is not an array cannot produce a valid one: null + // would emit 'Z' and an object '{', neither of which a reader + // accepts after '#'. Both have to stay plain objects. + json const j_null = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", nullptr}, {"_ArrayData_", json::array()}}); + const auto out_null = json::to_bjdata(j_null); + CHECK(out_null.at(0) == '{'); + CHECK(json::from_bjdata(out_null) == j_null); + + // an object shape passes the per-entry check by iterating its + // values rather than dimensions, so it needs rejecting too + json const j_obj = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {{"a", 1}}}, {"_ArrayData_", {1}}}); + const auto out_obj = json::to_bjdata(j_obj); + CHECK(out_obj.at(0) == '{'); + CHECK(json::from_bjdata(out_obj) == j_obj); + + // a scalar shape is not a dimension list either + json const j_num = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", 1}, {"_ArrayData_", {1}}}); + const auto out_num = json::to_bjdata(j_num); + CHECK(out_num.at(0) == '{'); + CHECK(json::from_bjdata(out_num) == j_num); + } } }