diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 25a64d648..e2d47f75a 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -465,18 +465,12 @@ class serializer { if (codepoint <= 0xFFFF) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x", - static_cast(codepoint))); - bytes += 6; + bytes = write_u_escape(bytes, static_cast(codepoint)); } else { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", - static_cast(0xD7C0u + (codepoint >> 10u)), - static_cast(0xDC00u + (codepoint & 0x3FFu)))); - bytes += 12; + bytes = write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + bytes = write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } else @@ -683,6 +677,33 @@ class serializer return result; } + /*! + * @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer + * + * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the + * string escaping hot path. It writes exactly six characters ('\\', 'u' and + * four hex digits) at position @a pos of @a string_buffer via a nibble + * lookup table, avoiding the format-string parsing and locale machinery of + * `snprintf`. + * + * @param[in] pos position in @a string_buffer to write at; there must + * be at least 6 bytes of headroom + * @param[in] codeunit 16-bit value to encode + * @return position just after the written escape sequence (@a pos + 6) + */ + std::size_t write_u_escape(std::size_t pos, std::uint16_t codeunit) noexcept + { + JSON_ASSERT(string_buffer.size() - pos >= 6); + static constexpr const char nibble_to_hex[] = "0123456789abcdef"; + string_buffer[pos + 0] = '\\'; + string_buffer[pos + 1] = 'u'; + string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; + string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; + string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; + string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + return pos + 6; + } + // templates to avoid warnings about useless casts template ::value, int> = 0> bool is_negative_number(NumberType x) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1a2b3d357..6e69c8c4d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19752,18 +19752,12 @@ class serializer { if (codepoint <= 0xFFFF) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x", - static_cast(codepoint))); - bytes += 6; + bytes = write_u_escape(bytes, static_cast(codepoint)); } else { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", - static_cast(0xD7C0u + (codepoint >> 10u)), - static_cast(0xDC00u + (codepoint & 0x3FFu)))); - bytes += 12; + bytes = write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + bytes = write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } else @@ -19970,6 +19964,33 @@ class serializer return result; } + /*! + * @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer + * + * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the + * string escaping hot path. It writes exactly six characters ('\\', 'u' and + * four hex digits) at position @a pos of @a string_buffer via a nibble + * lookup table, avoiding the format-string parsing and locale machinery of + * `snprintf`. + * + * @param[in] pos position in @a string_buffer to write at; there must + * be at least 6 bytes of headroom + * @param[in] codeunit 16-bit value to encode + * @return position just after the written escape sequence (@a pos + 6) + */ + std::size_t write_u_escape(std::size_t pos, std::uint16_t codeunit) noexcept + { + JSON_ASSERT(string_buffer.size() - pos >= 6); + static constexpr const char nibble_to_hex[] = "0123456789abcdef"; + string_buffer[pos + 0] = '\\'; + string_buffer[pos + 1] = 'u'; + string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; + string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; + string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; + string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + return pos + 6; + } + // templates to avoid warnings about useless casts template ::value, int> = 0> bool is_negative_number(NumberType x) diff --git a/tests/src/unit-convenience.cpp b/tests/src/unit-convenience.cpp index f5104855f..037a4e589 100644 --- a/tests/src/unit-convenience.cpp +++ b/tests/src/unit-convenience.cpp @@ -168,6 +168,32 @@ TEST_CASE("convenience functions") CHECK_THROWS_WITH_AS(check_escaped("\xC2"), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2", json::type_error&); } + SECTION("string escape with ensure_ascii") + { + // control characters are escaped regardless of ensure_ascii + check_escaped("\x01", "\\u0001", true); + check_escaped("\x1f", "\\u001f", true); + + // non-ASCII code points in the Basic Multilingual Plane are emitted as + // a single lowercase \uXXXX escape (exercises every nibble position) + check_escaped("\xC2\x80", "\\u0080", true); // U+0080 + check_escaped("\xC3\xBF", "\\u00ff", true); // U+00FF (ÿ) + check_escaped("\xDF\xBF", "\\u07ff", true); // U+07FF + check_escaped("\xE4\xBD\xA0", "\\u4f60", true); // U+4F60 (你) + check_escaped("\xEA\xAF\x8D", "\\uabcd", true); // U+ABCD + check_escaped("\xEF\xBF\xBD", "\\ufffd", true); // U+FFFD (replacement char, all-f nibbles) + + // code points outside the BMP are emitted as a UTF-16 surrogate pair + // of two lowercase \uXXXX escapes + check_escaped("\xF0\x90\x80\x80", "\\ud800\\udc00", true); // U+10000 (lowest astral) + check_escaped("\xF0\x9F\x98\x80", "\\ud83d\\ude00", true); // U+1F600 (😀) + check_escaped("\xF4\x8F\xBF\xBF", "\\udbff\\udfff", true); // U+10FFFF (highest code point) + + // with ensure_ascii disabled, non-ASCII input is passed through verbatim + check_escaped("\xE4\xBD\xA0", "\xE4\xBD\xA0", false); + check_escaped("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", false); + } + SECTION("string concat") { using nlohmann::detail::concat;