Compare commits

...
Author SHA1 Message Date
Niels Lohmann da4ce8b8a3 Skip the indentation after a newline 8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which
skip_whitespace() read one character at a time. For contiguous input
(bulk_scan) the lexer now skips the spaces after a newline 8 bytes at a
time directly in the input buffer, including the leading spaces of the
last, partial word. This bypasses get(), which is safe because
bulk_scan implies the lazy position from the previous commit: nothing
is counted per character.

Compared to lazy positions alone, pretty-printed input (twitter,
citm_catalog with indent 2 and 4) parses up to 13% faster and accept()
gets up to 30% faster (GCC 13 and clang 18 on x86-64, Apple clang 21 on
arm64). Minified input is unchanged within noise.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 11:24:24 +02:00
Niels Lohmann 1d4dfc3af0 Report the parse errors of the position test via SAX
The test caught json::parse_error, so it aborted in the
ci_test_noexceptions build, where JSON_NOEXCEPTION turns the throw into
std::abort(). sax_parse() hands the error position and the exception
object to parse_error() without throwing, so the test now records them
there and runs in every configuration.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 11:22:48 +02:00
Niels Lohmann db3d98de07 Move the std::vector<std::byte> binary type tests out of unit-regression3
The #4804/#4209 tests instantiated a second basic_json specialization
(with_binary_t<std::vector<std::byte>>), including its parser and CBOR
reader. With clang 18, the C++20 object of unit-regression3.cpp had
32656 COFF sections on develop; the lazy position helpers pushed it to
32975, past what the MinGW linker of the Windows clang jobs handles
("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against .rdata").
unit-custom-binary-type.cpp already instantiates the same type, so the
tests move there; unit-regression3.cpp drops to 27616 sections.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 09:40:36 +02:00
Niels Lohmann 3dbed2a0cb Derive the lexer position lazily for seekable input
The lexer counted the read position on every character: two increments
and a newline check per get(), all on members kept in memory. For
seekable input (pointers, strings, other random-access byte ranges) the
position can be derived instead: chars_read_total is the adapter's read
count (including reads at EOF) minus a pending unget, and line and
column are recomputed from the consumed input only when get_position()
is called, i.e. on parse errors. JSON_DIAGNOSTIC_POSITIONS uses the new
O(1) get_chars_read_total(). Stream, file, wide-string and user-defined
adapters keep the eager counters.

