From 0ffe9ab4a8035b956944183010d7bf74cd2e2f9d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 09:54:00 +0200 Subject: [PATCH] Merge the duplicated UBJSON/BJData integer marker ladders write_number_with_ubjson_prefix() (unsigned and signed overloads) and ubjson_prefix() (number_integer and number_unsigned cases) each picked the UBJSON/BJData integer marker (i, U, I, u, l, m, L, M, H) with their own independent if/else ladder, and the values beyond 64 bits were handled by a second, tag-dispatched pair of ladders. An optimized container announces the marker of its first element via ubjson_prefix() and then writes every element through write_number_with_ubjson_prefix(), so the two had to be kept in lockstep by hand across four call sites. Replace all of that with one ubjson_integer_prefix() built on value_in_range_of, and one write_ubjson_integer_payload() that writes the value (or, for 'H', the decimal digits) for a given marker. write_number_with_ubjson_prefix() and ubjson_prefix() keep their signatures and now just call these two helpers. Behavior, the public API and the ABI are unchanged. Verified with a new regression test covering scalars and $-optimized arrays/objects at every int8/uint8/int16/uint16/int32/uint32/int64/uint64 boundary for to_ubjson/to_bjdata (both use_size/use_type settings), and by diffing to_ubjson/to_bjdata output before and after over the json_test_data corpus (bit-identical). Part of #5710 Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 336 +++++------------- single_include/nlohmann/json.hpp | 336 +++++------------- tests/src/unit-ubjson.cpp | 221 ++++++++++++ 3 files changed, 413 insertions(+), 480 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index b39146a4f..36798ec84 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1494,202 +1494,122 @@ class binary_writer write_number(n, use_bjdata); } - // UBJSON: write number (unsigned integer) + // UBJSON: write number (integer) template::value, int>::type = 0> + std::is_integral::value, int>::type = 0> void write_number_with_ubjson_prefix(const NumberType n, const bool add_prefix, const bool use_bjdata) { - if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('i')); // int8 - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('U')); // uint8 - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('I')); // int16 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('u')); // uint16 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('l')); // int32 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('m')); // uint32 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('L')); // int64 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata) - { - if (add_prefix) - { - oa.write_character(to_char_type('M')); // uint64 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else - { - if (add_prefix) - { - oa.write_character(to_char_type('H')); // high-precision number - } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } - } - } - - // UBJSON: write number (signed integer) - template < typename NumberType, typename std::enable_if < - std::is_signed::value&& - !std::is_floating_point::value, int >::type = 0 > - void write_number_with_ubjson_prefix(const NumberType n, - const bool add_prefix, - const bool use_bjdata) - { - if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('i')); // int8 - } - write_number(static_cast(n), use_bjdata); - } - else if (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('U')); // uint8 - } - write_number(static_cast(n), use_bjdata); - } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('I')); // int16 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)()))) - { - if (add_prefix) - { - oa.write_character(to_char_type('u')); // uint16 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('l')); // int32 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)()))) - { - if (add_prefix) - { - oa.write_character(to_char_type('m')); // uint32 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else - { - // every value of an integer type of at most 64 bits fits into an - // int64; only a wider type needs a range check - write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, - std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); - } - } - - template - void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::true_type /*fits_int64*/) - { + const CharType prefix = ubjson_integer_prefix(n, use_bjdata); if (add_prefix) { - oa.write_character(to_char_type('L')); // int64 + oa.write_character(prefix); } - write_number(static_cast(n), use_bjdata); + write_ubjson_integer_payload(prefix, n, use_bjdata); } + /*! + @brief determine the UBJSON/BJData type marker of an integer + + This is the only place that picks the marker of an integer: both + write_number_with_ubjson_prefix() and ubjson_prefix() use it. An optimized + container announces the marker of its first value after `$` and then + writes every value without a marker, so the two must never disagree. + + @param[in] n the integer + @param[in] use_bjdata whether the BJData-only markers `u`, `m`, and `M` + may be used + + @return the first marker of `i`, `U`, `I`, `u` (BJData), `l`, `m` (BJData), + `L`, `M` (BJData, unsigned types only), and `H` (high-precision + number) whose range contains @a n + */ template - void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::false_type /*fits_int64*/) + static CharType ubjson_integer_prefix(const NumberType n, const bool use_bjdata) noexcept { - if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + if (value_in_range_of(n)) { - write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, std::true_type {}); - return; + return 'i'; } - - if (add_prefix) + if (value_in_range_of(n)) { - oa.write_character(to_char_type('H')); // high-precision number + return 'U'; } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) + if (value_in_range_of(n)) { - oa.write_character(to_char_type(static_cast(number[i]))); + return 'I'; } + if (use_bjdata && value_in_range_of(n)) + { + return 'u'; + } + if (value_in_range_of(n)) + { + return 'l'; + } + if (use_bjdata && value_in_range_of(n)) + { + return 'm'; + } + if (value_in_range_of(n)) + { + return 'L'; + } + if (use_bjdata && std::is_unsigned::value) + { + return 'M'; + } + // anything else is treated as a high-precision number + return 'H'; } + /*! + @brief write the value of an integer for the marker chosen by + ubjson_integer_prefix() + */ template - static constexpr CharType ubjson_int64_or_high_precision_prefix(const NumberType /*n*/, std::true_type /*fits_int64*/) noexcept + void write_ubjson_integer_payload(const CharType prefix, const NumberType n, const bool use_bjdata) { - return 'L'; - } - - template - static CharType ubjson_int64_or_high_precision_prefix(const NumberType n, std::false_type /*fits_int64*/) noexcept - { - // anything outside of the range of an int64 is treated as a - // high-precision number - return ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) ? 'L' : 'H'; + switch (prefix) + { + case 'i': + write_number(static_cast(n), use_bjdata); + break; + case 'U': + write_number(static_cast(n), use_bjdata); + break; + case 'I': + write_number(static_cast(n), use_bjdata); + break; + case 'u': + write_number(static_cast(n), use_bjdata); + break; + case 'l': + write_number(static_cast(n), use_bjdata); + break; + case 'm': + write_number(static_cast(n), use_bjdata); + break; + case 'L': + write_number(static_cast(n), use_bjdata); + break; + case 'M': + write_number(static_cast(n), use_bjdata); + break; + default: + { + // high-precision number: the decimal digits as a string + JSON_ASSERT(prefix == 'H'); + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + break; + } + } } /*! @@ -1706,74 +1626,10 @@ class binary_writer return j.m_data.m_value.boolean ? 'T' : 'F'; case value_t::number_integer: - { - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'i'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'U'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'I'; - } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) - { - return 'u'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'l'; - } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) - { - return 'm'; - } - // every value of an integer type of at most 64 bits fits into - // an int64; only a wider type needs a range check - return ubjson_int64_or_high_precision_prefix(j.m_data.m_value.number_integer, - std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); - } + return ubjson_integer_prefix(j.m_data.m_value.number_integer, use_bjdata); case value_t::number_unsigned: - { - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'i'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'U'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'I'; - } - if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'u'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'l'; - } - if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'm'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'L'; - } - if (use_bjdata) - { - return 'M'; - } - // anything else is treated as a high-precision number - return 'H'; - } + return ubjson_integer_prefix(j.m_data.m_value.number_unsigned, use_bjdata); case value_t::number_float: return get_ubjson_float_prefix(j.m_data.m_value.number_float); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 63630a008..2cc621bc6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21824,202 +21824,122 @@ class binary_writer write_number(n, use_bjdata); } - // UBJSON: write number (unsigned integer) + // UBJSON: write number (integer) template::value, int>::type = 0> + std::is_integral::value, int>::type = 0> void write_number_with_ubjson_prefix(const NumberType n, const bool add_prefix, const bool use_bjdata) { - if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('i')); // int8 - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('U')); // uint8 - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('I')); // int16 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('u')); // uint16 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('l')); // int32 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('m')); // uint32 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if (n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('L')); // int64 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata) - { - if (add_prefix) - { - oa.write_character(to_char_type('M')); // uint64 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else - { - if (add_prefix) - { - oa.write_character(to_char_type('H')); // high-precision number - } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } - } - } - - // UBJSON: write number (signed integer) - template < typename NumberType, typename std::enable_if < - std::is_signed::value&& - !std::is_floating_point::value, int >::type = 0 > - void write_number_with_ubjson_prefix(const NumberType n, - const bool add_prefix, - const bool use_bjdata) - { - if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('i')); // int8 - } - write_number(static_cast(n), use_bjdata); - } - else if (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)())) - { - if (add_prefix) - { - oa.write_character(to_char_type('U')); // uint8 - } - write_number(static_cast(n), use_bjdata); - } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('I')); // int16 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)()))) - { - if (add_prefix) - { - oa.write_character(to_char_type('u')); // uint16 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('l')); // int32 - } - write_number(static_cast(n), use_bjdata); - } - else if (use_bjdata && (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)()))) - { - if (add_prefix) - { - oa.write_character(to_char_type('m')); // uint32 - bjdata only - } - write_number(static_cast(n), use_bjdata); - } - else - { - // every value of an integer type of at most 64 bits fits into an - // int64; only a wider type needs a range check - write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, - std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); - } - } - - template - void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::true_type /*fits_int64*/) - { + const CharType prefix = ubjson_integer_prefix(n, use_bjdata); if (add_prefix) { - oa.write_character(to_char_type('L')); // int64 + oa.write_character(prefix); } - write_number(static_cast(n), use_bjdata); + write_ubjson_integer_payload(prefix, n, use_bjdata); } + /*! + @brief determine the UBJSON/BJData type marker of an integer + + This is the only place that picks the marker of an integer: both + write_number_with_ubjson_prefix() and ubjson_prefix() use it. An optimized + container announces the marker of its first value after `$` and then + writes every value without a marker, so the two must never disagree. + + @param[in] n the integer + @param[in] use_bjdata whether the BJData-only markers `u`, `m`, and `M` + may be used + + @return the first marker of `i`, `U`, `I`, `u` (BJData), `l`, `m` (BJData), + `L`, `M` (BJData, unsigned types only), and `H` (high-precision + number) whose range contains @a n + */ template - void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::false_type /*fits_int64*/) + static CharType ubjson_integer_prefix(const NumberType n, const bool use_bjdata) noexcept { - if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + if (value_in_range_of(n)) { - write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, std::true_type {}); - return; + return 'i'; } - - if (add_prefix) + if (value_in_range_of(n)) { - oa.write_character(to_char_type('H')); // high-precision number + return 'U'; } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) + if (value_in_range_of(n)) { - oa.write_character(to_char_type(static_cast(number[i]))); + return 'I'; } + if (use_bjdata && value_in_range_of(n)) + { + return 'u'; + } + if (value_in_range_of(n)) + { + return 'l'; + } + if (use_bjdata && value_in_range_of(n)) + { + return 'm'; + } + if (value_in_range_of(n)) + { + return 'L'; + } + if (use_bjdata && std::is_unsigned::value) + { + return 'M'; + } + // anything else is treated as a high-precision number + return 'H'; } + /*! + @brief write the value of an integer for the marker chosen by + ubjson_integer_prefix() + */ template - static constexpr CharType ubjson_int64_or_high_precision_prefix(const NumberType /*n*/, std::true_type /*fits_int64*/) noexcept + void write_ubjson_integer_payload(const CharType prefix, const NumberType n, const bool use_bjdata) { - return 'L'; - } - - template - static CharType ubjson_int64_or_high_precision_prefix(const NumberType n, std::false_type /*fits_int64*/) noexcept - { - // anything outside of the range of an int64 is treated as a - // high-precision number - return ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) ? 'L' : 'H'; + switch (prefix) + { + case 'i': + write_number(static_cast(n), use_bjdata); + break; + case 'U': + write_number(static_cast(n), use_bjdata); + break; + case 'I': + write_number(static_cast(n), use_bjdata); + break; + case 'u': + write_number(static_cast(n), use_bjdata); + break; + case 'l': + write_number(static_cast(n), use_bjdata); + break; + case 'm': + write_number(static_cast(n), use_bjdata); + break; + case 'L': + write_number(static_cast(n), use_bjdata); + break; + case 'M': + write_number(static_cast(n), use_bjdata); + break; + default: + { + // high-precision number: the decimal digits as a string + JSON_ASSERT(prefix == 'H'); + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + break; + } + } } /*! @@ -22036,74 +21956,10 @@ class binary_writer return j.m_data.m_value.boolean ? 'T' : 'F'; case value_t::number_integer: - { - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'i'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'U'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'I'; - } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) - { - return 'u'; - } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'l'; - } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) - { - return 'm'; - } - // every value of an integer type of at most 64 bits fits into - // an int64; only a wider type needs a range check - return ubjson_int64_or_high_precision_prefix(j.m_data.m_value.number_integer, - std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); - } + return ubjson_integer_prefix(j.m_data.m_value.number_integer, use_bjdata); case value_t::number_unsigned: - { - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'i'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'U'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'I'; - } - if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'u'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'l'; - } - if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'm'; - } - if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) - { - return 'L'; - } - if (use_bjdata) - { - return 'M'; - } - // anything else is treated as a high-precision number - return 'H'; - } + return ubjson_integer_prefix(j.m_data.m_value.number_unsigned, use_bjdata); case value_t::number_float: return get_ubjson_float_prefix(j.m_data.m_value.number_float); diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index c315fac94..80c771d42 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -3033,3 +3033,224 @@ TEST_CASE("UBJSON optimized array of unsigned integers beyond int64") CHECK(json::to_ubjson(j, true, true) == expected); CHECK(json::from_ubjson(expected) == j); } + +namespace +{ +// the bytes that follow the marker of an integer: the value in the width of +// the marker (big endian for UBJSON, little endian for BJData), or, for a +// high-precision number, the length and the decimal digits +std::vector integer_payload(const char marker, const json& value, const bool little_endian) +{ + std::size_t width = 0; + switch (marker) + { + case 'i': + case 'U': + width = 1; + break; + case 'I': + case 'u': + width = 2; + break; + case 'l': + case 'm': + width = 4; + break; + case 'L': + case 'M': + width = 8; + break; + default: + { + const std::string digits = value.dump(); + std::vector result = {'i', static_cast(digits.size())}; + for (const char c : digits) + { + result.push_back(static_cast(c)); + } + return result; + } + } + + const std::uint64_t bits = value.is_number_unsigned() + ? value.get() + : static_cast(value.get()); + std::vector result(width); + for (std::size_t i = 0; i < width; ++i) + { + result[little_endian ? i : width - 1 - i] = static_cast(bits >> (8 * i)); + } + return result; +} + +json i64(const std::int64_t v) +{ + return v; +} + +json u64(const std::uint64_t v) +{ + return v; +} +} // namespace + +TEST_CASE("UBJSON and BJData integer markers at every range edge") +{ + // An optimized container announces the marker of its values after `$` and + // then writes every value without a marker, so the marker the writer + // announces and the width it writes must match for every value. This + // checks both for the values around each edge of the integer types, as + // scalars and as the values of optimized arrays and objects. + struct integer_case + { + json value; + char ubjson; // expected UBJSON marker + char bjdata; // expected BJData marker + }; + + const std::int64_t int64_min = (std::numeric_limits::min)(); + const std::int64_t int64_max = (std::numeric_limits::max)(); + const std::uint64_t uint64_max = (std::numeric_limits::max)(); + + const std::vector cases = + { + // int8 + {i64(-129), 'I', 'I'}, + {i64(-128), 'i', 'i'}, + {i64(-127), 'i', 'i'}, + {i64(-1), 'i', 'i'}, + {i64(0), 'i', 'i'}, + {u64(0), 'i', 'i'}, + {i64(126), 'i', 'i'}, + {i64(127), 'i', 'i'}, + {u64(127), 'i', 'i'}, + {i64(128), 'U', 'U'}, + {u64(128), 'U', 'U'}, + // uint8 + {i64(254), 'U', 'U'}, + {i64(255), 'U', 'U'}, + {u64(255), 'U', 'U'}, + {i64(256), 'I', 'I'}, + {u64(256), 'I', 'I'}, + // int16 + {i64(-32769), 'l', 'l'}, + {i64(-32768), 'I', 'I'}, + {i64(-32767), 'I', 'I'}, + {i64(32766), 'I', 'I'}, + {i64(32767), 'I', 'I'}, + {u64(32767), 'I', 'I'}, + {i64(32768), 'l', 'u'}, + {u64(32768), 'l', 'u'}, + // uint16 (BJData only) + {i64(65534), 'l', 'u'}, + {i64(65535), 'l', 'u'}, + {u64(65535), 'l', 'u'}, + {i64(65536), 'l', 'l'}, + {u64(65536), 'l', 'l'}, + // int32 + {i64(-2147483649LL), 'L', 'L'}, + {i64(-2147483648LL), 'l', 'l'}, + {i64(-2147483647LL), 'l', 'l'}, + {i64(2147483646LL), 'l', 'l'}, + {i64(2147483647LL), 'l', 'l'}, + {u64(2147483647ULL), 'l', 'l'}, + {i64(2147483648LL), 'L', 'm'}, + {u64(2147483648ULL), 'L', 'm'}, + // uint32 (BJData only) + {i64(4294967294LL), 'L', 'm'}, + {i64(4294967295LL), 'L', 'm'}, + {u64(4294967295ULL), 'L', 'm'}, + {i64(4294967296LL), 'L', 'L'}, + {u64(4294967296ULL), 'L', 'L'}, + // int64 + {i64(int64_min), 'L', 'L'}, + {i64(int64_min + 1), 'L', 'L'}, + {i64(int64_max - 1), 'L', 'L'}, + {i64(int64_max), 'L', 'L'}, + {u64(static_cast(int64_max)), 'L', 'L'}, + // uint64 (BJData only; UBJSON writes a high-precision number) + {u64(static_cast(int64_max) + 1), 'H', 'M'}, + {u64(uint64_max - 1), 'H', 'M'}, + {u64(uint64_max), 'H', 'M'}, + }; + + for (const auto& c : cases) + { + for (const bool bjdata : + { + false, true + }) + { + const char marker = bjdata ? c.bjdata : c.ubjson; + const std::vector payload = integer_payload(marker, c.value, bjdata); + const auto to_binary = [bjdata](const json & j, const bool use_size, const bool use_type) + { + return bjdata ? json::to_bjdata(j, use_size, use_type) : json::to_ubjson(j, use_size, use_type); + }; + const auto from_binary = [bjdata](const std::vector& v) + { + return bjdata ? json::from_bjdata(v) : json::from_ubjson(v); + }; + INFO("value = " << c.value.dump() << (c.value.is_number_unsigned() ? " (unsigned)" : "") << ", format = " << (bjdata ? "BJData" : "UBJSON")); + + // scalar + std::vector expected = {static_cast(marker)}; + expected.insert(expected.end(), payload.begin(), payload.end()); + for (const bool use_size : + { + false, true + }) + { + CHECK(to_binary(c.value, use_size, false) == expected); + } + CHECK(from_binary(expected) == c.value); + + const json arr = {c.value, c.value, c.value}; + + // array without count or type: every value has its marker + expected = {'['}; + for (int i = 0; i < 3; ++i) + { + expected.push_back(static_cast(marker)); + expected.insert(expected.end(), payload.begin(), payload.end()); + } + expected.push_back(']'); + CHECK(to_binary(arr, false, false) == expected); + CHECK(from_binary(expected) == arr); + + // array with count: every value has its marker + expected = {'[', '#', 'i', 3}; + for (int i = 0; i < 3; ++i) + { + expected.push_back(static_cast(marker)); + expected.insert(expected.end(), payload.begin(), payload.end()); + } + CHECK(to_binary(arr, true, false) == expected); + CHECK(from_binary(expected) == arr); + + // array with type and count: the marker once, then the payloads + expected = {'[', '$', static_cast(marker), '#', 'i', 3}; + for (int i = 0; i < 3; ++i) + { + expected.insert(expected.end(), payload.begin(), payload.end()); + } + CHECK(to_binary(arr, true, true) == expected); + CHECK(from_binary(expected) == arr); + + // object with type and count: the marker once, then key and payload + const json obj = {{"a", c.value}, {"b", c.value}}; + expected = {'{', '$', static_cast(marker), '#', 'i', 2}; + for (const char key : + {'a', 'b' + }) + { + expected.push_back('i'); + expected.push_back(1); + expected.push_back(static_cast(key)); + expected.insert(expected.end(), payload.begin(), payload.end()); + } + CHECK(to_binary(obj, true, true) == expected); + CHECK(from_binary(expected) == obj); + } + } +}