From 25e7edad54b8347ee3bbeefadf11a144e14d9fdc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 23 Oct 2026 10:10:45 +0200 Subject: [PATCH] Add shared detail helpers for the #5891 cleanup Add detail::is_primitive(value_t), detail::hex4_lower(), detail::utf8_shape(), detail::input_format_name(), detail::empty_input_message(), the exception ids 116, 319, 320 and 416 to detail::exception_id, and make detail::is_ascii_copyable() constexpr. Later commits switch the duplicated code over to them. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/exceptions.hpp | 4 + .../nlohmann/detail/input/input_adapters.hpp | 30 +++++++ include/nlohmann/detail/input/string_scan.hpp | 2 +- include/nlohmann/detail/string_utils.hpp | 78 +++++++++++++++++++ include/nlohmann/detail/value_t.hpp | 29 +++++++ 5 files changed, 142 insertions(+), 1 deletion(-) diff --git a/include/nlohmann/detail/exceptions.hpp b/include/nlohmann/detail/exceptions.hpp index 7d7f4e038..c887f7ecd 100644 --- a/include/nlohmann/detail/exceptions.hpp +++ b/include/nlohmann/detail/exceptions.hpp @@ -66,6 +66,7 @@ enum class exception_id : int invalid_string_or_size = 113, ///< a binary input contains an invalid string or size specification bson_unsupported_type = 114, ///< a BSON record type is not supported invalid_high_precision_number = 115, ///< a UBJSON/BJData high-precision number cannot be parsed + invalid_document_image = 116, ///< json_document::load() rejects an image // invalid_iterator iterators_incompatible = 201, ///< the iterators of a range belong to different values iterator_from_other_value = 202, ///< an iterator does not belong to the value it is used with @@ -100,6 +101,8 @@ enum class exception_id : int invalid_utf8 = 316, ///< dump() finds a string that is not valid UTF-8 type_not_serializable = 317, ///< a value cannot be serialized to the requested binary format enum_key_duplicate = 318, ///< an enum-keyed map has two keys that serialize to the same string + document_binary_value = 319, ///< a binary value is stored in a json_document + document_image_unsupported = 320, ///< a json_document image cannot be saved or loaded (discarded document, big-endian target) discarded_value_used = 321, ///< a discarded value is used // out_of_range array_index_out_of_range = 401, ///< an array index is out of range @@ -117,6 +120,7 @@ enum class exception_id : int patch_remove_parent_not_container = 413, ///< the parent of a JSON Patch 'remove' target is not a container patch_move_into_child = 414, ///< a JSON Patch 'move' moves a value into one of its children subtype_out_of_range = 415, ///< a binary subtype does not fit into one byte + document_too_large = 416, ///< a json_document input, its edit storage, or an image reaches the 32-bit (4 GiB) limit // other_error internal_error = 500, ///< unreachable code was reached patch_test_failed = 501, ///< a JSON Patch 'test' operation failed diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 05fd4c816..2246c3f4f 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -67,6 +67,36 @@ namespace detail /// the supported input formats enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; +/// @return the name of a binary input format, as used in exception messages +inline const char* input_format_name(const input_format_t format) noexcept +{ + switch (format) + { + case input_format_t::cbor: + return "CBOR"; + case input_format_t::msgpack: + return "MessagePack"; + case input_format_t::ubjson: + return "UBJSON"; + case input_format_t::bson: + return "BSON"; + case input_format_t::bjdata: + return "BJData"; + case input_format_t::bon8: + return "BON8"; + case input_format_t::json: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return ""; // LCOV_EXCL_LINE + } +} + +/// @return the message of the parse_error thrown or reported for an empty input +inline const char* empty_input_message() noexcept +{ + return "attempting to parse an empty input; check that your input string or stream contains the expected JSON"; +} + //////////////////// // input adapters // //////////////////// diff --git a/include/nlohmann/detail/input/string_scan.hpp b/include/nlohmann/detail/input/string_scan.hpp index abe39e475..d01a2f1a4 100644 --- a/include/nlohmann/detail/input/string_scan.hpp +++ b/include/nlohmann/detail/input/string_scan.hpp @@ -96,7 +96,7 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* d // printable ASCII except '"' and '\\' - is emitted unchanged. Note this differs // from is_string_special() only in that 0x7F is also a stop (it is escaped as // \u007f under ensure_ascii). -inline bool is_ascii_copyable(unsigned char c) noexcept +constexpr bool is_ascii_copyable(unsigned char c) noexcept { return c >= 0x20u && c < 0x7Fu && c != '"' && c != '\\'; } diff --git a/include/nlohmann/detail/string_utils.hpp b/include/nlohmann/detail/string_utils.hpp index e54a56da6..0fd041893 100644 --- a/include/nlohmann/detail/string_utils.hpp +++ b/include/nlohmann/detail/string_utils.hpp @@ -48,6 +48,24 @@ inline std::string hex_byte(const std::uint8_t byte) return result; } +/*! +@brief write a UTF-16 code unit as four lowercase hexadecimal digits + +The lowercase counterpart of hex_byte(), used for the `\\uXXXX` escapes of the +serializers, whose output is lowercase. Writes exactly four characters to +@a out and returns the pointer past them. +*/ +JSON_HEDLEY_ALWAYS_INLINE char* hex4_lower(const std::uint16_t codeunit, char* out) noexcept +{ + constexpr const char* nibble_to_hex = "0123456789abcdef"; + const unsigned int unit = codeunit; + out[0] = nibble_to_hex[(unit >> 12u) & 0x0Fu]; + out[1] = nibble_to_hex[(unit >> 8u) & 0x0Fu]; + out[2] = nibble_to_hex[(unit >> 4u) & 0x0Fu]; + out[3] = nibble_to_hex[unit & 0x0Fu]; + return out + 4; +} + /////////////////// // UTF-8 encoding // /////////////////// @@ -103,6 +121,66 @@ void encode_utf8(std::uint32_t cp, const Out& out) } } +/*! +@brief the shape of a well-formed UTF-8 sequence, by its lead byte + +@a length is 1 for ASCII, 2 to 4 for a valid lead byte, and 0 for a byte that +cannot start a well-formed sequence (a continuation byte, 0xC0, 0xC1, or 0xF5 +to 0xFF). For a multibyte sequence, the second byte must lie in +[@a second_lo, @a second_hi], and every later byte in [0x80, 0xBF] +(RFC 3629, section 4; Unicode Table 3-7). The narrower second-byte ranges +exclude overlong forms (0xE0, 0xF0), surrogates (0xED), and code points +beyond U+10FFFF (0xF4). +*/ +struct utf8_sequence_shape +{ + std::uint8_t length; + std::uint8_t second_lo; + std::uint8_t second_hi; +}; + +/// @return the shape of a UTF-8 sequence starting with @a lead, see utf8_sequence_shape +inline utf8_sequence_shape utf8_shape(const std::uint8_t lead) noexcept +{ + if (lead < 0x80u) + { + return {1, 0, 0}; + } + if (lead < 0xC2u) + { + return {0, 0, 0}; + } + if (lead <= 0xDFu) + { + return {2, 0x80u, 0xBFu}; + } + if (lead == 0xE0u) + { + return {3, 0xA0u, 0xBFu}; + } + if (lead == 0xEDu) + { + return {3, 0x80u, 0x9Fu}; + } + if (lead <= 0xEFu) + { + return {3, 0x80u, 0xBFu}; + } + if (lead == 0xF0u) + { + return {4, 0x90u, 0xBFu}; + } + if (lead <= 0xF3u) + { + return {4, 0x80u, 0xBFu}; + } + if (lead == 0xF4u) + { + return {4, 0x80u, 0x8Fu}; + } + return {0, 0, 0}; +} + /////////////////// // UTF-8 decoding // /////////////////// diff --git a/include/nlohmann/detail/value_t.hpp b/include/nlohmann/detail/value_t.hpp index 9f64cb90b..f52762c4e 100644 --- a/include/nlohmann/detail/value_t.hpp +++ b/include/nlohmann/detail/value_t.hpp @@ -177,6 +177,35 @@ inline bool owns_memory(const value_t t) noexcept } } +/*! +@brief whether a value of the given type is neither an object nor an array + +Unlike basic_json::is_primitive(), this includes value_t::discarded: it names +the types for which iterators, JSON Pointer resolution, and the serializer take +the "single value" path. Every enumerator is listed so that -Wswitch-enum flags +this function when a new type is added. +*/ +inline bool is_primitive(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + return false; + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return true; + } +} + /*! @brief compare an integer with a floating point number without precision loss