diff --git a/docs/mkdocs/docs/api/basic_json_document/load.md b/docs/mkdocs/docs/api/basic_json_document/load.md index 46a71cb06..2782653d7 100644 --- a/docs/mkdocs/docs/api/basic_json_document/load.md +++ b/docs/mkdocs/docs/api/basic_json_document/load.md @@ -113,11 +113,12 @@ How thoroughly `load()` validates `image` before trusting it. `full` (the default) : Checks everything the parser itself guarantees: structure and bounds; that every string is valid UTF-8 (and, for a string still in the source text, that it contains no quote, backslash, or control character); and that every number - token is well-formed and matches the value stored for it. Strings and float tokens may share a range only if the - ranges are identical (as nodes that share a value do), and never overlap otherwise. Reading and serializing a - checked image is safe and always produces valid JSON, exactly as for a parsed document. This is the right choice for - an image from anything you do not fully control — a file, a cache shared with other processes, a peer on the - network. + token is well-formed and matches the value stored for it, and has the type the parser gives it (a float node only + for a token with a fraction or an exponent, or an integer that does not fit 64 bits). Strings and float tokens may + share a range only if the ranges are identical (as nodes that share a value do), and never overlap otherwise. + Reading and serializing a checked image is safe and always produces valid JSON, exactly as for a parsed document. + This is the right choice for an image from anything you do not fully control — a file, a cache shared with other + processes, a peer on the network. `bounds` : Checks structure and bounds only: that every offset and count in the node index stays inside the image. So diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp index e6680730f..5459f435f 100644 --- a/include/nlohmann/detail/view/image.hpp +++ b/include/nlohmann/detail/view/image.hpp @@ -416,6 +416,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu return true; } +/// whether an integer token (well-formed, no fraction or exponent) fits 64 +/// bits: the parser stores a number_integer or number_unsigned node for it, +/// and a number_float node only for a token that does not fit +inline bool integer_token_fits(const number_token& t) +{ + if (t.int_digits > 20) + { + return false; + } + if (t.int_digits == 20) + { + // (20 digits are compared with 2^64 - 1; a negative one is below -2^63) + // (only a leading 1 needs the comparison: the first digit of 20 is not 0) + return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0; + } + if (t.negative && t.int_digits == 19) + { + return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0; + } + return true; +} + /// whether an integer node matches its token the way the parser records it /// (after the bounds check; the token has at most 256 characters) inline bool check_integer(const node& n, const unsigned char* text) @@ -535,7 +557,8 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned if (!plain_float_token(s, r.len, r.extra)) { number_token t{}; - if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + // (an integer token is a float node only if it does not fit 64 bits) + if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t)) { return false; } diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index b2523284d..f2a39d268 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7439,6 +7439,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu return true; } +/// whether an integer token (well-formed, no fraction or exponent) fits 64 +/// bits: the parser stores a number_integer or number_unsigned node for it, +/// and a number_float node only for a token that does not fit +inline bool integer_token_fits(const number_token& t) +{ + if (t.int_digits > 20) + { + return false; + } + if (t.int_digits == 20) + { + // (20 digits are compared with 2^64 - 1; a negative one is below -2^63) + // (only a leading 1 needs the comparison: the first digit of 20 is not 0) + return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0; + } + if (t.negative && t.int_digits == 19) + { + return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0; + } + return true; +} + /// whether an integer node matches its token the way the parser records it /// (after the bounds check; the token has at most 256 characters) inline bool check_integer(const node& n, const unsigned char* text) @@ -7558,7 +7580,8 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned if (!plain_float_token(s, r.len, r.extra)) { number_token t{}; - if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + // (an integer token is a float node only if it does not fit 64 bits) + if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t)) { return false; } diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index a4e282639..a339385b5 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -759,6 +759,48 @@ TEST_CASE("json_view images: errors") } } + SECTION("float tokens: an integer token is a float node only if it does not fit 64 bits") + { + for (const char* const token : + { + "18446744073709551616", "-9223372036854775809", "100000000000000000000", "-18446744073709551616", "123456789012345678901234567890" + }) + { + CAPTURE(token) + const std::string text = std::string("[") + token + "]"; + const json_document d = json_document::parse(text); + CHECK(d.root()[0].is_number_float()); + const std::vector image = d.save(); + const std::size_t at = text_at(image) + 1; // the first byte of the token + CHECK(loaded_dump(image) == json::parse(text).dump()); + for (std::size_t pos = token[0] == '-' ? 1 : 0; token[pos] != '\0'; ++pos) // (not the sign: that changes the digits) + { + for (char digit = '0'; digit <= '9'; ++digit) + { + std::vector changed = image; + changed[at + pos] = static_cast(digit); + std::string t = token; + t[pos] = digit; + CAPTURE(pos) + CAPTURE(digit) + CAPTURE(t) + // the parser stores an integer for the changed token if it is one (and fits) + const bool float_node = json::accept(t) && json::parse(t).is_number_float(); + const std::string result = load_result(changed, image_check::full); + if (float_node) + { + CHECK(result.empty()); + CHECK(loaded_dump(changed) == json::parse("[" + t + "]").dump()); + } + else + { + CHECK(result == check_failed); + } + } + } + } + } + SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw") { const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");