From 5f121d8c50433bc6c33b8954d93a6a86dc8544fe Mon Sep 17 00:00:00 2001 From: Angadi56 Date: Mon, 3 Aug 2026 12:05:53 +0530 Subject: [PATCH] avoid sign extension in char_traits::to_int_type (#5336) * avoid sign extension in char_traits::to_int_type Signed-off-by: Angadi Yashaswini * spell out-of-range signed char constants as negative values (MSVC C4309) Signed-off-by: Angadi Yashaswini --------- Signed-off-by: Angadi Yashaswini --- include/nlohmann/detail/meta/type_traits.hpp | 4 ++- single_include/nlohmann/json.hpp | 4 ++- tests/src/unit-deserialization.cpp | 24 ++++++++++++++++ tests/src/unit-type_traits.cpp | 30 ++++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 1d5092742..ebf6a2c26 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -231,7 +231,9 @@ struct char_traits : std::char_traits // Redefine to_int_type function static int_type to_int_type(char_type c) noexcept { - return static_cast(c); + // cast via unsigned char: sign-extending a negative char_type would make + // byte 0xFF indistinguishable from eof() + return static_cast(static_cast(c)); } static char_type to_char_type(int_type i) noexcept diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2dbf1f911..2c0869257 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3994,7 +3994,9 @@ struct char_traits : std::char_traits // Redefine to_int_type function static int_type to_int_type(char_type c) noexcept { - return static_cast(c); + // cast via unsigned char: sign-extending a negative char_type would make + // byte 0xFF indistinguishable from eof() + return static_cast(static_cast(c)); } static char_type to_char_type(int_type i) noexcept diff --git a/tests/src/unit-deserialization.cpp b/tests/src/unit-deserialization.cpp index cedcb21a9..77d49c08b 100644 --- a/tests/src/unit-deserialization.cpp +++ b/tests/src/unit-deserialization.cpp @@ -427,6 +427,30 @@ TEST_CASE("deserialization") CHECK(l.events == std::vector({"boolean(true)"})); } + SECTION("from std::vector") + { + std::vector const v = {'t', 'r', 'u', 'e'}; + CHECK(json::parse(v) == json(true)); + CHECK(json::accept(v)); + + SaxEventLogger l; + CHECK(json::sax_parse(v, &l)); + CHECK(l.events.size() == 1); + CHECK(l.events == std::vector({"boolean(true)"})); + + // bytes outside ASCII are negative here and must not be sign-extended; + // 0xC3 and 0xA9 do not fit in signed char (MSVC C4309), so spell them as negative values + std::vector const umlaut = {'"', static_cast(0xC3 - 0x100), static_cast(0xA9 - 0x100), '"'}; + CHECK(json::parse(umlaut) == json("\xC3\xA9")); + CHECK(json::accept(umlaut)); + + // 0xFF (spelled as -1 to stay in range) must not be reported as end of input + std::vector const trailing = {'t', 'r', 'u', 'e', static_cast(0xFF - 0x100)}; + json _; + CHECK_THROWS_WITH_AS(_ = json::parse(trailing), "[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - invalid literal; last read: 'true\xFF'; expected end of input", json::parse_error&); + CHECK(!json::accept(trailing)); + } + SECTION("from std::array") { std::array const v { {'t', 'r', 'u', 'e'} }; diff --git a/tests/src/unit-type_traits.cpp b/tests/src/unit-type_traits.cpp index d083293dd..6dc166d03 100644 --- a/tests/src/unit-type_traits.cpp +++ b/tests/src/unit-type_traits.cpp @@ -53,4 +53,34 @@ TEST_CASE("type traits") // NOLINTEND(hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) } } + + SECTION("char_traits") + { + SECTION("to_int_type does not sign-extend") + { + using unsigned_traits = nlohmann::detail::char_traits; + using signed_traits = nlohmann::detail::char_traits; + + CHECK(unsigned_traits::to_int_type(static_cast(0x7F)) == 0x7F); + CHECK(unsigned_traits::to_int_type(static_cast(0x80)) == 0x80); + CHECK(unsigned_traits::to_int_type(static_cast(0xFF)) == 0xFF); + + CHECK(signed_traits::to_int_type(static_cast(0x7F)) == 0x7F); + // 0x80 and 0xFF do not fit in signed char (MSVC C4309), so spell them as negative values + CHECK(signed_traits::to_int_type(static_cast(0x80 - 0x100)) == 0x80); + CHECK(signed_traits::to_int_type(static_cast(0xFF - 0x100)) == 0xFF); + } + + SECTION("no byte value collides with eof") + { + using unsigned_traits = nlohmann::detail::char_traits; + using signed_traits = nlohmann::detail::char_traits; + + for (int i = 0; i < 256; ++i) + { + CHECK(unsigned_traits::to_int_type(static_cast(i)) != unsigned_traits::eof()); + CHECK(signed_traits::to_int_type(static_cast(i)) != signed_traits::eof()); + } + } + } }