Parsing pretty-printed input gets 8-12% faster and accept() 12-27%
(canada, citm_catalog, twitter; clang 21 arm64 and GCC 13 x86-64).
Error messages, byte offsets, SAX positions and diagnostic positions are
unchanged (differential comparison against develop on 548k inputs).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 08:52:49 +02:00
11 changed files with 607 additions and 130 deletions
+4 -2
View File
@@ -185,9 +185,11 @@ The lexer detects two optional extensions at compile time. Only `iterator_input_
random-access input of single-byte characters: random-access input of single-byte characters:
- `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input - `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input
for error messages instead of copying every character it reads. for error messages instead of copying every character it reads. With `get_read_count()` and
`count_consumed_lines()`, the lexer also derives the read position from the adapter and counts lines only when an
error needs them, instead of counting characters and lines on every read.
- `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in - `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in
contiguous memory, several bytes at a time. contiguous memory, several bytes at a time, and skip the spaces that indent a line in 8-byte steps.
The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the
`from_*` functions: `from_*` functions:
+19
View File
@@ -42,6 +42,25 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#endif #endif
} }
/// number of trailing zero bits of x (x != 0)
inline int count_trailing_zeros(std::uint64_t x) noexcept
{
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#else
int n = 0;
for (int shift = 32; shift != 0; shift >>= 1)
{
if ((x << (64 - shift)) == 0)
{
n += shift;
x >>= shift;
}
}
return n;
#endif
}
/// the 128-bit product of two 64-bit numbers /// the 128-bit product of two 64-bit numbers
struct uint128_parts struct uint128_parts
{ {
@@ -8,12 +8,12 @@
#pragma once #pragma once
#include <algorithm> // min #include <algorithm> // count, find, min
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstring> // strlen #include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next #include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <streambuf> // streambuf #include <streambuf> // streambuf
#include <string> // string, char_traits #include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer #include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
@@ -297,6 +297,7 @@ class iterator_input_adapter
return result; return result;
} }
++eof_reads;
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
@@ -306,6 +307,26 @@ class iterator_input_adapter
return static_cast<std::size_t>(std::distance(begin, current)); return static_cast<std::size_t>(std::distance(begin, current));
} }
// number of get_character() calls so far, including those that returned
// EOF; the lexer derives its position from it (see lexer::lazy_position)
std::size_t get_read_count() const
{
return get_consumed_count() + eof_reads;
}
// number of newlines in the consumed range [0, last_index); @a line_start
// is set to the index after the last of them (0 if there is none); only
// valid when supports_seek
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
}
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek // [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType> template<typename ContainerType>
@@ -422,6 +443,8 @@ class iterator_input_adapter
IteratorType begin; IteratorType begin;
IteratorType current; IteratorType current;
SentinelType end; SentinelType end;
/// number of get_character() calls that returned EOF
std::size_t eof_reads = 0;
template<typename BaseInputAdapter, size_t T> template<typename BaseInputAdapter, size_t T>
friend struct wide_string_input_helper; friend struct wide_string_input_helper;
+3 -3
View File
@@ -217,7 +217,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
v.start_position = lexer->get_position() - 1; v.start_position = lexer->get_chars_read_total() - 1;
} }
} }
@@ -231,7 +231,7 @@ struct diagnostic_positions
if (lexer) if (lexer)
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
} }
} }
@@ -247,7 +247,7 @@ struct diagnostic_positions
// Lexer has read past the current field value, so set the end position to the current position. // Lexer has read past the current field value, so set the end position to the current position.
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
switch (v.type()) switch (v.type())
{ {
+138 -20
View File
@@ -17,6 +17,7 @@
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/input_adapters.hpp> #include <nlohmann/detail/input/input_adapters.hpp>
#include <nlohmann/detail/input/number_parse.hpp> #include <nlohmann/detail/input/number_parse.hpp>
#include <nlohmann/detail/input/position_t.hpp> #include <nlohmann/detail/input/position_t.hpp>
@@ -186,6 +187,15 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string = static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {}); input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
/// chars_read_total is the adapter's read count (minus a pending unget),
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
/// whether a simulated unget can be passed on to the input adapter, which /// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see /// then leaves the character in the input; see
/// input_adapter_supports_lookahead /// input_adapter_supports_lookahead
@@ -195,7 +205,9 @@ class lexer : public lexer_base<BasicJsonType>
/// whether string scanning may bulk-consume runs of ordinary characters /// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires /// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing /// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. /// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
static constexpr bool bulk_scan = static constexpr bool bulk_scan =
lazy_token_string lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {}); && input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
@@ -331,10 +343,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(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 /// streaming input: no bulk fast path
@@ -1567,8 +1575,6 @@ scan_number_done:
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{ {
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return integer_result; return integer_result;
} }
// The value does not fit an integer, so this token converts as a // The value does not fit an integer, so this token converts as a
@@ -1585,8 +1591,6 @@ scan_number_done:
decimal_point_position = dot_index; decimal_point_position = dot_index;
ia.bulk_skip(len - 1); 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); return convert_number(number_type, mantissa_end);
} }
@@ -1637,7 +1641,7 @@ scan_number_done:
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
// the first character of the token has already been read, hence the -1 // the first character of the token has already been read, hence the -1
token_start_position = position.chars_read_total - 1; token_start_position = get_chars_read_total() - 1;
#endif #endif
note_token_start(std::integral_constant<bool, lazy_token_string> {}); note_token_start(std::integral_constant<bool, lazy_token_string> {});
@@ -1690,6 +1694,15 @@ scan_number_done:
/// per-character position counters (line-count-on-'\n' bookkeeping is /// per-character position counters (line-count-on-'\n' bookkeeping is
/// handled afterwards, in track_after_read(), once `current` is known) /// handled afterwards, in track_after_read(), once `current` is known)
void advance_position() noexcept void advance_position() noexcept
{
advance_position_impl(std::integral_constant<bool, lazy_position> {});
}
/// seekable adapter: the position is derived from the adapter on demand
void advance_position_impl(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the character
void advance_position_impl(std::false_type /*lazy*/) noexcept
{ {
++position.chars_read_total; ++position.chars_read_total;
++position.chars_read_current_line; ++position.chars_read_current_line;
@@ -1703,7 +1716,16 @@ scan_number_done:
// seekable adapters reconstruct the token lazily on error (see // seekable adapters reconstruct the token lazily on error (see
// get_token_string), so the eager per-character copy is skipped // get_token_string), so the eager per-character copy is skipped
capture_char(std::integral_constant<bool, lazy_token_string> {}); capture_char(std::integral_constant<bool, lazy_token_string> {});
track_newline(std::integral_constant<bool, lazy_position> {});
return current;
}
/// seekable adapter: lines are counted on demand in get_position()
void track_newline(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: start a new line after reading a newline
void track_newline(std::false_type /*lazy*/) noexcept
{
if (current == '\n') if (current == '\n')
{ {
++position.lines_read; ++position.lines_read;
@@ -1712,8 +1734,6 @@ scan_number_done:
chars_read_before_newline = position.chars_read_current_line; chars_read_before_newline = position.chars_read_current_line;
position.chars_read_current_line = 0; position.chars_read_current_line = 0;
} }
return current;
} }
/*! /*!
@@ -1757,12 +1777,22 @@ scan_number_done:
We implement unget by setting variable next_unget to true. The input is not We implement unget by setting variable next_unget to true. The input is not
changed - we just simulate ungetting by modifying chars_read_total, changed - we just simulate ungetting by modifying chars_read_total,
chars_read_current_line, and token_string. The next call to get() will chars_read_current_line, and token_string. The next call to get() will
behave as if the unget character is read again. behave as if the unget character is read again. With lazy_position, the
pending next_unget is all get_chars_read_total() needs.
*/ */
void unget() void unget()
{ {
next_unget = true; next_unget = true;
unget_position(std::integral_constant<bool, lazy_position> {});
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: get_chars_read_total() accounts for next_unget
void unget_position(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: step the counters back over the ungotten character
void unget_position(std::false_type /*lazy*/) noexcept
{
--position.chars_read_total; --position.chars_read_total;
// in case we "unget" a newline, we have to also decrement the lines_read // in case we "unget" a newline, we have to also decrement the lines_read
@@ -1785,8 +1815,6 @@ scan_number_done:
{ {
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
} }
/// adapter without lookahead: nothing to do (see release_lookahead) /// adapter without lookahead: nothing to do (see release_lookahead)
@@ -1857,10 +1885,21 @@ scan_number_done:
// diagnostics // diagnostics
///////////////////// /////////////////////
/// return position of last read token /*!
constexpr position_t get_position() const noexcept @brief return the number of characters read so far
Unlike get_position(), this is O(1) for all adapters, so it may be called
for every value (JSON_DIAGNOSTIC_POSITIONS).
*/
std::size_t get_chars_read_total() const noexcept
{ {
return position; return get_chars_read_total_impl(std::integral_constant<bool, lazy_position> {});
}
/// return position of last read token
position_t get_position() const
{
return get_position_impl(std::integral_constant<bool, lazy_position> {});
} }
/*! /*!
@@ -1897,6 +1936,43 @@ scan_number_done:
} }
#endif #endif
private:
/// seekable adapter: every get() either read from the adapter (including
/// reads at EOF, which get() counts as characters as well) or replayed an
/// ungotten character, so the count is the adapter's read count minus a
/// still pending unget
std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept
{
return ia.get_read_count() - (next_unget ? 1u : 0u);
}
/// streaming adapter: the counter is maintained by get() and unget()
std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept
{
return position.chars_read_total;
}
/// seekable adapter: count the lines in the consumed input; O(input read
/// so far), but only called on errors
position_t get_position_impl(std::true_type /*lazy*/) const
{
position_t result;
result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
std::size_t line_start = 0;
result.lines_read = ia.count_consumed_lines(consumed, line_start);
result.chars_read_current_line = result.chars_read_total - line_start;
return result;
}
/// streaming adapter: the position is maintained by get() and unget()
position_t get_position_impl(std::false_type /*lazy*/) const noexcept
{
return position;
}
public:
/// seekable adapter: rebuild the last read token from the input on demand /// 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 const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{ {
@@ -2004,15 +2080,57 @@ scan_number_done:
// into one that reloads the pointer from memory every character // into one that reloads the pointer from memory every character
do do
{ {
if (current == '\n')
{
skip_indentation(std::integral_constant<bool, bulk_scan> {});
}
get_ignoring_pending_unget(); get_ignoring_pending_unget();
} }
while (current_is_whitespace()); while (current_is_whitespace());
} }
/*!
@brief contiguous input: skip the space indentation that follows a newline
8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
*/
void skip_indentation(std::true_type /*bulk*/)
{
JSON_ASSERT(!next_unget);
const std::size_t remaining = ia.bulk_remaining();
if (remaining < 8)
{
return;
}
const char* const data = reinterpret_cast<const char*>(ia.bulk_data());
std::size_t pos = 0;
for (; pos + 8 <= remaining; pos += 8)
{
// little-endian, so the first byte is the lowest
const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull;
if (non_space != 0)
{
// skip the spaces before the first other byte of this word
pos += static_cast<std::size_t>(count_trailing_zeros(non_space)) / 8;
break;
}
}
ia.bulk_skip(pos);
}
/// streaming input: no bulk fast path
void skip_indentation(std::false_type /*bulk*/) const noexcept {}
token_type scan() token_type scan()
{ {
// initially, skip the BOM // initially, skip the BOM
if (position.chars_read_total == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
return token_type::parse_error; return token_type::parse_error;
@@ -2039,7 +2157,7 @@ scan_number_done:
token_type scan_expecting(token_type expected_type) token_type scan_expecting(token_type expected_type)
{ {
JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator);
JSON_ASSERT(position.chars_read_total > 0); JSON_ASSERT(get_chars_read_total() > 0);
const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ','); const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ',');
skip_whitespace(); skip_whitespace();
if (JSON_HEDLEY_LIKELY(current == expected_char)) if (JSON_HEDLEY_LIKELY(current == expected_char))
+5 -3
View File
@@ -370,7 +370,7 @@ class parser
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON"); return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
} }
@@ -520,8 +520,10 @@ class parser
{ {
// MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static
static_cast<void>(sax); static_cast<void>(sax);
return sax.parse_error(m_lexer.get_position(), m_lexer.get_token_string(), // computing the position is O(input read so far) for seekable input
parse_error::create(exception_id::syntax_error, m_lexer.get_position(), message, nullptr)); const position_t position = m_lexer.get_position();
return sax.parse_error(position, m_lexer.get_token_string(),
parse_error::create(exception_id::syntax_error, position, message, nullptr));
} }
std::string exception_message(const token_type expected, const std::string& context) std::string exception_message(const token_type expected, const std::string& context)
+215 -52
View File
@@ -7963,12 +7963,12 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // min #include <algorithm> // count, find, min
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstring> // strlen #include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next #include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <streambuf> // streambuf #include <streambuf> // streambuf
#include <string> // string, char_traits #include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer #include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
@@ -8257,6 +8257,7 @@ class iterator_input_adapter
return result; return result;
} }
++eof_reads;
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
@@ -8266,6 +8267,26 @@ class iterator_input_adapter
return static_cast<std::size_t>(std::distance(begin, current)); return static_cast<std::size_t>(std::distance(begin, current));
} }
// number of get_character() calls so far, including those that returned
// EOF; the lexer derives its position from it (see lexer::lazy_position)
std::size_t get_read_count() const
{
return get_consumed_count() + eof_reads;
}
// number of newlines in the consumed range [0, last_index); @a line_start
// is set to the index after the last of them (0 if there is none); only
// valid when supports_seek
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
}
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek // [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType> template<typename ContainerType>
@@ -8382,6 +8403,8 @@ class iterator_input_adapter
IteratorType begin; IteratorType begin;
IteratorType current; IteratorType current;
SentinelType end; SentinelType end;
/// number of get_character() calls that returned EOF
std::size_t eof_reads = 0;
template<typename BaseInputAdapter, size_t T> template<typename BaseInputAdapter, size_t T>
friend struct wide_string_input_helper; friend struct wide_string_input_helper;
@@ -8902,30 +8925,6 @@ NLOHMANN_JSON_NAMESPACE_END
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
// #include <nlohmann/detail/input/input_adapters.hpp>
// #include <nlohmann/detail/input/number_parse.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2021 The fast_float authors <https://github.com/fastfloat/fast_float>
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <array> // array
#include <cfloat> // FLT_EVAL_METHOD
#include <clocale> // localeconv
#include <cstddef> // size_t
#include <cstdint> // int64_t, uint64_t
#include <cstdlib> // strtof, strtod, strtold
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
// #include <nlohmann/detail/bit_ops.hpp> // #include <nlohmann/detail/bit_ops.hpp>
// __ _____ _____ _____ // __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ // __| | __| | | | JSON for Modern C++
@@ -8972,6 +8971,25 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#endif #endif
} }
/// number of trailing zero bits of x (x != 0)
inline int count_trailing_zeros(std::uint64_t x) noexcept
{
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#else
int n = 0;
for (int shift = 32; shift != 0; shift >>= 1)
{
if ((x << (64 - shift)) == 0)
{
n += shift;
x >>= shift;
}
}
return n;
#endif
}
/// the 128-bit product of two 64-bit numbers /// the 128-bit product of two 64-bit numbers
struct uint128_parts struct uint128_parts
{ {
@@ -9019,6 +9037,32 @@ inline std::uint64_t read_eight_bytes(const char* p) noexcept
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/input/input_adapters.hpp>
// #include <nlohmann/detail/input/number_parse.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2021 The fast_float authors <https://github.com/fastfloat/fast_float>
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <array> // array
#include <cfloat> // FLT_EVAL_METHOD
#include <clocale> // localeconv
#include <cstddef> // size_t
#include <cstdint> // int64_t, uint64_t
#include <cstdlib> // strtof, strtod, strtold
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
// #include <nlohmann/detail/bit_ops.hpp>
// #include <nlohmann/detail/input/pow5_table.hpp> // #include <nlohmann/detail/input/pow5_table.hpp>
// __ _____ _____ _____ // __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ // __| | __| | | | JSON for Modern C++
@@ -10584,6 +10628,15 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string = static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {}); input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
/// chars_read_total is the adapter's read count (minus a pending unget),
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
/// whether a simulated unget can be passed on to the input adapter, which /// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see /// then leaves the character in the input; see
/// input_adapter_supports_lookahead /// input_adapter_supports_lookahead
@@ -10593,7 +10646,9 @@ class lexer : public lexer_base<BasicJsonType>
/// whether string scanning may bulk-consume runs of ordinary characters /// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires /// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing /// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. /// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
static constexpr bool bulk_scan = static constexpr bool bulk_scan =
lazy_token_string lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {}); && input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
@@ -10729,10 +10784,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(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 /// streaming input: no bulk fast path
@@ -11965,8 +12016,6 @@ scan_number_done:
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{ {
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return integer_result; return integer_result;
} }
// The value does not fit an integer, so this token converts as a // The value does not fit an integer, so this token converts as a
@@ -11983,8 +12032,6 @@ scan_number_done:
decimal_point_position = dot_index; decimal_point_position = dot_index;
ia.bulk_skip(len - 1); 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); return convert_number(number_type, mantissa_end);
} }
@@ -12035,7 +12082,7 @@ scan_number_done:
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
// the first character of the token has already been read, hence the -1 // the first character of the token has already been read, hence the -1
token_start_position = position.chars_read_total - 1; token_start_position = get_chars_read_total() - 1;
#endif #endif
note_token_start(std::integral_constant<bool, lazy_token_string> {}); note_token_start(std::integral_constant<bool, lazy_token_string> {});
@@ -12088,6 +12135,15 @@ scan_number_done:
/// per-character position counters (line-count-on-'\n' bookkeeping is /// per-character position counters (line-count-on-'\n' bookkeeping is
/// handled afterwards, in track_after_read(), once `current` is known) /// handled afterwards, in track_after_read(), once `current` is known)
void advance_position() noexcept void advance_position() noexcept
{
advance_position_impl(std::integral_constant<bool, lazy_position> {});
}
/// seekable adapter: the position is derived from the adapter on demand
void advance_position_impl(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the character
void advance_position_impl(std::false_type /*lazy*/) noexcept
{ {
++position.chars_read_total; ++position.chars_read_total;
++position.chars_read_current_line; ++position.chars_read_current_line;
@@ -12101,7 +12157,16 @@ scan_number_done:
// seekable adapters reconstruct the token lazily on error (see // seekable adapters reconstruct the token lazily on error (see
// get_token_string), so the eager per-character copy is skipped // get_token_string), so the eager per-character copy is skipped
capture_char(std::integral_constant<bool, lazy_token_string> {}); capture_char(std::integral_constant<bool, lazy_token_string> {});
track_newline(std::integral_constant<bool, lazy_position> {});
return current;
}
/// seekable adapter: lines are counted on demand in get_position()
void track_newline(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: start a new line after reading a newline
void track_newline(std::false_type /*lazy*/) noexcept
{
if (current == '\n') if (current == '\n')
{ {
++position.lines_read; ++position.lines_read;
@@ -12110,8 +12175,6 @@ scan_number_done:
chars_read_before_newline = position.chars_read_current_line; chars_read_before_newline = position.chars_read_current_line;
position.chars_read_current_line = 0; position.chars_read_current_line = 0;
} }
return current;
} }
/*! /*!
@@ -12155,12 +12218,22 @@ scan_number_done:
We implement unget by setting variable next_unget to true. The input is not We implement unget by setting variable next_unget to true. The input is not
changed - we just simulate ungetting by modifying chars_read_total, changed - we just simulate ungetting by modifying chars_read_total,
chars_read_current_line, and token_string. The next call to get() will chars_read_current_line, and token_string. The next call to get() will
behave as if the unget character is read again. behave as if the unget character is read again. With lazy_position, the
pending next_unget is all get_chars_read_total() needs.
*/ */
void unget() void unget()
{ {
next_unget = true; next_unget = true;
unget_position(std::integral_constant<bool, lazy_position> {});
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: get_chars_read_total() accounts for next_unget
void unget_position(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: step the counters back over the ungotten character
void unget_position(std::false_type /*lazy*/) noexcept
{
--position.chars_read_total; --position.chars_read_total;
// in case we "unget" a newline, we have to also decrement the lines_read // in case we "unget" a newline, we have to also decrement the lines_read
@@ -12183,8 +12256,6 @@ scan_number_done:
{ {
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
} }
/// adapter without lookahead: nothing to do (see release_lookahead) /// adapter without lookahead: nothing to do (see release_lookahead)
@@ -12255,10 +12326,21 @@ scan_number_done:
// diagnostics // diagnostics
///////////////////// /////////////////////
/// return position of last read token /*!
constexpr position_t get_position() const noexcept @brief return the number of characters read so far
Unlike get_position(), this is O(1) for all adapters, so it may be called
for every value (JSON_DIAGNOSTIC_POSITIONS).
*/
std::size_t get_chars_read_total() const noexcept
{ {
return position; return get_chars_read_total_impl(std::integral_constant<bool, lazy_position> {});
}
/// return position of last read token
position_t get_position() const
{
return get_position_impl(std::integral_constant<bool, lazy_position> {});
} }
/*! /*!
@@ -12295,6 +12377,43 @@ scan_number_done:
} }
#endif #endif
private:
/// seekable adapter: every get() either read from the adapter (including
/// reads at EOF, which get() counts as characters as well) or replayed an
/// ungotten character, so the count is the adapter's read count minus a
/// still pending unget
std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept
{
return ia.get_read_count() - (next_unget ? 1u : 0u);
}
/// streaming adapter: the counter is maintained by get() and unget()
std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept
{
return position.chars_read_total;
}
/// seekable adapter: count the lines in the consumed input; O(input read
/// so far), but only called on errors
position_t get_position_impl(std::true_type /*lazy*/) const
{
position_t result;
result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
std::size_t line_start = 0;
result.lines_read = ia.count_consumed_lines(consumed, line_start);
result.chars_read_current_line = result.chars_read_total - line_start;
return result;
}
/// streaming adapter: the position is maintained by get() and unget()
position_t get_position_impl(std::false_type /*lazy*/) const noexcept
{
return position;
}
public:
/// seekable adapter: rebuild the last read token from the input on demand /// 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 const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{ {
@@ -12402,15 +12521,57 @@ scan_number_done:
// into one that reloads the pointer from memory every character // into one that reloads the pointer from memory every character
do do
{ {
if (current == '\n')
{
skip_indentation(std::integral_constant<bool, bulk_scan> {});
}
get_ignoring_pending_unget(); get_ignoring_pending_unget();
} }
while (current_is_whitespace()); while (current_is_whitespace());
} }
/*!
@brief contiguous input: skip the space indentation that follows a newline
8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
*/
void skip_indentation(std::true_type /*bulk*/)
{
JSON_ASSERT(!next_unget);
const std::size_t remaining = ia.bulk_remaining();
if (remaining < 8)
{
return;
}
const char* const data = reinterpret_cast<const char*>(ia.bulk_data());
std::size_t pos = 0;
for (; pos + 8 <= remaining; pos += 8)
{
// little-endian, so the first byte is the lowest
const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull;
if (non_space != 0)
{
// skip the spaces before the first other byte of this word
pos += static_cast<std::size_t>(count_trailing_zeros(non_space)) / 8;
break;
}
}
ia.bulk_skip(pos);
}
/// streaming input: no bulk fast path
void skip_indentation(std::false_type /*bulk*/) const noexcept {}
token_type scan() token_type scan()
{ {
// initially, skip the BOM // initially, skip the BOM
if (position.chars_read_total == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
return token_type::parse_error; return token_type::parse_error;
@@ -12437,7 +12598,7 @@ scan_number_done:
token_type scan_expecting(token_type expected_type) token_type scan_expecting(token_type expected_type)
{ {
JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator);
JSON_ASSERT(position.chars_read_total > 0); JSON_ASSERT(get_chars_read_total() > 0);
const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ','); const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ',');
skip_whitespace(); skip_whitespace();
if (JSON_HEDLEY_LIKELY(current == expected_char)) if (JSON_HEDLEY_LIKELY(current == expected_char))
@@ -12791,7 +12952,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
v.start_position = lexer->get_position() - 1; v.start_position = lexer->get_chars_read_total() - 1;
} }
} }
@@ -12805,7 +12966,7 @@ struct diagnostic_positions
if (lexer) if (lexer)
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
} }
} }
@@ -12821,7 +12982,7 @@ struct diagnostic_positions
// Lexer has read past the current field value, so set the end position to the current position. // Lexer has read past the current field value, so set the end position to the current position.
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
switch (v.type()) switch (v.type())
{ {
@@ -18664,7 +18825,7 @@ class parser
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON"); return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
} }
@@ -18814,8 +18975,10 @@ class parser
{ {
// MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static
static_cast<void>(sax); static_cast<void>(sax);
return sax.parse_error(m_lexer.get_position(), m_lexer.get_token_string(), // computing the position is O(input read so far) for seekable input
parse_error::create(exception_id::syntax_error, m_lexer.get_position(), message, nullptr)); const position_t position = m_lexer.get_position();
return sax.parse_error(position, m_lexer.get_token_string(),
parse_error::create(exception_id::syntax_error, position, message, nullptr));
} }
std::string exception_message(const token_type expected, const std::string& context) std::string exception_message(const token_type expected, const std::string& context)
+4
View File
@@ -938,6 +938,10 @@ TEST_CASE("Eisel-Lemire float conversion")
const int k = i % 64; const int k = i % 64;
const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1)); const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1));
CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k); CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k);
// the mirror image: bit k set, arbitrary bits above it
const std::uint64_t y = (std::uint64_t{1} << k) | (k == 63 ? 0 : (a << (k + 1)));
CHECK(nlohmann::detail::count_trailing_zeros(y) == k);
} }
} }
+57
View File
@@ -8,6 +8,13 @@
#include "doctest_compatibility.h" #include "doctest_compatibility.h"
// capture whether JSON_DISABLE_ENUM_SERIALIZATION was enabled on the command
// line before including json.hpp, which #undefs it; std::byte is an enum, so
// converting std::vector<std::byte> needs enum serialization
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cstdint> #include <cstdint>
@@ -85,3 +92,53 @@ TEST_CASE("binary type whose value type is not std::uint8_t")
} }
#endif #endif
} }
#ifdef JSON_HAS_CPP_17
// moved from unit-regression3.cpp, so that file does not instantiate a second
// basic_json specialization (MinGW section limit, see windows.yml)
TEST_CASE("std::vector<std::byte> as binary type")
{
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = byte_binary_json::from_cbor(data);
CHECK((decoded == byte_binary_json::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const byte_binary_json j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const nlohmann::json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = byte_binary_json::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
}
#endif
+137
View File
@@ -35,6 +35,7 @@ using nlohmann::json;
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <valarray> #include <valarray>
#include <vector>
#include "sax_event_loggers.hpp" #include "sax_event_loggers.hpp"
@@ -1250,3 +1251,139 @@ TEST_CASE_TEMPLATE("deserialization of different character types (UTF-32)", T, c
CHECK(json::sax_parse(v, &l)); CHECK(json::sax_parse(v, &l));
CHECK(l.events.size() == 1); CHECK(l.events.size() == 1);
} }
namespace
{
// records the position and message of a parse error; reporting the error via
// SAX does not throw, so the test also runs with JSON_NOEXCEPTION
struct SaxErrorRecorder : public SaxEventLogger
{
bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& ex) override
{
error = std::to_string(position) + " " + ex.what();
return false;
}
std::string error {}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parse error positions are the same for contiguous and stream input")
{
// contiguous input derives the position from the input on error, stream
// input counts it while reading (see lexer::lazy_position); the cases
// cover newlines before the error, ungetting a newline after a number,
// newlines inside a string, a comment, and reads at the end of input
struct test_case
{
std::string input;
std::size_t byte;
std::string message;
};
const std::vector<test_case> cases =
{
{"[\n 1,\n 2\n", 12, "[json.exception.parse_error.101] parse error at line 4, column 1: syntax error while parsing array - unexpected end of input; expected ']'"},
{"[1\n,2,\n]", 8, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal"},
{"[1\n x]", 5, "[json.exception.parse_error.101] parse error at line 2, column 2: syntax error while parsing array - invalid literal; last read: '1<U+000A> x'; expected ']'"},
{"1\n2", 3, "[json.exception.parse_error.101] parse error at line 2, column 1: syntax error while parsing value - unexpected number literal; expected end of input"},
{
"{\"a\":\n\"b\nc\"}", 9, R"([json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid string: control character U+000A (LF) must be escaped to \u000A or \n; last read: '"b<U+000A>')"
},
{"/* a\n b */\n x", 13, "[json.exception.parse_error.101] parse error at line 3, column 2: syntax error while parsing value - invalid literal; last read: '/* a<U+000A> b */<U+000A> x'"},
{"\n\n", 3, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal"},
{"[\"a\",\n 1e\n]", 10, "[json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid number; expected '+', '-', or digit after exponent; last read: '1e<U+000A>'"},
{"[tru\n]", 5, "[json.exception.parse_error.101] parse error at line 2, column 0: syntax error while parsing value - invalid literal; last read: '[tru<U+000A>'"},
};
for (const auto& c : cases)
{
CAPTURE(c.input)
const std::string expected = std::to_string(c.byte) + " " + c.message;
SaxErrorRecorder string_sax;
CHECK_FALSE(json::sax_parse(c.input, &string_sax, json::input_format_t::json, true, true));
CHECK(string_sax.error == expected);
SaxErrorRecorder pointer_sax;
CHECK_FALSE(json::sax_parse(c.input.data(), c.input.data() + c.input.size(), &pointer_sax, json::input_format_t::json, true, true));
CHECK(pointer_sax.error == expected);
std::istringstream is(c.input);
SaxErrorRecorder stream_sax;
CHECK_FALSE(json::sax_parse(is, &stream_sax, json::input_format_t::json, true, true));
CHECK(stream_sax.error == expected);
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("indentation runs of every length around the 8-byte skip")
{
// contiguous input skips the spaces after a newline 8 bytes at a time (see
// lexer::skip_indentation), stream input reads them one by one; results,
// byte offsets, and messages must be the same for runs that end anywhere
// relative to a word boundary, including at the end of the input
const auto parse_result = [](const std::string & input, bool contiguous)
{
try
{
if (contiguous)
{
return json::parse(input.data(), input.data() + input.size(), nullptr, true, true).dump();
}
std::istringstream is(input);
return json::parse(is, nullptr, true, true).dump();
}
catch (const json::parse_error& e)
{
return std::to_string(e.byte).append(" ").append(e.what());
}
};
// replace every '~' in a pattern by an indentation run
const auto indented = [](const std::string & pattern, const std::string & indent)
{
std::string result;
for (const char c : pattern)
{
if (c == '~')
{
result += indent;
}
else
{
result += c;
}
}
return result;
};
const std::vector<std::string> patterns =
{
"[\n~1,\n~2\n]",
"[\n~1,\n~x]",
"[\n~1,\n~",
"1\n~",
"[\n~\t1,\n~\r\n~2]",
"[\n~/* c */ 1]",
"[\n~1\n~2]",
};
for (std::size_t n = 0; n <= 33; ++n)
{
const std::string indent(n, ' ');
for (const auto& pattern : patterns)
{
const std::string input = indented(pattern, indent);
CAPTURE(n)
CAPTURE(input)
const std::string expected = parse_result(input, false);
CHECK(parse_result(input, true) == expected);
CHECK(json::accept(input.data(), input.data() + input.size(), true) == json::accept(input, true));
}
}
// and against fixed expectations, in case both paths broke the same way
CHECK(parse_result(indented("[\n~x]", std::string(16, ' ')), true) == "19 [json.exception.parse_error.101] parse error at line 2, column 17: syntax error while parsing value - invalid literal; last read: '[<U+000A> x'");
CHECK(parse_result(indented("[\n~1]", std::string(17, ' ')), true) == "[1]");
}
#endif
-48
View File
@@ -58,11 +58,6 @@ using ordered_json = nlohmann::ordered_json;
#include <optional> #include <optional>
#elif __has_include(<experimental/optional>) #elif __has_include(<experimental/optional>)
#endif #endif
/////////////////////////////////////////////////////////////////////
// for #4804
/////////////////////////////////////////////////////////////////////
using json_4804 = nlohmann::json::with_binary_t<std::vector<std::byte>>;
#endif #endif
#ifdef JSON_HAS_CPP_20 #ifdef JSON_HAS_CPP_20
@@ -458,49 +453,6 @@ TEST_CASE("regression tests 3")
#endif #endif
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = json_4804::from_cbor(data);
CHECK((decoded == json_4804::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const json_4804 j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = json_4804::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
{ {
const json jval{}; const json jval{};