mirror of
https://github.com/nlohmann/json.git
synced 2026-09-10 18:27:59 +00:00
* Speed up number parsing in the lexer (fast paths from the fast_float/simdjson world) The number scanner converted its already-validated digit buffer with std::strtoull/std::strtoll/std::strtod. Those pull in locale and errno machinery and dominate number-heavy parsing (strtod runs at ~6 M/s). Replace them with dedicated parsers over the validated buffer: - parse_integer_unsigned / parse_integer_signed: accumulate digits with overflow detection, falling back to the float path on overflow exactly as the strtoull/strtoll round-trip check did. Overflow behavior is unchanged for narrower or wider custom number types. - parse_float_fast: Clinger's exact fast path for `double` (<=19 significant digits, |exp10| <= 22, significand < 2^53), where significand * 10^exp is exact under IEEE round-to-nearest. This is the same fast path used by fast_float/simdjson. It is bit-identical to strtod on this subset and declines (falling back to strtod) otherwise. Only `double` uses it; float and long double keep std::strtof/std::strtold via a templated overload. Measured on representative data (g++ 13, -O3): - integers: DOM parse +11%, SAX +25-34% - floats: DOM parse +37%, SAX +70% (clang: float DOM ~1.9x) No dependencies added; header-only and C++11-clean. Existing parser, lexer, conversion and deserialization unit tests pass unchanged; a 3M-value random-double fuzz matches strtod bit-for-bit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Add SWAR bulk string scanning for contiguous input (simdjson-style) scan_string() read the input one character at a time through the input adapter and classified every byte with a large switch. For contiguous byte buffers we can instead scan 8 bytes at a time with a SWAR word test that finds the first byte needing individual handling (the closing quote, an escape, a control character, or a non-ASCII UTF-8 byte) and bulk-append the ordinary run in one go. - input adapters expose supports_bulk_scan / bulk_data / bulk_remaining / bulk_skip for provably-contiguous, same-type, 1-byte iterator ranges (raw pointers in every standard; std::string/std::vector/std::array and friends additionally in C++20 via std::contiguous_iterator). - the lexer gains a bulk_scan capability (gated on lazy_token_string so bypassing the per-character capture cannot lose error diagnostics) and a scan_string_bulk() fast path; streaming/wide/user adapters are unchanged and keep the byte-at-a-time scanner. The run contains no newline (all bytes < 0x20 are treated as special), so position bookkeeping stays exact, and error tokens are still reconstructed lazily from the consumed byte range. The SWAR special-byte test is pure uint64_t arithmetic - no intrinsics, no runtime dispatch, C++11-clean. Measured on representative data, pointer input, g++ 13 -O3 (string values discarded by accept() see the largest gains): long ASCII strings: DOM +4.5x, SAX +14x, accept +17x (to ~2 GB/s) short strings: DOM +15%, SAX +62%, accept +85% escape-heavy: DOM +31%, SAX +26%, accept +28% Same-input parity verified: 200k randomized documents (escapes, multibyte UTF-8, surrogate pairs) accept/parse identically via the contiguous SWAR path and the streaming byte path; unit lexer/parser/diagnostic-position/ deserialization/conversions suites pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Validate UTF-8 in the bulk string scanner (portable ~2-3x on non-ASCII text) The SWAR bulk string path stopped at the first non-ASCII byte and handed every multibyte character to the byte-at-a-time scanner, whose per-byte get()/next_byte_in_range()/add() machinery runs at roughly half the speed of validating straight from the buffer. As a result, dense non-ASCII text (CJK, emoji, accented Latin) parsed ~10-15x slower than ASCII. Fold well-formed UTF-8 into the bulk run: scan_string_bulk() now, on a non-ASCII lead byte, validates one sequence with validate_one_utf8() - which mirrors scan_string()'s per-byte switch ranges exactly (rejecting overlong forms, surrogates, and out-of-range code points) - and appends it in place, continuing until the closing quote, an escape, a control byte, or an ill-formed sequence. All error handling still defers to the byte path, so error messages and positions are byte-for-byte unchanged. Because only well-formed content is fast-pathed and every rejection falls through to the existing scanner, behavior is identical; the win is purely throughput. Measured on pointer input (accept, string values discarded): content g++ 13 clang 18 dense CJK 277 -> 648 ~605 MB/s (~2.3x) dense emoji 299 -> 857 ~702 MB/s (~2.6-2.9x) mixed 90% ASCII 246 -> 331 ~334 MB/s (~1.35x) pure ASCII unchanged (~3.2 / 4.1 GB/s) Verified: 2,000,000 randomized documents built from arbitrary bytes (overlong, surrogate, truncated, out-of-range sequences) accept/reject and parse identically via the contiguous path and the streaming byte path; lexer/parser/diagnostic-position/deserialization/conversions suites pass unchanged. Pure C++11, no intrinsics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Route contiguous byte containers through the pointer adapter (fast paths in C++11) json::parse(std::string) - the most common entry point - did not benefit from the contiguous fast paths (bulk string scanning, UTF-8 bulk validation, memcpy for binary formats) in C++11..17: std::string::iterator is a library wrapper, not a raw pointer, and pre-C++20 there is no portable way to prove it contiguous, so supports_bulk_scan was false. Only raw pointers, string literals, and C-arrays (and, in C++20, anything modelling std::contiguous_iterator) took the fast path. Detect contiguous single-byte containers (std::string, std::vector<char>, std::vector<std::uint8_t>, std::string_view, ...) via is_contiguous_byte_ container and route them through an iterator_input_adapter built from data()/data()+size(). The generic iterator-based container overload is constrained to exclude these, so the two overloads are disjoint and there is no ambiguity (a plain competing overload loses to the greedy forwarding-reference container overload on reference binding, and a factory partial-specialization is ambiguous - both were tried and rejected). The pointer keeps the container's own element type, so char_type - and therefore all parsing behavior - is byte-for-byte identical to the iterator path (const char* for std::string, const std::uint8_t* for std::vector<std::uint8_t>); only the raw pointer additionally turns on the fast paths. Lifetimes are unchanged: the container outlives the adapter for the full parse expression, exactly as the iterators it replaces did. Measured, C++11, json::parse/accept(std::string), g++ 13: long ASCII strings: accept 201 -> 3200 MB/s (~16x), parse 174 -> 1444 dense CJK: accept 263 -> 697 MB/s (~2.6x) short strings: accept 163 -> 243 MB/s (~1.5x) Verified: char_type preserved for std::string (char) and std::vector<std::uint8_t> (uint8_t); CBOR/MsgPack round-trips from std::vector<std::uint8_t> unchanged; 1,000,000 randomized documents accept and parse identically via std::string and via std::istream; deserialization/user-defined-input/parser/lexer/conversions/diagnostic- position suites pass (20,480 assertions); warning-clean on g++ and clang in C++11/17/20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Add optional simdutf backend for bulk UTF-8 validation (JSON_USE_SIMDUTF) The bulk string scanner validates UTF-8 straight from a contiguous buffer. The scalar validator caps at ~0.3-0.7 GB/s on non-ASCII text; a SIMD validator reaches several GB/s. Rather than hand-rolling SIMD UTF-8 validation (easy to get subtly wrong - a from-scratch SSE attempt rejected valid CJK), wire in the vetted simdutf library behind an opt-in switch. simdutf is not header-only (it ships simdutf.cpp and uses runtime CPU dispatch), so it is not vendored: defining JSON_USE_SIMDUTF includes <simdutf.h> and routes the bulk validator through simdutf::validate_utf8; the project supplies and links simdutf. Undefined (the default), nothing external is included and the portable C++11 scalar path is used, so the library stays header-only and its baseline behavior is unchanged. Design keeps behavior identical either way: - scan_string_bulk() now finds the run up to the next quote/escape/control byte (non-ASCII allowed) and validates it in one shot; on the rare validation failure it recomputes the exact valid prefix with the scalar helper, so ill-formed input still falls through to the byte path and is reported at the same position with the same message. - the per-sequence scalar path is factored into scalar_string_bulk_run() and is the default backend; the refactor is behavior-preserving and does not change scalar throughput. Verified: default and JSON_USE_SIMDUTF builds accept/reject/parse identically across 2,000,000 arbitrary-byte documents and 1,000,000 mixed-escape/UTF-8 documents (differential fuzz vs the streaming byte path); lexer/parser/diagnostic-position/deserialization suites pass under both configurations (20,188 assertions with the backend enabled); warning-clean on g++ and clang, C++11 and C++20, both configurations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Add a contiguous fast path for scanning numbers scan_number() reads a number one character at a time through the input adapter (get()) and appends each byte to token_buffer (add()) before converting. For contiguous input, the per-character get()/add() overhead dominates: it is roughly two thirds of the time spent on number-heavy parsing, far more than the value conversion itself. Add scan_number_bulk_contiguous(), which parses the whole number token straight from the input buffer: it validates and classifies the extent with the same grammar as scan_number()'s state machine, materializes token_buffer in one copy (substituting the locale decimal point exactly as scan_number() does), advances the adapter, and reuses the shared convert_number() tail. On anything it does not recognize as a well-formed number it makes no state change and returns token_type::uninitialized, so the caller falls back to scan_number(), which then produces the exact diagnostic. Errors and their positions are therefore unchanged. The conversion tail is factored out of scan_number() into convert_number() so both scanners share it; the fast path is selected by tag dispatch on the existing bulk_scan capability, so streaming/wide/user adapters are unaffected. Measured on pointer input, g++ 13 -O3: - integers: parse +65%, accept +98% - floats: parse +39%, accept +70% Verified: 2,000,000 randomized number documents (including overflow-range integers, long digit strings and %.17g doubles) parse identically via the contiguous path and the streaming byte path, matching value, type and round-trip text; the locale suite and existing parser/lexer/conversions/ deserialization tests pass; a new "lexer number fast path" test checks contiguous-vs-streaming parity, token classification, and that malformed numbers are rejected identically. Pure C++11, no intrinsics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Move byte-level scan/parse helpers out of lexer.hpp lexer.hpp had grown by ~600 lines of byte-level helpers that have no dependency on the lexer's template parameters and clutter the state machine. Move them, unchanged, into two focused headers as free functions in namespace detail: - number_parse.hpp: parse_integer_unsigned/parse_integer_signed (now templated on the number type) and parse_float_fast (Clinger's exact double fast path, with the decimal point passed as an argument instead of read from a lexer member). - string_scan.hpp: the SWAR string helpers (is_string_special, swar_string_special, find_string_special, validate_one_utf8, scalar_string_bulk_run) and the backend-dispatched string_bulk_run, including the optional simdutf include and find_string_delimiter. lexer.hpp now includes these and calls the free functions; the methods that touch lexer state (scan_string, scan_number, scan_string_bulk, scan_number_bulk_contiguous, convert_number) stay put. This is a pure code move with no behavior change: lexer.hpp drops from 2357 to 1934 lines, the now-unused <cstdint>/<cstring>/<limits> includes are removed, and the free-function form makes the SWAR helpers reusable elsewhere (e.g. the serializer's string escaping). Verified: default and JSON_USE_SIMDUTF builds compile; 2,000,000 number and 2,000,000 arbitrary-byte-string differential-fuzz documents parse identically to before; lexer/parser/conversions/deserialization/locale/ diagnostic-position suites pass (20,576 assertions); warning-clean on g++ and clang in C++11/17/20; the amalgamation regenerates and passes check-amalgamation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy findings and document JSON_USE_SIMDUTF in the nav - number_parse.hpp: use std::array for the powers-of-ten table (avoid-c-arrays) and `auto` for the cast-initialized result (modernize-use-auto), matching the codebase style (cf. the serializer's utf8d table). Indexing casts keep the -Wsign-conversion build clean. - add JSON_USE_SIMDUTF to the mkdocs navigation so the macro page is reachable. No behavior change; clang-tidy is clean on the new headers and the amalgamation is regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix number fast path for custom string types without assign() The contiguous number fast path materialized token_buffer with token_buffer.assign(data, len), but string_t is only required to provide the minimal interface the rest of the lexer uses (push_back, append, clear, operator[], ...). Custom string types such as the test's alt_string do not implement assign(), so scan_number_bulk_contiguous() failed to compile for them (unit-alt-string), breaking the gcc/clang standards and old-compiler CI jobs. reset() already clears token_buffer, so fill it with append() - which alt_string and std::string both provide and which the string fast path already relies on - instead of assign(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Satisfy clang-tidy: parenthesize math and drop unused forwarding reference The CI clang-tidy (newer than the locally available version) reported two additional checks on the new code: - readability-math-missing-parentheses: parenthesize the (a * b) + c digit accumulations in number_parse.hpp. - cppcoreguidelines-missing-std-forward: the contiguous-byte-container input_adapter overload took a forwarding reference but only reads data()/size() and never forwards it. It is already disjoint from the generic container overload via SFINAE, so a plain const& is correct and clearer (and keeps the container alive for the whole parse just as before). No behavior change; char_type and routing are unchanged (std::string and std::vector<std::uint8_t> still take the pointer adapter with char/uint8_t char_type), CBOR/MsgPack round-trips and the 2M number fuzz still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use std::from_chars (Eisel-Lemire) for float conversion when available The Clinger fast path is exact only for the "easy" subset (<=19 significant digits, |exp10| <= 22); high-precision and scientific floats fall through to strtod, where the failed Clinger attempt actually makes parsing a net loss. std::from_chars implements the Eisel-Lemire algorithm in modern standard libraries: locale-independent, correctly rounded, and fast over the whole value range. convert_number() now tries parse_float_from_chars() first (guarded by __cpp_lib_to_chars, so C++11 and libc++-without-float-support keep the Clinger + strtod path unchanged), then Clinger, then strtof. from_chars is used only when it consumes the entire token; a partial parse means a non-'.' locale decimal point, and an under-/overflow (result_out_of_range) also declines - in both cases the existing strtod fallback supplies the exact value and the well-defined +/-inf/0 the parser expects, side-stepping the P4168 divergence between implementations. float and long double now get the fast path too (Clinger was double-only). Measured, C++17, g++ 13 -O3, json::parse/accept: - canada-style floats: ~unchanged (Clinger already covered them) - high-precision (17 digits): parse 2.1x, accept 2.5x - scientific (17 digits + exp): parse 3.6x, accept 4.1x Verified: C++11 (Clinger/strtod) and C++17 (from_chars) parse every value - including subnormals, boundary values, and 1e9999/1e-9999 over-/underflow - to bit-identical results; 2M number-fuzz clean; conversions/deserialization/ locale/number-fast-path suites pass in both C++11 and C++17; clang-tidy clean; warning-clean on g++ and clang in C++11/17/20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Guard from_chars use on JSON_HAS_CPP_17, not just __cpp_lib_to_chars libstdc++ 15 defines __cpp_lib_to_chars even in C++14 mode (via bits/version.h pulled in by other headers), but <charconv> is only included under JSON_HAS_CPP_17. That made parse_float_from_chars() reference std::from_chars without the header in C++14 builds, breaking gcc-latest, icpx, and the offline-testdata jobs. Gate the use on JSON_HAS_CPP_17 && __cpp_lib_to_chars so it matches the include condition exactly; C++11/14 always take the scalar fallback. Verified by forcing __cpp_lib_to_chars in a C++14 build: the guard suppresses std::from_chars and it compiles. C++17 behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Disable Clinger float fast path under extended FP precision (x87) The contiguous number fast path uses a Clinger-style exact algorithm (significand * 10^scale in double arithmetic), which is only correctly rounded when double operations are evaluated in true 53-bit precision. On the x87 FPU used by 32-bit x86 (FLT_EVAL_METHOD == 2) the single multiply/divide is computed in 80-bit and then double-rounded to double, so a small fraction of values land 1 ULP off. This surfaced as test-cbor_cpp11 and test-msgpack_cpp11 failing on the mingw (x86) job for regression/floats.json: the C++17 builds pass because they take the correctly-rounded std::from_chars path, while C++11 falls back to parse_float_fast(). A 5M-sample check over shortest round-trip decimals reproduces it: 0 divergences with 53-bit doubles, ~1 in 25 000 with 80-bit intermediates; declining to std::strtod fixes all of them. Guard parse_float_fast() on FLT_EVAL_METHOD so it declines whenever the platform evaluates doubles in extended precision, letting the caller use the correctly-rounded std::from_chars / std::strtod path instead. On mainstream x86-64/ARM64 (FLT_EVAL_METHOD == 0) the fast path is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Guard <charconv> include with __has_include for GCC 7 GCC 7 sets __cplusplus to the C++17 value under -std=gnu++1z, so JSON_HAS_CPP_17 is defined, but its libstdc++ ships no <charconv> header (added in GCC 8; floating-point from_chars in GCC 11). The unconditional "#if defined(JSON_HAS_CPP_17) #include <charconv>" therefore failed to compile there: "fatal error: charconv: No such file or directory" in the ci_test_compilers_gcc (7) job. Wrap the include in __has_include(<charconv>), mirroring the library's existing handling of <version> and <filesystem> in macro_scope.hpp. When the header is absent, __cpp_lib_to_chars stays undefined and parse_float_from_chars() takes its scalar fallback, so the from_chars use site (already gated on __cpp_lib_to_chars) is never reached. GCC 8-10, which have <charconv> but no floating-point from_chars, are unaffected: they include the header but still take the fallback. GCC 11+ is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Restore the column when ungetting a newline The contiguous number fast path never reads the character that terminates a number token, while scan_number() reads it and then ungets it. When that character is a newline, get() has already cleared chars_read_current_line, and unget() could only restore lines_read - leaving the column at 0. The two paths therefore reported different columns for the same document: json::parse("[01\n]") -> line 1, column 3 json::parse(stringstream) -> line 1, column 0 Remember the column the newline was read at so unget() can restore it. Both paths now report the position the offending token actually starts at, which also fixes the pre-existing column-0 artifact for streaming input. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Extend the bulk scan fast paths to sized sentinels supports_bulk_scan required IteratorType and SentinelType to be the same type, which excluded std::counted_iterator paired with std::default_sentinel_t - the combination #5268 had already enabled for the memcpy fast path. Such input fell back to the byte-at-a-time scanner even though it is contiguous and its remaining length is computable in O(1). Factor the "distance is computable in O(1)" test into sentinel_is_sized and use it for iterator_is_contiguous, supports_seek, and supports_bulk_scan alike, and share the std::ranges::distance/std::distance dispatch through a remaining_count() helper. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Document JSON_USE_SIMDUTF on the macro overview page The macro was only listed in the API macro index; add it to the supported macros overview alongside the other JSON_USE_* macros, and note that it selects between two definitions of the same inline function and so must be defined identically in every translation unit. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Amalgamate source code Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Cover the counted-iterator bulk scan paths Sized sentinels newly reach the bulk string/number scanners and the seek-based token reconstruction, so exercise both: - diagnostics that quote the offending token, which are rebuilt from the consumed input via copy_consumed_range() - inputs whose count ends before the underlying buffer does, including a closing quote that exists only behind the count, a cut inside an 8-byte SWAR stride, and a cut inside a UTF-8 sequence Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the SentinelType example on iterator_input_adapter The comment offered "a C++20 sentinel or counted_iterator" as examples of a SentinelType, but std::counted_iterator is the IteratorType - the sentinel it pairs with is std::default_sentinel_t. #5268 corrected the same wording in the API documentation and left the code comment behind. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Do not discard the parse result in the error position check json::parse is declared warn_unused_result, and CHECK_THROWS_WITH_AS evaluates its expression as a discarded statement, so the assertion broke the -Werror builds (GCC -Werror=unused-result, MSVC C4834 under /WX). Compare against the helper that already captures the message instead. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Lock the two number grammars together with a parity test The JSON number grammar is encoded twice: as the scan_number() state machine and as the contiguous fast path. The fast path declining on anything it does not recognize keeps most divergence harmless, but if it ever accepted something the state machine rejects the result would be a silent correctness bug, and the existing test only pinned a hand-written list of numbers. Enumerate every string of length 1..4 over "01.eE+-" (2800 tokens) and require both paths to agree on the parsed value and on the exact error message. Verified to fail if the fast path's grammar is perturbed. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Skip token_buffer for integers on the contiguous path An integer token does not need token_buffer: the number_integer and number_unsigned SAX callbacks take only the value, and the overflow diagnostic rebuilds the text from the input via get_token_string(). Convert straight from the input buffer and materialize the token only for the floating-point tail, which still needs a NUL-terminated buffer for strtod. JSON_DIAGNOSTIC_POSITIONS derives a number's start position from get_string().size(), so the copy is kept when that is enabled. The integer dispatch is factored into convert_integer() and shared with convert_number(), so both scanners keep using one implementation. Integer-heavy input, 400k values, -O3: parse accept gcc 16 +14% +23% clang +15% +22% Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Amalgamate source code Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Cover the bulk string and UTF-8 scanners These paths had no dedicated tests and rested on differential fuzzing only. Add three sections, all comparing the contiguous scanner against the byte-at-a-time one on the parsed value and on the exact error message: - every string of length 1..3 over an alphabet of ordinary ASCII, both specials, a control byte, escape characters, UTF-8 lead and continuation bytes, and a byte that is never valid - each at offset 0 and offset 9, so the bulk scanner sees them with and without a run behind them - every kind of run-ending byte at each offset across two 8-byte SWAR words, so multibyte sequences also straddle the word boundary - the boundaries of every range validate_one_utf8() recognizes: shortest and longest encodings, overlongs, both ends of the surrogate block, U+10FFFF and just past it, and truncated sequences Verified to fail if the bulk validator accepts surrogates, and if the SWAR word test stops detecting control characters. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Format the new string fast path test with astyle The pinned astyle expands a braced-init-list used as a range-for range onto several lines; hoist the two offsets into a named vector instead, which reads better and leaves nothing for astyle to reformat. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix shadowed locals and guard the exception-dependent tests Two problems in the tests added for the bulk scanners, both found by CI: - the inner `const json j` in the counted-iterator diagnostics shadowed the one declared at test-case scope, which -Wshadow rejects on GCC and clang and C4456 rejects on MSVC under /WX; rename them - the new parity checks parse deliberately invalid input, which calls std::abort() rather than throwing when JSON_NOEXCEPTION is defined, so they would have crashed the no-exception build; guard them the way the other tests do json::accept() does not abort, so the UTF-8 range assertions stay compiled without exceptions and keep covering validate_one_utf8() there. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Gate the C++20 iterator classification on JSON_HAS_RANGES Making supports_bulk_scan depend on iterator_is_contiguous meant the trait is now instantiated for every adapter, not only when get_elements() is called. On standard libraries with an incomplete <ranges> that is fatal: libstdc++ 10 evaluates std::contiguous_iterator<std::counted_iterator<T*>> by calling std::to_address, which needs an operator-> its counted_iterator does not have, so satisfaction checking is a hard error rather than false. Reported by clang 14 + libstdc++ 10. JSON_HAS_RANGES already encodes exactly this ("libstdc++ < 11 has incomplete C++20 ranges", #4440), so require it for the C++20 branch. Affected toolchains fall back to the pointer-only test and the byte-at-a-time scanner, which parses identically, just without the bulk fast paths. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Amalgamate source code Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Satisfy clang-tidy in the new bulk scanner tests - give the helper lambdas an explicit std::string return type and return braced initializer lists (modernize-return-braced-init-list) - replace the C-style array of test cases with a std::vector (modernize-avoid-c-arrays) - silence pro-type-member-init on the two brace-initialized aggregates; default member initializers would stop them being aggregates in C++11 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Avoid escaped literals in the counted-iterator diagnostics list clang-tidy reads "[\"\\ud834\"]" as a literal better written raw, and the two literals written next to each other in "[\"a\x01""b\"]" as a missing comma. The concatenation was there to stop the hex escape swallowing the following character; build those documents from explicit bytes instead and use raw strings elsewhere. The byte sequences are unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Reattach convert_number's documentation @gregmarr spotted that convert_integer() was inserted between convert_number() and its doc block, leaving convert_integer() with two stacked blocks and convert_number() with none. Comment only; no code change. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address review comments on the simdutf backend string_bulk_run() had the same `return scalar_string_bulk_run(...)` in both arms of the `#if`. The simdutf arm already falls through when validation fails, so a single return after the `#endif` says the same thing. The JSON_USE_SIMDUTF example showed `#include <simdutf.h>`, which string_scan.hpp already does under the same guard; users only have to put the header on the include path and link the library, not include it themselves. Set the version history entry to 3.13.0, matching the other macro pages documenting unreleased features. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Pin the number error position against a non-newline terminator The comment claimed the reported column is the one the offending token starts at. It is the column reached after the token's last character - which is the actual point of the unget() change: a number terminated by a newline now reports what the same number terminated by a space always did. Assert that equality directly, and add a multi-character token where the start and end columns differ, so the invariant cannot be read off a single-character example. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Do not repeat the integer conversion that just failed scan_number_bulk_contiguous() converts an integer token straight from the input buffer. When the value does not fit, it materializes token_buffer and calls convert_number(), which tried the very same integer conversion again before falling back to floating point. Recording the outcome in number_type skips the second attempt. The resulting token type and value are unchanged: convert_number() reached the float tail either way. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Require a container's value_type to match what data() points at is_contiguous_byte_container accepted any type with a data() returning a pointer to a single-byte integral plus a size(). That is duck typing: the two members say nothing about size() counting the units data() points at. A type where it does not - fixed-size records, say - was routed to the pointer-based adapter and parsed as [data(), data() + size()) bytes, silently truncating input the iterator-based adapter had read in full: struct record_buffer { using value_type = std::array<char, 4>; std::string bytes; const char* data() const; // raw bytes std::size_t size() const; // in records const char* begin() const; const char* end() const; }; json::parse(record_buffer{"[1,2,3,4,5]"}); // parse error at column 3 Requiring the container's own value_type to be that same element type ties the two together. Every contiguous standard container satisfies it, so std::string, std::vector<char>, std::array<char, N> and std::string_view keep the fast path; anything else falls back to the iterator-based adapter, which is always correct. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Only compile the simdutf backend from C++17 on simdutf.h rejects anything below C++17 with an #error, so defining JSON_USE_SIMDUTF in a C++11 or C++14 translation unit did not fail with a message about simdutf being unavailable - it failed to compile at all, taking the library's C++11 support with it. Nothing caught this because no build ever compiled that path. Gate the include and both uses on JSON_HAS_CPP_17, the same way number_parse.hpp gates std::from_chars. Below C++17 the macro now has no effect and the scalar validator runs; it accepts and rejects exactly the same input, so the macro is safe to set project-wide even when some translation units use an older standard. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Test the simdutf backend in CI JSON_USE_SIMDUTF was documented and shipped but never compiled by anything in the repository, so nothing held the backend to the behavior the docs promise. Add JSON_TestSimdutf (OFF by default), which fetches simdutf and defines JSON_USE_SIMDUTF for every test target, and a ci_test_simdutf target that runs the whole suite in that configuration. Because simdutf needs C++17, the suite is built at C++11 as well, so one job covers both the scalar fallback with the macro defined and simdutf itself. The dependency hangs off test_main, whose usage requirements every test target inherits. The library target and the installed CMake package are deliberately untouched: making nlohmann_json link simdutf would put a find_dependency() in the exported package, which is a separate decision. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Warn when JSON_TestSimdutf cannot reach the simdutf backend simdutf needs C++17: without it the dependency does not even compile, and with a C++17 compiler but no C++17-or-later standard under test it builds and then goes unused. Either way the option silently did nothing useful, or broke the configure step outright. Resolve the tested standards first, then check them: when none of them can reach simdutf, skip the dependency and say so, naming which of the two reasons applies and how to fix it. The tests then run against the scalar validator, which is what would have happened anyway. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use record_buffer::data() so clang does not flag it unneeded The record_buffer test type declares data() and size() so the is_contiguous_byte_container trait can see both and still reject the type on its value_type. data() was never called, so clang's -Wunneeded-member-function (under -Weverything -Werror) failed the C++20 build. Assert that data() points at the underlying bytes: it ODR-uses the member and documents the property the type is meant to demonstrate. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAYM1qhSA2FDaDcGfPW3fG Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Skip the float fast path when it cannot succeed parse_float_fast() (Clinger) needs a significand below 2^53, so it always declines once the mantissa has 17 or more significant digits. convert_number() called it unconditionally, so those numbers were walked an extra time before strtod had to run anyway. On streaming input, where scanning is byte-at-a-time and there is no compensating win, that made canada.json about 6% slower than develop. Derive the significant-digit count from token_buffer indices - the digits are not scanned again - and skip the call when it is guaranteed to decline. Both scanners pass the offset where the mantissa ends; the count only has to be corrected for a leading "0", which the JSON grammar admits nowhere else. The integer path returns before the check, so integer-heavy input is unaffected. Values are unchanged: this only avoids an attempt that would have failed. Verified bit-exact against develop over every number in canada.json, floats.json, signed_ints.json, unsigned_ints.json, small_signed_ints.json, citm_catalog.json and twitter.json, for both the contiguous and the streaming scanner. parse, streaming develop before after canada.json 19.4ms 20.5ms 19.3ms floats.json 135.9ms 131.8ms 128.0ms parse, contiguous develop before after canada.json 15.5ms 12.9ms 11.7ms floats.json 98.6ms 69.8ms 66.7ms Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2229 lines
80 KiB
C++
2229 lines
80 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#pragma once
|
|
|
|
#include <array> // array
|
|
#include <clocale> // localeconv
|
|
#include <cstddef> // size_t
|
|
#include <cstdio> // snprintf
|
|
#include <cstdlib> // strtof, strtod, strtold, strtoll, strtoull
|
|
#include <initializer_list> // initializer_list
|
|
#include <string> // char_traits, string
|
|
#include <utility> // move
|
|
#include <vector> // vector
|
|
|
|
#include <nlohmann/detail/input/input_adapters.hpp>
|
|
#include <nlohmann/detail/input/number_parse.hpp>
|
|
#include <nlohmann/detail/input/position_t.hpp>
|
|
#include <nlohmann/detail/input/string_scan.hpp>
|
|
#include <nlohmann/detail/macro_scope.hpp>
|
|
#include <nlohmann/detail/meta/type_traits.hpp>
|
|
|
|
NLOHMANN_JSON_NAMESPACE_BEGIN
|
|
namespace detail
|
|
{
|
|
|
|
///////////
|
|
// lexer //
|
|
///////////
|
|
|
|
template<typename BasicJsonType>
|
|
class lexer_base
|
|
{
|
|
public:
|
|
/// token types for the parser
|
|
enum class token_type
|
|
{
|
|
uninitialized, ///< indicating the scanner is uninitialized
|
|
literal_true, ///< the `true` literal
|
|
literal_false, ///< the `false` literal
|
|
literal_null, ///< the `null` literal
|
|
value_string, ///< a string -- use get_string() for actual value
|
|
value_unsigned, ///< an unsigned integer -- use get_number_unsigned() for actual value
|
|
value_integer, ///< a signed integer -- use get_number_integer() for actual value
|
|
value_float, ///< an floating point number -- use get_number_float() for actual value
|
|
begin_array, ///< the character for array begin `[`
|
|
begin_object, ///< the character for object begin `{`
|
|
end_array, ///< the character for array end `]`
|
|
end_object, ///< the character for object end `}`
|
|
name_separator, ///< the name separator `:`
|
|
value_separator, ///< the value separator `,`
|
|
parse_error, ///< indicating a parse error
|
|
end_of_input, ///< indicating the end of the input buffer
|
|
literal_or_value ///< a literal or the begin of a value (only for diagnostics)
|
|
};
|
|
|
|
/// return name of values of type token_type (only used for errors)
|
|
JSON_HEDLEY_RETURNS_NON_NULL
|
|
JSON_HEDLEY_CONST
|
|
static const char* token_type_name(const token_type t) noexcept
|
|
{
|
|
switch (t)
|
|
{
|
|
case token_type::uninitialized:
|
|
return "<uninitialized>";
|
|
case token_type::literal_true:
|
|
return "true literal";
|
|
case token_type::literal_false:
|
|
return "false literal";
|
|
case token_type::literal_null:
|
|
return "null literal";
|
|
case token_type::value_string:
|
|
return "string literal";
|
|
case token_type::value_unsigned:
|
|
case token_type::value_integer:
|
|
case token_type::value_float:
|
|
return "number literal";
|
|
case token_type::begin_array:
|
|
return "'['";
|
|
case token_type::begin_object:
|
|
return "'{'";
|
|
case token_type::end_array:
|
|
return "']'";
|
|
case token_type::end_object:
|
|
return "'}'";
|
|
case token_type::name_separator:
|
|
return "':'";
|
|
case token_type::value_separator:
|
|
return "','";
|
|
case token_type::parse_error:
|
|
return "<parse error>";
|
|
case token_type::end_of_input:
|
|
return "end of input";
|
|
case token_type::literal_or_value:
|
|
return "'[', '{', or a literal";
|
|
// LCOV_EXCL_START
|
|
default: // catch non-enum values
|
|
return "unknown token";
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
};
|
|
|
|
// Detect whether an input adapter can reconstruct already-consumed input on
|
|
// demand (see iterator_input_adapter::supports_seek). Adapters that do not
|
|
// expose the flag - e.g. file, stream, wide-string, and user-defined adapters -
|
|
// are treated as non-seekable streaming input, for which the lexer keeps
|
|
// copying every scanned character eagerly. The value is read via tag dispatch
|
|
// on is_detected so the flag is only referenced for adapters that provide it.
|
|
template<typename InputAdapterType>
|
|
using detect_supports_seek = decltype(InputAdapterType::supports_seek);
|
|
|
|
template<typename InputAdapterType>
|
|
constexpr bool input_adapter_supports_seek(std::true_type /*detected*/)
|
|
{
|
|
return InputAdapterType::supports_seek;
|
|
}
|
|
|
|
template<typename InputAdapterType>
|
|
constexpr bool input_adapter_supports_seek(std::false_type /*detected*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Detect whether an input adapter exposes a contiguous byte block that the
|
|
// lexer can scan directly (see iterator_input_adapter::supports_bulk_scan).
|
|
// Adapters without the flag - file, stream, wide-string, user-defined - fall
|
|
// back to the character-at-a-time string scanner.
|
|
template<typename InputAdapterType>
|
|
using detect_supports_bulk_scan = decltype(InputAdapterType::supports_bulk_scan);
|
|
|
|
template<typename InputAdapterType>
|
|
constexpr bool input_adapter_supports_bulk_scan(std::true_type /*detected*/)
|
|
{
|
|
return InputAdapterType::supports_bulk_scan;
|
|
}
|
|
|
|
template<typename InputAdapterType>
|
|
constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
@brief lexical analysis
|
|
|
|
This class organizes the lexical analysis during JSON deserialization.
|
|
*/
|
|
template<typename BasicJsonType, typename InputAdapterType>
|
|
class lexer : public lexer_base<BasicJsonType>
|
|
{
|
|
using number_integer_t = typename BasicJsonType::number_integer_t;
|
|
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
|
|
using number_float_t = typename BasicJsonType::number_float_t;
|
|
using string_t = typename BasicJsonType::string_t;
|
|
using char_type = typename InputAdapterType::char_type;
|
|
using char_int_type = typename char_traits<char_type>::int_type;
|
|
|
|
/// whether the last read token can be reconstructed from the input adapter
|
|
/// on demand (in error paths) instead of being copied on every scanned
|
|
/// character; see input_adapter_supports_seek
|
|
static constexpr bool lazy_token_string =
|
|
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
|
|
|
|
/// whether string scanning may bulk-consume runs of ordinary characters
|
|
/// directly from a contiguous input buffer (SWAR fast path). This requires
|
|
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
|
|
/// the per-character capture in get() cannot lose error diagnostics.
|
|
static constexpr bool bulk_scan =
|
|
lazy_token_string
|
|
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
|
|
|
|
public:
|
|
using token_type = typename lexer_base<BasicJsonType>::token_type;
|
|
|
|
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false, bool discard_number_values_ = false) noexcept
|
|
: ia(std::move(adapter))
|
|
, ignore_comments(ignore_comments_)
|
|
, decimal_point_char(static_cast<char_int_type>(get_decimal_point()))
|
|
, discard_number_values(discard_number_values_)
|
|
{}
|
|
|
|
// deleted because of pointer members
|
|
lexer(const lexer&) = delete;
|
|
lexer(lexer&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor)
|
|
lexer& operator=(lexer&) = delete;
|
|
lexer& operator=(lexer&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor)
|
|
~lexer() = default;
|
|
|
|
private:
|
|
/////////////////////
|
|
// locales
|
|
/////////////////////
|
|
|
|
/// return the locale-dependent decimal point
|
|
JSON_HEDLEY_PURE
|
|
static char get_decimal_point() noexcept
|
|
{
|
|
const auto* loc = localeconv();
|
|
JSON_ASSERT(loc != nullptr);
|
|
return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point);
|
|
}
|
|
|
|
/////////////////////
|
|
// scan functions
|
|
/////////////////////
|
|
|
|
/*!
|
|
@brief get codepoint from 4 hex characters following `\u`
|
|
|
|
For input "\u c1 c2 c3 c4" the codepoint is:
|
|
(c1 * 0x1000) + (c2 * 0x0100) + (c3 * 0x0010) + c4
|
|
= (c1 << 12) + (c2 << 8) + (c3 << 4) + (c4 << 0)
|
|
|
|
Furthermore, the possible characters '0'..'9', 'A'..'F', and 'a'..'f'
|
|
must be converted to the integers 0x0..0x9, 0xA..0xF, 0xA..0xF, resp. The
|
|
conversion is done by subtracting the offset (0x30, 0x37, and 0x57)
|
|
between the ASCII value of the character and the desired integer value.
|
|
|
|
@return codepoint (0x0000..0xFFFF) or -1 in case of an error (e.g. EOF or
|
|
non-hex character)
|
|
*/
|
|
int get_codepoint()
|
|
{
|
|
// this function only makes sense after reading `\u`
|
|
JSON_ASSERT(current == 'u');
|
|
int codepoint = 0;
|
|
|
|
const auto factors = { 12u, 8u, 4u, 0u };
|
|
for (const auto factor : factors)
|
|
{
|
|
get();
|
|
|
|
if (current >= '0' && current <= '9')
|
|
{
|
|
codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x30u) << factor);
|
|
}
|
|
else if (current >= 'A' && current <= 'F')
|
|
{
|
|
codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x37u) << factor);
|
|
}
|
|
else if (current >= 'a' && current <= 'f')
|
|
{
|
|
codepoint += static_cast<int>((static_cast<unsigned int>(current) - 0x57u) << factor);
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
JSON_ASSERT(0x0000 <= codepoint && codepoint <= 0xFFFF);
|
|
return codepoint;
|
|
}
|
|
|
|
/*!
|
|
@brief check if the next byte(s) are inside a given range
|
|
|
|
Adds the current byte and, for each passed range, reads a new byte and
|
|
checks if it is inside the range. If a violation was detected, set up an
|
|
error message and return false. Otherwise, return true.
|
|
|
|
@param[in] ranges list of integers; interpreted as list of pairs of
|
|
inclusive lower and upper bound, respectively
|
|
|
|
@pre The passed list @a ranges must have 2, 4, or 6 elements; that is,
|
|
1, 2, or 3 pairs. This precondition is enforced by an assertion.
|
|
|
|
@return true if and only if no range violation was detected
|
|
*/
|
|
bool next_byte_in_range(std::initializer_list<char_int_type> ranges)
|
|
{
|
|
JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
|
|
add(current);
|
|
|
|
for (auto range = ranges.begin(); range != ranges.end(); ++range)
|
|
{
|
|
get();
|
|
if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions)
|
|
{
|
|
add(current);
|
|
}
|
|
else
|
|
{
|
|
error_message = "invalid string: ill-formed UTF-8 byte";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// contiguous input: bulk-append the run of ordinary characters and complete
|
|
/// well-formed UTF-8 sequences starting at the current read position, leaving
|
|
/// the first byte that needs individual handling (the closing quote, an
|
|
/// escape, a control character, or an ill-formed UTF-8 byte) for get()
|
|
void scan_string_bulk(std::true_type /*bulk*/)
|
|
{
|
|
// a pending unget must be consumed through the normal path first
|
|
if (next_unget)
|
|
{
|
|
return;
|
|
}
|
|
const std::size_t remaining = ia.bulk_remaining();
|
|
if (remaining == 0)
|
|
{
|
|
return;
|
|
}
|
|
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
|
|
|
const std::size_t pos = string_bulk_run(data, remaining);
|
|
if (pos == 0)
|
|
{
|
|
return;
|
|
}
|
|
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
|
ia.bulk_skip(pos);
|
|
// the run contains no newline (all bytes < 0x20 are treated as special),
|
|
// so only the flat character counters advance
|
|
position.chars_read_total += pos;
|
|
position.chars_read_current_line += pos;
|
|
}
|
|
|
|
/// streaming input: no bulk fast path
|
|
void scan_string_bulk(std::false_type /*bulk*/) const noexcept {}
|
|
|
|
/*!
|
|
@brief scan a string literal
|
|
|
|
This function scans a string according to Sect. 7 of RFC 8259. While
|
|
scanning, bytes are escaped and copied into buffer token_buffer. Then the
|
|
function returns successfully, token_buffer is *not* null-terminated (as it
|
|
may contain \0 bytes), and token_buffer.size() is the number of bytes in the
|
|
string.
|
|
|
|
@return token_type::value_string if string could be successfully scanned,
|
|
token_type::parse_error otherwise
|
|
|
|
@note In case of errors, variable error_message contains a textual
|
|
description.
|
|
*/
|
|
token_type scan_string()
|
|
{
|
|
// reset token_buffer (ignore opening quote)
|
|
reset();
|
|
|
|
// we entered the function by reading an open quote
|
|
JSON_ASSERT(current == '\"');
|
|
|
|
while (true)
|
|
{
|
|
// bulk-consume ordinary characters from contiguous input, then
|
|
// handle the next special byte through the switch below
|
|
scan_string_bulk(std::integral_constant<bool, bulk_scan> {});
|
|
|
|
// get the next character
|
|
switch (get())
|
|
{
|
|
// end of file while parsing the string
|
|
case char_traits<char_type>::eof():
|
|
{
|
|
error_message = "invalid string: missing closing quote";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// closing quote
|
|
case '\"':
|
|
{
|
|
return token_type::value_string;
|
|
}
|
|
|
|
// escapes
|
|
case '\\':
|
|
{
|
|
switch (get())
|
|
{
|
|
// quotation mark
|
|
case '\"':
|
|
add('\"');
|
|
break;
|
|
// reverse solidus
|
|
case '\\':
|
|
add('\\');
|
|
break;
|
|
// solidus
|
|
case '/':
|
|
add('/');
|
|
break;
|
|
// backspace
|
|
case 'b':
|
|
add('\b');
|
|
break;
|
|
// form feed
|
|
case 'f':
|
|
add('\f');
|
|
break;
|
|
// line feed
|
|
case 'n':
|
|
add('\n');
|
|
break;
|
|
// carriage return
|
|
case 'r':
|
|
add('\r');
|
|
break;
|
|
// tab
|
|
case 't':
|
|
add('\t');
|
|
break;
|
|
|
|
// unicode escapes
|
|
case 'u':
|
|
{
|
|
const int codepoint1 = get_codepoint();
|
|
int codepoint = codepoint1; // start with codepoint1
|
|
|
|
if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1))
|
|
{
|
|
error_message = "invalid string: '\\u' must be followed by 4 hex digits";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// check if code point is a high surrogate
|
|
if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF)
|
|
{
|
|
// expect next \uxxxx entry
|
|
if (JSON_HEDLEY_LIKELY(get() == '\\' && get() == 'u'))
|
|
{
|
|
const int codepoint2 = get_codepoint();
|
|
|
|
if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1))
|
|
{
|
|
error_message = "invalid string: '\\u' must be followed by 4 hex digits";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// check if codepoint2 is a low surrogate
|
|
if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 && codepoint2 <= 0xDFFF))
|
|
{
|
|
// overwrite codepoint
|
|
codepoint = static_cast<int>(
|
|
// high surrogate occupies the most significant 22 bits
|
|
(static_cast<unsigned int>(codepoint1) << 10u)
|
|
// low surrogate occupies the least significant 15 bits
|
|
+ static_cast<unsigned int>(codepoint2)
|
|
// there is still the 0xD800, 0xDC00, and 0x10000 noise
|
|
// in the result, so we have to subtract with:
|
|
// (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00
|
|
- 0x35FDC00u);
|
|
}
|
|
else
|
|
{
|
|
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF))
|
|
{
|
|
error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
// the result of the above calculation yields a proper codepoint
|
|
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
|
|
|
|
// translate codepoint into bytes
|
|
if (codepoint < 0x80)
|
|
{
|
|
// 1-byte characters: 0xxxxxxx (ASCII)
|
|
add(static_cast<char_int_type>(codepoint));
|
|
}
|
|
else if (codepoint <= 0x7FF)
|
|
{
|
|
// 2-byte characters: 110xxxxx 10xxxxxx
|
|
add(static_cast<char_int_type>(0xC0u | (static_cast<unsigned int>(codepoint) >> 6u)));
|
|
add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
|
|
}
|
|
else if (codepoint <= 0xFFFF)
|
|
{
|
|
// 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx
|
|
add(static_cast<char_int_type>(0xE0u | (static_cast<unsigned int>(codepoint) >> 12u)));
|
|
add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 6u) & 0x3Fu)));
|
|
add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
|
|
}
|
|
else
|
|
{
|
|
// 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
|
add(static_cast<char_int_type>(0xF0u | (static_cast<unsigned int>(codepoint) >> 18u)));
|
|
add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 12u) & 0x3Fu)));
|
|
add(static_cast<char_int_type>(0x80u | ((static_cast<unsigned int>(codepoint) >> 6u) & 0x3Fu)));
|
|
add(static_cast<char_int_type>(0x80u | (static_cast<unsigned int>(codepoint) & 0x3Fu)));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// other characters after escape
|
|
default:
|
|
error_message = "invalid string: forbidden character after backslash";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// invalid control characters
|
|
case 0x00:
|
|
{
|
|
error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x01:
|
|
{
|
|
error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x02:
|
|
{
|
|
error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x03:
|
|
{
|
|
error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x04:
|
|
{
|
|
error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x05:
|
|
{
|
|
error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x06:
|
|
{
|
|
error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x07:
|
|
{
|
|
error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x08:
|
|
{
|
|
error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x09:
|
|
{
|
|
error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0A:
|
|
{
|
|
error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0B:
|
|
{
|
|
error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0C:
|
|
{
|
|
error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0D:
|
|
{
|
|
error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0E:
|
|
{
|
|
error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x0F:
|
|
{
|
|
error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x10:
|
|
{
|
|
error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x11:
|
|
{
|
|
error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x12:
|
|
{
|
|
error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x13:
|
|
{
|
|
error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x14:
|
|
{
|
|
error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x15:
|
|
{
|
|
error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x16:
|
|
{
|
|
error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x17:
|
|
{
|
|
error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x18:
|
|
{
|
|
error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x19:
|
|
{
|
|
error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1A:
|
|
{
|
|
error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1B:
|
|
{
|
|
error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1C:
|
|
{
|
|
error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1D:
|
|
{
|
|
error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1E:
|
|
{
|
|
error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
case 0x1F:
|
|
{
|
|
error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// U+0020..U+007F (except U+0022 (quote) and U+005C (backspace))
|
|
case 0x20:
|
|
case 0x21:
|
|
case 0x23:
|
|
case 0x24:
|
|
case 0x25:
|
|
case 0x26:
|
|
case 0x27:
|
|
case 0x28:
|
|
case 0x29:
|
|
case 0x2A:
|
|
case 0x2B:
|
|
case 0x2C:
|
|
case 0x2D:
|
|
case 0x2E:
|
|
case 0x2F:
|
|
case 0x30:
|
|
case 0x31:
|
|
case 0x32:
|
|
case 0x33:
|
|
case 0x34:
|
|
case 0x35:
|
|
case 0x36:
|
|
case 0x37:
|
|
case 0x38:
|
|
case 0x39:
|
|
case 0x3A:
|
|
case 0x3B:
|
|
case 0x3C:
|
|
case 0x3D:
|
|
case 0x3E:
|
|
case 0x3F:
|
|
case 0x40:
|
|
case 0x41:
|
|
case 0x42:
|
|
case 0x43:
|
|
case 0x44:
|
|
case 0x45:
|
|
case 0x46:
|
|
case 0x47:
|
|
case 0x48:
|
|
case 0x49:
|
|
case 0x4A:
|
|
case 0x4B:
|
|
case 0x4C:
|
|
case 0x4D:
|
|
case 0x4E:
|
|
case 0x4F:
|
|
case 0x50:
|
|
case 0x51:
|
|
case 0x52:
|
|
case 0x53:
|
|
case 0x54:
|
|
case 0x55:
|
|
case 0x56:
|
|
case 0x57:
|
|
case 0x58:
|
|
case 0x59:
|
|
case 0x5A:
|
|
case 0x5B:
|
|
case 0x5D:
|
|
case 0x5E:
|
|
case 0x5F:
|
|
case 0x60:
|
|
case 0x61:
|
|
case 0x62:
|
|
case 0x63:
|
|
case 0x64:
|
|
case 0x65:
|
|
case 0x66:
|
|
case 0x67:
|
|
case 0x68:
|
|
case 0x69:
|
|
case 0x6A:
|
|
case 0x6B:
|
|
case 0x6C:
|
|
case 0x6D:
|
|
case 0x6E:
|
|
case 0x6F:
|
|
case 0x70:
|
|
case 0x71:
|
|
case 0x72:
|
|
case 0x73:
|
|
case 0x74:
|
|
case 0x75:
|
|
case 0x76:
|
|
case 0x77:
|
|
case 0x78:
|
|
case 0x79:
|
|
case 0x7A:
|
|
case 0x7B:
|
|
case 0x7C:
|
|
case 0x7D:
|
|
case 0x7E:
|
|
case 0x7F:
|
|
{
|
|
add(current);
|
|
break;
|
|
}
|
|
|
|
// U+0080..U+07FF: bytes C2..DF 80..BF
|
|
case 0xC2:
|
|
case 0xC3:
|
|
case 0xC4:
|
|
case 0xC5:
|
|
case 0xC6:
|
|
case 0xC7:
|
|
case 0xC8:
|
|
case 0xC9:
|
|
case 0xCA:
|
|
case 0xCB:
|
|
case 0xCC:
|
|
case 0xCD:
|
|
case 0xCE:
|
|
case 0xCF:
|
|
case 0xD0:
|
|
case 0xD1:
|
|
case 0xD2:
|
|
case 0xD3:
|
|
case 0xD4:
|
|
case 0xD5:
|
|
case 0xD6:
|
|
case 0xD7:
|
|
case 0xD8:
|
|
case 0xD9:
|
|
case 0xDA:
|
|
case 0xDB:
|
|
case 0xDC:
|
|
case 0xDD:
|
|
case 0xDE:
|
|
case 0xDF:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF})))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+0800..U+0FFF: bytes E0 A0..BF 80..BF
|
|
case 0xE0:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+1000..U+CFFF: bytes E1..EC 80..BF 80..BF
|
|
// U+E000..U+FFFF: bytes EE..EF 80..BF 80..BF
|
|
case 0xE1:
|
|
case 0xE2:
|
|
case 0xE3:
|
|
case 0xE4:
|
|
case 0xE5:
|
|
case 0xE6:
|
|
case 0xE7:
|
|
case 0xE8:
|
|
case 0xE9:
|
|
case 0xEA:
|
|
case 0xEB:
|
|
case 0xEC:
|
|
case 0xEE:
|
|
case 0xEF:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+D000..U+D7FF: bytes ED 80..9F 80..BF
|
|
case 0xED:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
|
|
case 0xF0:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
|
|
case 0xF1:
|
|
case 0xF2:
|
|
case 0xF3:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
|
|
case 0xF4:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// the remaining bytes (80..C1 and F5..FF) are ill-formed
|
|
default:
|
|
{
|
|
error_message = "invalid string: ill-formed UTF-8 byte";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* @brief scan a comment
|
|
* @return whether comment could be scanned successfully
|
|
*/
|
|
bool scan_comment()
|
|
{
|
|
switch (get())
|
|
{
|
|
// single-line comments skip input until a newline or EOF is read
|
|
case '/':
|
|
{
|
|
while (true)
|
|
{
|
|
switch (get())
|
|
{
|
|
case '\n':
|
|
case '\r':
|
|
case char_traits<char_type>::eof():
|
|
case '\0':
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
JSON_HEDLEY_UNREACHABLE();
|
|
}
|
|
|
|
// multi-line comments skip input until */ is read
|
|
case '*':
|
|
{
|
|
while (true)
|
|
{
|
|
switch (get())
|
|
{
|
|
case char_traits<char_type>::eof():
|
|
case '\0':
|
|
{
|
|
error_message = "invalid comment; missing closing '*/'";
|
|
return false;
|
|
}
|
|
|
|
case '*':
|
|
{
|
|
switch (get())
|
|
{
|
|
case '/':
|
|
return true;
|
|
|
|
default:
|
|
{
|
|
unget();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
|
|
JSON_HEDLEY_UNREACHABLE();
|
|
}
|
|
|
|
// unexpected character after reading '/'
|
|
default:
|
|
{
|
|
error_message = "invalid comment; expecting '/' or '*' after '/'";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
JSON_HEDLEY_NON_NULL(2)
|
|
static void strtof(float& f, const char* str, char** endptr) noexcept
|
|
{
|
|
f = std::strtof(str, endptr);
|
|
}
|
|
|
|
JSON_HEDLEY_NON_NULL(2)
|
|
static void strtof(double& f, const char* str, char** endptr) noexcept
|
|
{
|
|
f = std::strtod(str, endptr);
|
|
}
|
|
|
|
JSON_HEDLEY_NON_NULL(2)
|
|
static void strtof(long double& f, const char* str, char** endptr) noexcept
|
|
{
|
|
f = std::strtold(str, endptr);
|
|
}
|
|
|
|
/*!
|
|
@brief scan a number literal
|
|
|
|
This function scans a string according to Sect. 6 of RFC 8259.
|
|
|
|
The function is realized with a deterministic finite state machine derived
|
|
from the grammar described in RFC 8259. Starting in state "init", the
|
|
input is read and used to determined the next state. Only state "done"
|
|
accepts the number. State "error" is a trap state to model errors. In the
|
|
table below, "anything" means any character but the ones listed before.
|
|
|
|
state | 0 | 1-9 | e E | + | - | . | anything
|
|
---------|----------|----------|----------|---------|---------|----------|-----------
|
|
init | zero | any1 | [error] | [error] | minus | [error] | [error]
|
|
minus | zero | any1 | [error] | [error] | [error] | [error] | [error]
|
|
zero | done | done | exponent | done | done | decimal1 | done
|
|
any1 | any1 | any1 | exponent | done | done | decimal1 | done
|
|
decimal1 | decimal2 | decimal2 | [error] | [error] | [error] | [error] | [error]
|
|
decimal2 | decimal2 | decimal2 | exponent | done | done | done | done
|
|
exponent | any2 | any2 | [error] | sign | sign | [error] | [error]
|
|
sign | any2 | any2 | [error] | [error] | [error] | [error] | [error]
|
|
any2 | any2 | any2 | done | done | done | done | done
|
|
|
|
The state machine is realized with one label per state (prefixed with
|
|
"scan_number_") and `goto` statements between them. The state machine
|
|
contains cycles, but any cycle can be left when EOF is read. Therefore,
|
|
the function is guaranteed to terminate.
|
|
|
|
During scanning, the read bytes are stored in token_buffer. This string is
|
|
then converted to a signed integer, an unsigned integer, or a
|
|
floating-point number.
|
|
|
|
@return token_type::value_unsigned, token_type::value_integer, or
|
|
token_type::value_float if number could be successfully scanned,
|
|
token_type::parse_error otherwise
|
|
|
|
@note The scanner is independent of the current locale. Internally, the
|
|
locale's decimal point is used instead of `.` to work with the
|
|
locale-dependent converters.
|
|
*/
|
|
token_type scan_number() // lgtm [cpp/use-of-goto] `goto` is used in this function to implement the number-parsing state machine described above. By design, any finite input will eventually reach the "done" state or return token_type::parse_error. In each intermediate state, 1 byte of the input is appended to the token_buffer vector, and only the already initialized variables token_buffer, number_type, and error_message are manipulated.
|
|
{
|
|
// reset token_buffer to store the number's bytes
|
|
reset();
|
|
|
|
// the type of the parsed number; initially set to unsigned; will be
|
|
// changed if minus sign, decimal point, or exponent is read
|
|
token_type number_type = token_type::value_unsigned;
|
|
|
|
// offset just past the last mantissa byte in token_buffer (i.e. the
|
|
// index of 'e'/'E', or the whole token when there is no exponent).
|
|
// convert_number() uses it to count significant digits; npos means
|
|
// "not seen an exponent yet" and is resolved at scan_number_done
|
|
std::size_t mantissa_end = std::string::npos;
|
|
|
|
// state (init): we just found out we need to scan a number
|
|
switch (current)
|
|
{
|
|
case '-':
|
|
{
|
|
add(current);
|
|
goto scan_number_minus;
|
|
}
|
|
|
|
case '0':
|
|
{
|
|
add(current);
|
|
goto scan_number_zero;
|
|
}
|
|
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any1;
|
|
}
|
|
|
|
// all other characters are rejected outside scan_number()
|
|
default: // LCOV_EXCL_LINE
|
|
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
|
}
|
|
|
|
scan_number_minus:
|
|
// state: we just parsed a leading minus sign
|
|
number_type = token_type::value_integer;
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
{
|
|
add(current);
|
|
goto scan_number_zero;
|
|
}
|
|
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any1;
|
|
}
|
|
|
|
default:
|
|
{
|
|
error_message = "invalid number; expected digit after '-'";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
scan_number_zero:
|
|
// state: we just parse a zero (maybe with a leading minus sign)
|
|
switch (get())
|
|
{
|
|
case '.':
|
|
{
|
|
add(decimal_point_char);
|
|
decimal_point_position = token_buffer.size() - 1;
|
|
goto scan_number_decimal1;
|
|
}
|
|
|
|
case 'e':
|
|
case 'E':
|
|
{
|
|
add(current);
|
|
goto scan_number_exponent;
|
|
}
|
|
|
|
default:
|
|
goto scan_number_done;
|
|
}
|
|
|
|
scan_number_any1:
|
|
// state: we just parsed a number 0-9 (maybe with a leading minus sign)
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any1;
|
|
}
|
|
|
|
case '.':
|
|
{
|
|
add(decimal_point_char);
|
|
decimal_point_position = token_buffer.size() - 1;
|
|
goto scan_number_decimal1;
|
|
}
|
|
|
|
case 'e':
|
|
case 'E':
|
|
{
|
|
add(current);
|
|
goto scan_number_exponent;
|
|
}
|
|
|
|
default:
|
|
goto scan_number_done;
|
|
}
|
|
|
|
scan_number_decimal1:
|
|
// state: we just parsed a decimal point
|
|
number_type = token_type::value_float;
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_decimal2;
|
|
}
|
|
|
|
default:
|
|
{
|
|
error_message = "invalid number; expected digit after '.'";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
scan_number_decimal2:
|
|
// we just parsed at least one number after a decimal point
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_decimal2;
|
|
}
|
|
|
|
case 'e':
|
|
case 'E':
|
|
{
|
|
add(current);
|
|
goto scan_number_exponent;
|
|
}
|
|
|
|
default:
|
|
goto scan_number_done;
|
|
}
|
|
|
|
scan_number_exponent:
|
|
// we just parsed an exponent
|
|
number_type = token_type::value_float;
|
|
// this label is reached only right after the 'e'/'E' was appended (from
|
|
// the zero, any1, and decimal2 states), so the mantissa ends before it
|
|
mantissa_end = token_buffer.size() - 1;
|
|
switch (get())
|
|
{
|
|
case '+':
|
|
case '-':
|
|
{
|
|
add(current);
|
|
goto scan_number_sign;
|
|
}
|
|
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any2;
|
|
}
|
|
|
|
default:
|
|
{
|
|
error_message =
|
|
"invalid number; expected '+', '-', or digit after exponent";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
scan_number_sign:
|
|
// we just parsed an exponent sign
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any2;
|
|
}
|
|
|
|
default:
|
|
{
|
|
error_message = "invalid number; expected digit after exponent sign";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
scan_number_any2:
|
|
// we just parsed a number after the exponent or exponent sign
|
|
switch (get())
|
|
{
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
{
|
|
add(current);
|
|
goto scan_number_any2;
|
|
}
|
|
|
|
default:
|
|
goto scan_number_done;
|
|
}
|
|
|
|
scan_number_done:
|
|
// unget the character after the number (we only read it to know that
|
|
// we are done scanning a number)
|
|
unget();
|
|
|
|
// no exponent was scanned: the mantissa spans the whole token
|
|
if (mantissa_end == std::string::npos)
|
|
{
|
|
mantissa_end = token_buffer.size();
|
|
}
|
|
|
|
return convert_number(number_type, mantissa_end);
|
|
}
|
|
|
|
/*!
|
|
@brief convert an already-validated integer token to its value
|
|
|
|
The digit sequence in [first, last) has been validated by the caller, so a
|
|
dedicated parser can avoid the locale/errno overhead of std::strtoull.
|
|
|
|
@return the token type on success; token_type::uninitialized if @a
|
|
number_type is not an integer type or the value does not fit, in
|
|
which case the caller falls back to the floating-point conversion
|
|
(matching the previous std::strtoull/std::strtoll behavior)
|
|
*/
|
|
token_type convert_integer(token_type number_type, const char* first, const char* last)
|
|
{
|
|
if (number_type == token_type::value_unsigned)
|
|
{
|
|
if (parse_integer_unsigned(first, last, value_unsigned))
|
|
{
|
|
return token_type::value_unsigned;
|
|
}
|
|
}
|
|
else if (number_type == token_type::value_integer)
|
|
{
|
|
if (parse_integer_signed(first, last, value_integer))
|
|
{
|
|
return token_type::value_integer;
|
|
}
|
|
}
|
|
|
|
return token_type::uninitialized;
|
|
}
|
|
|
|
/*!
|
|
@brief check whether Clinger's fast path can still succeed for this token
|
|
|
|
parse_float_fast() needs a significand below 2^53. A mantissa with 17 or
|
|
more significant digits is at least 10^16 and therefore always exceeds it,
|
|
so calling the fast path would walk the token one extra time only to
|
|
decline before strtod has to run anyway.
|
|
|
|
Significant digits are the mantissa's digits from the first nonzero one on;
|
|
the sign, the decimal point, leading zeros, and the exponent do not count.
|
|
The answer is derived from indices - the digits are not scanned again - so
|
|
this stays off the hot path of the number scanners.
|
|
|
|
@param[in] mantissa_end offset just past the last mantissa byte in
|
|
token_buffer
|
|
@return false if parse_float_fast() is guaranteed to decline
|
|
*/
|
|
bool mantissa_fits_clinger(std::size_t mantissa_end) const
|
|
{
|
|
// 10^16 already exceeds 2^53, so 17 digits can never fit
|
|
constexpr std::size_t limit = 17;
|
|
|
|
const std::size_t neg = (!token_buffer.empty() && token_buffer[0] == '-') ? 1u : 0u;
|
|
const std::size_t has_dot = (decimal_point_position != std::string::npos) ? 1u : 0u;
|
|
// the JSON grammar restricts the integer part to "0" or [1-9][0-9]*, so
|
|
// a leading zero can only be a lone "0", which is not significant
|
|
const std::size_t lead_zero = (token_buffer[neg] == '0') ? 1u : 0u;
|
|
JSON_ASSERT(mantissa_end >= neg + has_dot + lead_zero);
|
|
std::size_t digits = mantissa_end - neg - has_dot - lead_zero;
|
|
|
|
if (JSON_HEDLEY_LIKELY(digits < limit))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Only a number below 1 can carry further insignificant zeros, and only
|
|
// while the count stays at the limit does removing them change the
|
|
// answer - so this loop is skipped for all but a few tokens. Note
|
|
// token_buffer holds the locale's decimal point, so the fraction is
|
|
// located through decimal_point_position rather than by searching '.'.
|
|
if (lead_zero != 0)
|
|
{
|
|
JSON_ASSERT(has_dot != 0); // an integer "0" cannot reach the limit
|
|
for (std::size_t i = decimal_point_position + 1;
|
|
digits >= limit && i < mantissa_end && token_buffer[i] == '0'; ++i)
|
|
{
|
|
--digits;
|
|
}
|
|
}
|
|
|
|
return digits < limit;
|
|
}
|
|
|
|
/*!
|
|
@brief convert the number text in token_buffer to its value and token type
|
|
|
|
The digit sequence in token_buffer has already been validated (by the
|
|
scan_number() state machine or by the contiguous fast path) and holds the
|
|
locale decimal point in place of '.'. Integers are parsed first and fall
|
|
back to floating point on overflow. This is shared so both scanners produce
|
|
identical results.
|
|
|
|
@param[in] mantissa_end offset just past the last mantissa byte in
|
|
token_buffer (the index of 'e'/'E', or
|
|
token_buffer.size() when there is no exponent);
|
|
used to skip Clinger's fast path when it cannot
|
|
possibly succeed - see mantissa_fits_clinger()
|
|
*/
|
|
token_type convert_number(token_type number_type, std::size_t mantissa_end)
|
|
{
|
|
// If the caller does not need the converted value (only whether the
|
|
// input is syntactically valid; see json_sax_acceptor/accept()), an
|
|
// unsigned/integer token can be reported without calling
|
|
// strtoull()/strtoll() at all, *provided* we can already tell from
|
|
// the digit count alone that the conversion cannot overflow 64 bits.
|
|
// Such tokens are always finite and are accepted unconditionally by
|
|
// the parser regardless of their actual value (parser::sax_parse_internal()
|
|
// never checks finiteness for value_unsigned/value_integer), so the
|
|
// classification below is all that is needed.
|
|
//
|
|
// A decimal number with up to 18 digits is always representable in
|
|
// both std::uint64_t and std::int64_t (18 nines is ~1e18, well below
|
|
// both UINT64_MAX ~1.8e19 and INT64_MAX ~9.2e18), so strtoull()/strtoll()
|
|
// could not have set errno to ERANGE for it. Numbers with more digits
|
|
// (rare in practice) fall through to the exact code below, unchanged,
|
|
// so their handling -- including reclassification to value_float when
|
|
// the value overflows 64 bits, and rejection when it is not even
|
|
// finite as a double -- is bit-for-bit identical to before this
|
|
// optimization.
|
|
//
|
|
// Note this reasons about std::uint64_t/std::int64_t, not about
|
|
// number_unsigned_t/number_integer_t (BasicJsonType's own, possibly
|
|
// narrower, template parameters -- e.g. std::uint32_t). That is fine
|
|
// *only* because discard_number_values is exclusively set by
|
|
// accept() (see json.hpp), and accept() always parses through the
|
|
// library's own json_sax_acceptor -- never a user-supplied SAX
|
|
// consumer -- whose number_unsigned()/number_integer()/number_float()
|
|
// callbacks unconditionally discard their argument and return true.
|
|
// So for every caller that can reach this branch, neither the token
|
|
// classification below nor the eventual (possibly narrowed, and on
|
|
// this fast path left stale/unset) value_unsigned/value_integer is
|
|
// ever consulted -- an unsigned/integer token is accepted outright,
|
|
// and even a >18-digit token that this fast path deliberately falls
|
|
// through for is, once reclassified to value_float, still finite
|
|
// (and thus accepted) for any digit count that fits in number_unsigned_t
|
|
// or number_integer_t regardless of that type's width. If this
|
|
// function is ever taught to run with discard_number_values true for
|
|
// a caller that *does* read the converted value, this reasoning (and
|
|
// the fast path below) would need to be revisited.
|
|
if (discard_number_values)
|
|
{
|
|
constexpr std::size_t safe_digit_count = 18;
|
|
if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count)
|
|
{
|
|
return token_type::value_unsigned;
|
|
}
|
|
if (number_type == token_type::value_integer && token_buffer.size() - 1 <= safe_digit_count)
|
|
{
|
|
return token_type::value_integer;
|
|
}
|
|
}
|
|
|
|
const char* const num_begin = token_buffer.data();
|
|
const char* const num_end = num_begin + token_buffer.size();
|
|
|
|
if (number_type != token_type::value_float)
|
|
{
|
|
const token_type integer_result = convert_integer(number_type, num_begin, num_end);
|
|
if (integer_result != token_type::uninitialized)
|
|
{
|
|
return integer_result;
|
|
}
|
|
}
|
|
|
|
// this code is reached if we parse a floating-point number or if an
|
|
// integer conversion above overflowed. Prefer std::from_chars
|
|
// (Eisel-Lemire, locale-independent, correctly rounded) when available;
|
|
// otherwise the exact Clinger fast path (double only); otherwise the
|
|
// locale-aware strtof/strtod.
|
|
if (parse_float_from_chars(num_begin, num_end, value_float))
|
|
{
|
|
return token_type::value_float;
|
|
}
|
|
// Skipping a fast path that cannot succeed is lossless and saves a full
|
|
// extra pass over the token's bytes, which otherwise shows up on
|
|
// high-precision inputs such as canada.json
|
|
if (mantissa_fits_clinger(mantissa_end)
|
|
&& parse_float_fast(num_begin, num_end, decimal_point_char, value_float))
|
|
{
|
|
return token_type::value_float;
|
|
}
|
|
|
|
char* endptr = nullptr; // NOLINT(misc-const-correctness,cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
strtof(value_float, token_buffer.data(), &endptr);
|
|
|
|
// we checked the number format before
|
|
JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size());
|
|
|
|
return token_type::value_float;
|
|
}
|
|
|
|
/*!
|
|
@brief contiguous fast path for scanning a number
|
|
|
|
Parses the whole number token straight from the input buffer, avoiding the
|
|
per-character get()/add() of scan_number(). On success it fills token_buffer
|
|
(with the locale decimal point substituted, as scan_number() does) and
|
|
returns the token type. On anything it does not fully recognize as a
|
|
well-formed number it makes no state change and returns
|
|
token_type::uninitialized, so the caller falls back to scan_number(), which
|
|
then produces the exact diagnostic. @a current is the first digit or the
|
|
leading minus (already read); the remaining bytes are taken from the adapter.
|
|
*/
|
|
token_type scan_number_bulk_contiguous()
|
|
{
|
|
// a pending unget offsets the buffer position from current; fall back
|
|
if (next_unget)
|
|
{
|
|
return token_type::uninitialized;
|
|
}
|
|
const std::size_t rem = ia.bulk_remaining();
|
|
if (rem == 0)
|
|
{
|
|
// the first digit is the last input byte; let scan_number() finish
|
|
return token_type::uninitialized;
|
|
}
|
|
// the byte before the next unread one is current (contiguous input)
|
|
const char* const data = reinterpret_cast<const char*>(ia.bulk_data()) - 1;
|
|
const std::size_t avail = rem + 1;
|
|
|
|
// validate + classify the number extent (mirrors scan_number()'s grammar)
|
|
std::size_t i = 0;
|
|
std::size_t dot_index = std::string::npos;
|
|
token_type number_type = token_type::value_unsigned;
|
|
if (data[0] == '-')
|
|
{
|
|
number_type = token_type::value_integer;
|
|
i = 1;
|
|
if (i >= avail)
|
|
{
|
|
return token_type::uninitialized;
|
|
}
|
|
}
|
|
if (data[i] == '0')
|
|
{
|
|
++i;
|
|
}
|
|
else if (data[i] >= '1' && data[i] <= '9')
|
|
{
|
|
++i;
|
|
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return token_type::uninitialized;
|
|
}
|
|
if (i < avail && data[i] == '.')
|
|
{
|
|
number_type = token_type::value_float;
|
|
dot_index = i;
|
|
++i;
|
|
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
|
{
|
|
return token_type::uninitialized;
|
|
}
|
|
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
// the mantissa ends here, whether or not an exponent part follows
|
|
const std::size_t mantissa_end = i;
|
|
if (i < avail && (data[i] == 'e' || data[i] == 'E'))
|
|
{
|
|
number_type = token_type::value_float;
|
|
++i;
|
|
if (i < avail && (data[i] == '+' || data[i] == '-'))
|
|
{
|
|
++i;
|
|
}
|
|
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
|
{
|
|
return token_type::uninitialized;
|
|
}
|
|
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
const std::size_t len = i;
|
|
|
|
// reset() records where this token starts (for diagnostics), so it has
|
|
// to run before the input position advances below
|
|
reset();
|
|
|
|
// An integer token needs no token_buffer: the SAX callbacks for
|
|
// number_integer/number_unsigned take only the value, and the overflow
|
|
// diagnostic rebuilds the text from the input. Convert straight from the
|
|
// input buffer and leave token_buffer empty. (JSON_DIAGNOSTIC_POSITIONS
|
|
// derives a number's start position from get_string().size(), so there
|
|
// the token still has to be materialized.)
|
|
#if !JSON_DIAGNOSTIC_POSITIONS
|
|
if (number_type != token_type::value_float)
|
|
{
|
|
const token_type integer_result = convert_integer(number_type, data, data + len);
|
|
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
|
|
{
|
|
ia.bulk_skip(len - 1);
|
|
position.chars_read_total += (len - 1);
|
|
position.chars_read_current_line += (len - 1);
|
|
return integer_result;
|
|
}
|
|
// The value does not fit an integer, so this token converts as a
|
|
// float. Recording that here keeps convert_number() below from
|
|
// repeating the integer attempt that just failed.
|
|
number_type = token_type::value_float;
|
|
}
|
|
#endif
|
|
|
|
// materialize the token exactly as scan_number() would, substituting the
|
|
// locale decimal point so convert_number()'s strtof fallback stays valid.
|
|
// reset() already cleared token_buffer, so append() fills it (assign() is
|
|
// avoided because custom string_t types need not provide it)
|
|
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
|
if (dot_index != std::string::npos)
|
|
{
|
|
token_buffer[dot_index] = static_cast<typename string_t::value_type>(decimal_point_char);
|
|
decimal_point_position = dot_index;
|
|
}
|
|
|
|
ia.bulk_skip(len - 1);
|
|
position.chars_read_total += (len - 1);
|
|
position.chars_read_current_line += (len - 1);
|
|
|
|
return convert_number(number_type, mantissa_end);
|
|
}
|
|
|
|
/// contiguous input: try the number fast path, else the byte-path scanner
|
|
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
|
{
|
|
const token_type t = scan_number_bulk_contiguous();
|
|
return (t != token_type::uninitialized) ? t : scan_number();
|
|
}
|
|
|
|
/// streaming input: always use the byte-path scanner
|
|
token_type scan_number_dispatch(std::false_type /*bulk*/)
|
|
{
|
|
return scan_number();
|
|
}
|
|
|
|
/*!
|
|
@param[in] literal_text the literal text to expect
|
|
@param[in] length the length of the passed literal text
|
|
@param[in] return_type the token type to return on success
|
|
*/
|
|
JSON_HEDLEY_NON_NULL(2)
|
|
token_type scan_literal(const char_type* literal_text, const std::size_t length,
|
|
token_type return_type)
|
|
{
|
|
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]);
|
|
for (std::size_t i = 1; i < length; ++i)
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != literal_text[i]))
|
|
{
|
|
error_message = "invalid literal";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
return return_type;
|
|
}
|
|
|
|
/////////////////////
|
|
// input management
|
|
/////////////////////
|
|
|
|
/// reset token_buffer; current character is beginning of token
|
|
void reset() noexcept
|
|
{
|
|
token_buffer.clear();
|
|
decimal_point_position = std::string::npos;
|
|
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
// the first character of the token has already been read, hence the -1
|
|
token_start_position = position.chars_read_total - 1;
|
|
#endif
|
|
|
|
note_token_start(std::integral_constant<bool, lazy_token_string> {});
|
|
}
|
|
|
|
/// seekable adapter: remember where the current token starts so it can be
|
|
/// reconstructed from the input on error; current has already been
|
|
/// consumed, hence the -1
|
|
void note_token_start(std::true_type /*lazy*/) noexcept
|
|
{
|
|
token_string_start = ia.get_consumed_count() - 1;
|
|
}
|
|
|
|
/// streaming adapter: start copying the token eagerly, beginning with the
|
|
/// already-read first character
|
|
void note_token_start(std::false_type /*lazy*/) noexcept
|
|
{
|
|
token_string.clear();
|
|
token_string.push_back(char_traits<char_type>::to_char_type(current));
|
|
}
|
|
|
|
/*
|
|
@brief get next character from the input
|
|
|
|
This function provides the interface to the used input adapter. It does
|
|
not throw in case the input reached EOF, but returns a
|
|
`char_traits<char>::eof()` in that case. Stores the scanned characters
|
|
for use in error messages.
|
|
|
|
@return character read from the input
|
|
*/
|
|
char_int_type get()
|
|
{
|
|
advance_position();
|
|
|
|
if (next_unget)
|
|
{
|
|
// only reset the next_unget variable and work with current
|
|
next_unget = false;
|
|
}
|
|
else
|
|
{
|
|
current = ia.get_character();
|
|
}
|
|
|
|
return track_after_read();
|
|
}
|
|
|
|
/// shared head of get() / get_ignoring_pending_unget(): bump the
|
|
/// per-character position counters (line-count-on-'\n' bookkeeping is
|
|
/// handled afterwards, in track_after_read(), once `current` is known)
|
|
void advance_position() noexcept
|
|
{
|
|
++position.chars_read_total;
|
|
++position.chars_read_current_line;
|
|
}
|
|
|
|
/// shared tail of get() / get_ignoring_pending_unget(): capture the
|
|
/// character for error messages (if needed) and update line/column
|
|
/// bookkeeping for the character now in `current`
|
|
char_int_type track_after_read()
|
|
{
|
|
// seekable adapters reconstruct the token lazily on error (see
|
|
// get_token_string), so the eager per-character copy is skipped
|
|
capture_char(std::integral_constant<bool, lazy_token_string> {});
|
|
|
|
if (current == '\n')
|
|
{
|
|
++position.lines_read;
|
|
// remember the column the newline was read at: chars_read_current_line
|
|
// is about to be cleared, and a matching unget() cannot reconstruct it
|
|
chars_read_before_newline = position.chars_read_current_line;
|
|
position.chars_read_current_line = 0;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/*!
|
|
@brief like get(), but for call sites that can prove no unget() is pending
|
|
|
|
get() has to check the `next_unget` flag on every call, because a
|
|
previous token may have ended with unget() (e.g. scan_number() always
|
|
ungets the character that terminated the number, so the next call to
|
|
scan() can see it again). skip_whitespace() reads that first,
|
|
possibly-ungotten character via a plain get(), but every further
|
|
character it reads is guaranteed to be a fresh read: nothing between
|
|
those calls invokes unget(). This variant skips the (otherwise always
|
|
false) next_unget branch for those calls; it is not a general
|
|
replacement for get().
|
|
*/
|
|
char_int_type get_ignoring_pending_unget()
|
|
{
|
|
JSON_ASSERT(!next_unget);
|
|
|
|
advance_position();
|
|
current = ia.get_character();
|
|
|
|
return track_after_read();
|
|
}
|
|
|
|
/// seekable adapter: nothing to capture, the token is rebuilt on error
|
|
void capture_char(std::true_type /*lazy*/) const noexcept {}
|
|
|
|
/// streaming adapter: copy the scanned character into token_string
|
|
void capture_char(std::false_type /*lazy*/)
|
|
{
|
|
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
|
|
{
|
|
token_string.push_back(char_traits<char_type>::to_char_type(current));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
@brief unget current character (read it again on next get)
|
|
|
|
We implement unget by setting variable next_unget to true. The input is not
|
|
changed - we just simulate ungetting by modifying chars_read_total,
|
|
chars_read_current_line, and token_string. The next call to get() will
|
|
behave as if the unget character is read again.
|
|
*/
|
|
void unget()
|
|
{
|
|
next_unget = true;
|
|
|
|
--position.chars_read_total;
|
|
|
|
// in case we "unget" a newline, we have to also decrement the lines_read
|
|
// and restore the column that get() cleared when it saw the newline;
|
|
// chars_read_current_line == 0 can only mean the last get() read one
|
|
if (position.chars_read_current_line == 0)
|
|
{
|
|
if (position.lines_read > 0)
|
|
{
|
|
--position.lines_read;
|
|
}
|
|
|
|
// chars_read_before_newline counts the newline itself, which is the
|
|
// character being ungotten, hence the -1
|
|
position.chars_read_current_line = (chars_read_before_newline > 0)
|
|
? chars_read_before_newline - 1
|
|
: 0;
|
|
}
|
|
else
|
|
{
|
|
--position.chars_read_current_line;
|
|
}
|
|
|
|
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
|
|
}
|
|
|
|
/// seekable adapter: nothing was captured, so nothing to undo
|
|
void uncapture_char(std::true_type /*lazy*/) const noexcept {}
|
|
|
|
/// streaming adapter: drop the character copied by the matching get()
|
|
void uncapture_char(std::false_type /*lazy*/)
|
|
{
|
|
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
|
|
{
|
|
JSON_ASSERT(!token_string.empty());
|
|
token_string.pop_back();
|
|
}
|
|
}
|
|
|
|
/// add a character to token_buffer
|
|
void add(char_int_type c)
|
|
{
|
|
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
|
|
}
|
|
|
|
public:
|
|
/////////////////////
|
|
// value getters
|
|
/////////////////////
|
|
|
|
/// return integer value
|
|
constexpr number_integer_t get_number_integer() const noexcept
|
|
{
|
|
return value_integer;
|
|
}
|
|
|
|
/// return unsigned integer value
|
|
constexpr number_unsigned_t get_number_unsigned() const noexcept
|
|
{
|
|
return value_unsigned;
|
|
}
|
|
|
|
/// return floating-point value
|
|
constexpr number_float_t get_number_float() const noexcept
|
|
{
|
|
return value_float;
|
|
}
|
|
|
|
/// return current string value (implicitly resets the token; useful only once)
|
|
string_t& get_string()
|
|
{
|
|
// translate decimal points from locale back to '.' (#4084)
|
|
if (decimal_point_char != '.' && decimal_point_position != std::string::npos)
|
|
{
|
|
token_buffer[decimal_point_position] = '.';
|
|
}
|
|
return token_buffer;
|
|
}
|
|
|
|
/////////////////////
|
|
// diagnostics
|
|
/////////////////////
|
|
|
|
/// return position of last read token
|
|
constexpr position_t get_position() const noexcept
|
|
{
|
|
return position;
|
|
}
|
|
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
/// return the offset of the first character of the last read token; unlike
|
|
/// the token's parsed value, this accounts for escape sequences
|
|
constexpr std::size_t get_token_start_position() const noexcept
|
|
{
|
|
return token_start_position;
|
|
}
|
|
#endif
|
|
|
|
/// seekable adapter: rebuild the last read token from the input on demand
|
|
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
|
|
{
|
|
// a pending unget of a real (non-EOF) character means that character
|
|
// was consumed from the input but is not part of the token; EOF is
|
|
// never consumed, so it must not be subtracted (mirrors unget())
|
|
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
|
|
const std::size_t stop = ia.get_consumed_count() - (pending_real_unget ? 1u : 0u);
|
|
if (JSON_HEDLEY_LIKELY(stop >= token_string_start))
|
|
{
|
|
ia.copy_consumed_range(token_string_start, stop, out);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// streaming adapter: the token was copied eagerly while scanning
|
|
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& /*out*/, std::false_type /*lazy*/) const
|
|
{
|
|
return token_string;
|
|
}
|
|
|
|
/// return the last read token (for errors only). Will never contain EOF
|
|
/// (an arbitrary value that is not a valid char value, often -1), because
|
|
/// 255 may legitimately occur. May contain NUL, which should be escaped.
|
|
std::string get_token_string() const
|
|
{
|
|
std::vector<char_type> reconstructed;
|
|
const std::vector<char_type>& chars = collect_token_chars(reconstructed, std::integral_constant<bool, lazy_token_string> {});
|
|
|
|
// escape control characters
|
|
std::string result;
|
|
for (const auto c : chars)
|
|
{
|
|
if (static_cast<unsigned char>(c) <= '\x1F')
|
|
{
|
|
// escape control characters
|
|
std::array<char, 9> cs{{}};
|
|
static_cast<void>((std::snprintf)(cs.data(), cs.size(), "<U+%.4X>", static_cast<unsigned char>(c))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
result += cs.data();
|
|
}
|
|
else
|
|
{
|
|
// add character as is
|
|
result.push_back(static_cast<std::string::value_type>(c));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// return syntax error message
|
|
JSON_HEDLEY_RETURNS_NON_NULL
|
|
constexpr const char* get_error_message() const noexcept
|
|
{
|
|
return error_message;
|
|
}
|
|
|
|
/////////////////////
|
|
// actual scanner
|
|
/////////////////////
|
|
|
|
/*!
|
|
@brief skip the UTF-8 byte order mark
|
|
@return true iff there is no BOM or the correct BOM has been skipped
|
|
*/
|
|
bool skip_bom()
|
|
{
|
|
if (get() == 0xEF)
|
|
{
|
|
// check if we completely parse the BOM
|
|
return get() == 0xBB && get() == 0xBF;
|
|
}
|
|
|
|
// the first character is not the beginning of the BOM; unget it to
|
|
// process is later
|
|
unget();
|
|
return true;
|
|
}
|
|
|
|
/// whether `current` is one of the four JSON whitespace characters
|
|
bool current_is_whitespace() const noexcept
|
|
{
|
|
return current == ' ' || current == '\t' || current == '\n' || current == '\r';
|
|
}
|
|
|
|
void skip_whitespace()
|
|
{
|
|
// the first character may be a pending unget() left over from the
|
|
// previous token (see get_ignoring_pending_unget()); every
|
|
// subsequent character read by this loop is guaranteed fresh, since
|
|
// nothing below calls unget()
|
|
get();
|
|
|
|
if (!current_is_whitespace())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// this is written as an if-guarded do-while (rather than a plain
|
|
// while loop) because that shape is what lets both GCC and Clang
|
|
// keep the input adapter's read pointer in a register across
|
|
// iterations; the equivalent while-loop measurably defeated that
|
|
// optimization in testing, turning long whitespace runs (e.g. the
|
|
// indentation of pretty-printed JSON) from a register-only loop
|
|
// into one that reloads the pointer from memory every character
|
|
do
|
|
{
|
|
get_ignoring_pending_unget();
|
|
}
|
|
while (current_is_whitespace());
|
|
}
|
|
|
|
token_type scan()
|
|
{
|
|
// initially, skip the BOM
|
|
if (position.chars_read_total == 0 && !skip_bom())
|
|
{
|
|
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// read the next character and ignore whitespace
|
|
skip_whitespace();
|
|
|
|
// ignore comments
|
|
while (ignore_comments && current == '/')
|
|
{
|
|
if (!scan_comment())
|
|
{
|
|
return token_type::parse_error;
|
|
}
|
|
|
|
// skip following whitespace
|
|
skip_whitespace();
|
|
}
|
|
|
|
switch (current)
|
|
{
|
|
// structural characters
|
|
case '[':
|
|
return token_type::begin_array;
|
|
case ']':
|
|
return token_type::end_array;
|
|
case '{':
|
|
return token_type::begin_object;
|
|
case '}':
|
|
return token_type::end_object;
|
|
case ':':
|
|
return token_type::name_separator;
|
|
case ',':
|
|
return token_type::value_separator;
|
|
|
|
// literals
|
|
case 't':
|
|
{
|
|
std::array<char_type, 4> true_literal = {{static_cast<char_type>('t'), static_cast<char_type>('r'), static_cast<char_type>('u'), static_cast<char_type>('e')}};
|
|
return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
|
|
}
|
|
case 'f':
|
|
{
|
|
std::array<char_type, 5> false_literal = {{static_cast<char_type>('f'), static_cast<char_type>('a'), static_cast<char_type>('l'), static_cast<char_type>('s'), static_cast<char_type>('e')}};
|
|
return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
|
|
}
|
|
case 'n':
|
|
{
|
|
std::array<char_type, 4> null_literal = {{static_cast<char_type>('n'), static_cast<char_type>('u'), static_cast<char_type>('l'), static_cast<char_type>('l')}};
|
|
return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
|
|
}
|
|
|
|
// string
|
|
case '\"':
|
|
return scan_string();
|
|
|
|
// number
|
|
case '-':
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
return scan_number_dispatch(std::integral_constant<bool, bulk_scan> {});
|
|
|
|
// end of input (the null byte is needed when parsing from
|
|
// string literals)
|
|
case '\0':
|
|
case char_traits<char_type>::eof():
|
|
return token_type::end_of_input;
|
|
|
|
// error
|
|
default:
|
|
error_message = "invalid literal";
|
|
return token_type::parse_error;
|
|
}
|
|
}
|
|
|
|
private:
|
|
/// input adapter
|
|
InputAdapterType ia;
|
|
|
|
/// whether comments should be ignored (true) or signaled as errors (false)
|
|
const bool ignore_comments = false;
|
|
|
|
/// the current character
|
|
char_int_type current = char_traits<char_type>::eof();
|
|
|
|
/// whether the next get() call should just return current
|
|
bool next_unget = false;
|
|
|
|
/// the start position of the current token
|
|
position_t position {};
|
|
|
|
/// the value chars_read_current_line had when the last newline was read, so
|
|
/// that unget() can restore the column instead of leaving it at 0
|
|
std::size_t chars_read_before_newline = 0;
|
|
|
|
/// raw input token string for error messages; only populated for streaming
|
|
/// adapters (seekable adapters reconstruct it lazily via token_string_start)
|
|
std::vector<char_type> token_string {};
|
|
|
|
/// start offset of the current token within the input, used to reconstruct
|
|
/// the last read token on error for seekable adapters (see collect_token_chars)
|
|
std::size_t token_string_start = 0;
|
|
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
/// start offset of the current token within the input, used to report
|
|
/// diagnostic positions (see reset())
|
|
std::size_t token_start_position = 0;
|
|
#endif
|
|
|
|
/// buffer for variable-length tokens (numbers, strings)
|
|
string_t token_buffer {};
|
|
|
|
/// a description of occurred lexer errors
|
|
const char* error_message = "";
|
|
|
|
// number values
|
|
number_integer_t value_integer = 0;
|
|
number_unsigned_t value_unsigned = 0;
|
|
number_float_t value_float = 0;
|
|
|
|
/// the decimal point
|
|
const char_int_type decimal_point_char = '.';
|
|
/// the position of the decimal point in the input
|
|
std::size_t decimal_point_position = std::string::npos;
|
|
|
|
/// whether the caller (e.g. accept()/json_sax_acceptor) only needs the
|
|
/// token classification and never looks at the converted numeric value;
|
|
/// when set, scan_number() may skip strtoull()/strtoll() for
|
|
/// value_unsigned/value_integer tokens whose digit count guarantees they
|
|
/// fit into 64 bits (see scan_number())
|
|
const bool discard_number_values = false;
|
|
};
|
|
|
|
} // namespace detail
|
|
NLOHMANN_JSON_NAMESPACE_END
|