Skip to content

Fix to_bjdata() silently truncating out-of-range _ArrayData_ elements - #5473

Merged
nlohmann merged 1 commit into
developfrom
issue-5403-bjdata-uint-range
Sep 10, 2026
Merged

nlohmann merged 1 commit into
developfrom
issue-5403-bjdata-uint-range

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

The BJData ndarray writer (write_bjdata_ndarray in include/nlohmann/detail/output/binary_writer.hpp) validated the kind of each _ArrayData_ element (integer vs. float) against the declared _ArrayType_, but not its range. Each element is written with a narrowing cast, e.g. write_number(static_cast<std::uint8_t>(el.get<std::uint64_t>()), true) for _ArrayType_ = "uint8". An out-of-range element (e.g. 256) was silently wrapped by that cast instead of being reported — no error, no fallback.

json j;
j["_ArrayType_"] = "uint8";
j["_ArraySize_"] = json::array({2});
j["_ArrayData_"] = json::array({1, 256});   // 256 does not fit uint8
auto v = json::to_bjdata(j);
// json::from_bjdata(v).dump() used to give "[1,0]" instead of round-tripping

Fix

Each _ArrayData_ element is now range-checked against the C++ type named by _ArrayType_ before being cast:

  • integer types (uint8/int8/uint16/int16/uint32/int32/uint64/int64, plus char/byte which share the uint8 range) are checked with the existing value_in_range_of<TargetType> helper, handling both signed and unsigned JSON number storage (so a negative value is correctly rejected for an unsigned target).
  • single (32-bit float) is checked so that a finite double which would overflow to infinity when narrowed to float is rejected.
  • double needs no additional check since it already spans the full range of the internal float representation.

When any element is out of range, the function falls back to the same plain-object encoding already used by this function for other invalid-annotation cases (mismatched kind, non-array _ArraySize_, overflowing dimensions, etc.), so the value round-trips through from_bjdata(to_bjdata(j)) instead of being silently corrupted.

Tests

Added a new section "ndarray with out-of-range _ArrayData_ elements stays as object" in tests/src/unit-bjdata.cpp covering:

  • an out-of-range uint8 element (256) falls back and round-trips
  • an out-of-range int8 element (200) falls back and round-trips
  • a negative element under an unsigned type (uint16) falls back and round-trips
  • a single (float) element that overflows to infinity (1e40) falls back and round-trips
  • in-range boundary values (uint8 0/255, int8 -128/127, single 1.5) still use the compact ndarray encoding

Ran the full unit-bjdata suite offline (compiled against include/ with a stub test_data.hpp): 693935/693936 assertions pass; the one failure and the one skipped test case are pre-existing and unrelated (they require downloaded test data, unavailable in this offline setup).

Breaking change?

No breaking changes. This only affects the BJData ndarray fast-path encoding for annotated objects whose _ArrayData_ previously contained values outside the range implied by _ArrayType_; such objects are now encoded as plain objects (as they already are for the other invalid-annotation cases handled by this same function) instead of emitting silently corrupted data.

Fixes #5403.

— opened by Claude Code on behalf of @nlohmann

@nlohmann nlohmann added 🚀 ready to merge Ready to merge - just waiting for CI to complete. and removed review needed It would be great if someone could review the proposed changes. labels Sep 8, 2026
@nlohmann nlohmann added this to the Release 3.13.0 milestone Sep 9, 2026
write_bjdata_ndarray() validated that each _ArrayData_ element matched
the number kind (integer vs. float) named by _ArrayType_, but not its
range. An element that did not fit the target C++ type (e.g. 256 for
"uint8") was silently wrapped by the static_cast used to write it, or,
for "single", silently overflowed to infinity.

Range-check each element against the type named by _ArrayType_ before
writing it, reusing the existing fallback path that already encodes
the annotated object as a plain object for other invalid-annotation
cases in this function.

Fixes #5403.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the issue-5403-bjdata-uint-range branch from d2c2db9 to 9eb08e9 Compare September 9, 2026 08:21
@nlohmann
nlohmann merged commit bfb0778 into develop Sep 10, 2026
249 of 251 checks passed
@nlohmann
nlohmann deleted the issue-5403-bjdata-uint-range branch September 10, 2026 15:59
nlohmann added a commit that referenced this pull request Oct 4, 2026
- binary_reader: keep emitting "_ArrayType_" from get_ubjson_size_value()
  (before "_ArraySize_"), now via develop's static bjd_type_name(); drop
  develop's later emission in get_ubjson_array()
- binary_writer: develop's out-of-range check for ND-array elements
  (#5473, #5730) now also requires that single-precision elements survive
  the narrowing exactly, which is what this branch adds for #5661

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L 🚀 ready to merge Ready to merge - just waiting for CI to complete. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

to_bjdata() silently truncates out-of-range _ArrayData_ elements (e.g. 256 as uint8 becomes 0)

2 participants