Compare commits

..
Author SHA1 Message Date
Niels Lohmann e7ea330f35 BSON tests: fix clang-tidy findings in the one-pass writer test
ci_clang_tidy reports C-style arrays (handlers, bad_entries), functional casts
std::size_t(n) and a nested conditional operator in the new test. Use
std::array, unsigned literals and a switch.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 06:17:47 +01:00
Niels Lohmann d746f871c0 Fix ci_clang_tidy and ci_test_noexceptions for the BSON writer
ci_clang_tidy (modernize-use-auto) flagged the cast initialization in
bson_entry_fits(); ci_test_noexceptions aborted in the "errors at every
position" section of unit-bson.cpp, whose entries all throw, so that section
now needs exceptions like the rest of the error checks.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-31 20:22:33 +01:00
Niels Lohmann a624682c1d BSON writer: write to_bson(j) in one pass, with sizes written afterwards
BSON prefixes every document and array with its size. to_bson() therefore
walked the value twice: once to compute every size, once to write. When
the output is the vector that to_bson(j) returns, the writer now puts a
placeholder in front of each document and array and writes its size there
once the document is complete, so one walk is enough.

Every key and value is still checked as the size pass checked it, in the
same order, so the same exception is thrown; the partial output is
discarded with the vector. If a length would not fit BSON's 32-bit size
fields, the writer drops what it has written and writes the document in
two passes, which reports that error before writing anything, as before.

The two passes remain for output adapters (streams, strings, vectors
passed by reference), for error_handler_t::replace and ignore (which
change sizes), and for integer types wider than 64 bits.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-31 09:09:04 +01:00
8 changed files with 753 additions and 267 deletions

No files matched your search

+42 -92
View File
@@ -209,13 +209,8 @@ constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
@brief lexical analysis @brief lexical analysis
This class organizes the lexical analysis during JSON deserialization. This class organizes the lexical analysis during JSON deserialization.
DiscardValues: the caller only needs the token types and never looks at the values of number and string tokens (only
accept(), which parses through json_sax_acceptor). A template parameter rather than a member, so that the lexer of
parse() is compiled without the paths of accept(): with both string scans in one scan(), parse() was up to 2 % slower
on Apple M1.
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false> template<typename BasicJsonType, typename InputAdapterType>
class lexer : public lexer_base<BasicJsonType> class lexer : public lexer_base<BasicJsonType>
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
@@ -273,9 +268,10 @@ class lexer : public lexer_base<BasicJsonType>
public: public:
using token_type = typename lexer_base<BasicJsonType>::token_type; using token_type = typename lexer_base<BasicJsonType>::token_type;
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false) noexcept explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false, bool discard_number_values_ = false) noexcept
: ia(std::move(adapter)) : ia(std::move(adapter))
, ignore_comments(ignore_comments_) , ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{ {
// the token string of an error before the first token is everything // the token string of an error before the first token is everything
// read so far // read so far
@@ -392,8 +388,6 @@ class lexer : public lexer_base<BasicJsonType>
checks if it is inside the range. If a violation was detected, set up an checks if it is inside the range. If a violation was detected, set up an
error message and return false. Otherwise, return true. error message and return false. Otherwise, return true.
@param[in] keep whether the bytes are added to token_buffer (false for
accept(), see scan())
@param[in] ranges list of integers; interpreted as list of pairs of @param[in] ranges list of integers; interpreted as list of pairs of
inclusive lower and upper bound, respectively inclusive lower and upper bound, respectively
@@ -402,18 +396,17 @@ class lexer : public lexer_base<BasicJsonType>
@return true if and only if no range violation was detected @return true if and only if no range violation was detected
*/ */
template<bool Keep> bool next_byte_in_range(std::initializer_list<char_int_type> ranges)
bool next_byte_in_range(std::integral_constant<bool, Keep> keep, std::initializer_list<char_int_type> ranges)
{ {
JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6); JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
add_string_char(keep, current); add(current);
for (auto range = ranges.begin(); range != ranges.end(); ++range) for (auto range = ranges.begin(); range != ranges.end(); ++range)
{ {
get(); get();
if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions) if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions)
{ {
add_string_char(keep, current); add(current);
} }
else else
{ {
@@ -426,16 +419,14 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// contiguous input: bulk-append the run of ordinary characters and complete /// contiguous input: bulk-append the run of ordinary characters and complete
/// well-formed UTF-8 sequences starting at the current read position (only /// well-formed UTF-8 sequences starting at the current read position, then
/// skip it if @a keep is false), then
/// read the first byte that needs individual handling (the closing quote, an /// read the first byte that needs individual handling (the closing quote, an
/// escape, a control character, or an ill-formed UTF-8 byte) like get(). /// escape, a control character, or an ill-formed UTF-8 byte) like get().
/// That byte is taken from the buffer that was just scanned. Through get(), /// That byte is taken from the buffer that was just scanned. Through get(),
/// Apple Clang merged this read with the other get() calls of the scan /// Apple Clang merged this read with the other get() calls of the scan
/// function, which load the read position again right after it was stored /// function, which load the read position again right after it was stored
/// (3% of json::accept() on jeopardy.json on Apple M1). /// (3% of json::accept() on jeopardy.json on Apple M1).
template<bool Keep> char_int_type get_after_string_run(std::true_type /*bulk*/)
char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep)
{ {
// a pending unget must be consumed through the normal path first // a pending unget must be consumed through the normal path first
if (next_unget) if (next_unget)
@@ -453,7 +444,7 @@ class lexer : public lexer_base<BasicJsonType>
const std::size_t pos = lexer_string_bulk_run(data, remaining); const std::size_t pos = lexer_string_bulk_run(data, remaining);
if (pos != 0) if (pos != 0)
{ {
append_string_run(keep, data, pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
bulk_consume(reinterpret_cast<const char*>(data), pos); bulk_consume(reinterpret_cast<const char*>(data), pos);
} }
if (pos == remaining) if (pos == remaining)
@@ -497,8 +488,7 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
template<bool Keep> char_int_type get_after_string_run(std::false_type /*bulk*/)
char_int_type get_after_string_run(std::false_type /*bulk*/, std::integral_constant<bool, Keep> /*keep*/)
{ {
return get(); return get();
} }
@@ -512,18 +502,13 @@ class lexer : public lexer_base<BasicJsonType>
may contain \0 bytes), and token_buffer.size() is the number of bytes in the may contain \0 bytes), and token_buffer.size() is the number of bytes in the
string. string.
@param[in] keep whether the string is copied into token_buffer; if false
(accept(), see scan()), it is only checked, with the same
errors, and token_buffer stays empty
@return token_type::value_string if string could be successfully scanned, @return token_type::value_string if string could be successfully scanned,
token_type::parse_error otherwise token_type::parse_error otherwise
@note In case of errors, variable error_message contains a textual @note In case of errors, variable error_message contains a textual
description. description.
*/ */
template<bool Keep> token_type scan_string()
token_type scan_string(std::integral_constant<bool, Keep> keep)
{ {
// reset token_buffer (ignore opening quote) // reset token_buffer (ignore opening quote)
reset(); reset();
@@ -535,7 +520,7 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// bulk-consume ordinary characters from contiguous input, then // bulk-consume ordinary characters from contiguous input, then
// get the next character, the one that ends the run // get the next character, the one that ends the run
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>(), keep)) switch (get_after_string_run(std::integral_constant<bool, bulk_scan>()))
{ {
// end of file while parsing the string // end of file while parsing the string
case char_traits<char_type>::eof(): case char_traits<char_type>::eof():
@@ -557,35 +542,35 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// quotation mark // quotation mark
case '\"': case '\"':
add_string_char(keep, '\"'); add('\"');
break; break;
// reverse solidus // reverse solidus
case '\\': case '\\':
add_string_char(keep, '\\'); add('\\');
break; break;
// solidus // solidus
case '/': case '/':
add_string_char(keep, '/'); add('/');
break; break;
// backspace // backspace
case 'b': case 'b':
add_string_char(keep, '\b'); add('\b');
break; break;
// form feed // form feed
case 'f': case 'f':
add_string_char(keep, '\f'); add('\f');
break; break;
// line feed // line feed
case 'n': case 'n':
add_string_char(keep, '\n'); add('\n');
break; break;
// carriage return // carriage return
case 'r': case 'r':
add_string_char(keep, '\r'); add('\r');
break; break;
// tab // tab
case 't': case 't':
add_string_char(keep, '\t'); add('\t');
break; break;
// unicode escapes // unicode escapes
@@ -653,7 +638,10 @@ class lexer : public lexer_base<BasicJsonType>
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF); JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
// translate codepoint into bytes // translate codepoint into bytes
add_codepoint(keep, codepoint); encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
break; break;
} }
@@ -836,7 +824,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0x7E: case 0x7E:
case 0x7F: case 0x7F:
{ {
add_string_char(keep, current); add(current);
break; break;
} }
@@ -872,7 +860,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xDE: case 0xDE:
case 0xDF: case 0xDF:
{ {
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range(keep, {0x80, 0xBF}))) if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF})))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -882,7 +870,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+0800..U+0FFF: bytes E0 A0..BF 80..BF // U+0800..U+0FFF: bytes E0 A0..BF 80..BF
case 0xE0: case 0xE0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0xA0, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -906,7 +894,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xEE: case 0xEE:
case 0xEF: case 0xEF:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -916,7 +904,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+D000..U+D7FF: bytes ED 80..9F 80..BF // U+D000..U+D7FF: bytes ED 80..9F 80..BF
case 0xED: case 0xED:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x9F, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -926,7 +914,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+10000..U+3FFFF F0 90..BF 80..BF 80..BF // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
case 0xF0: case 0xF0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -938,7 +926,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xF2: case 0xF2:
case 0xF3: case 0xF3:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -948,7 +936,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+100000..U+10FFFF F4 80..8F 80..BF 80..BF // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
case 0xF4: case 0xF4:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -1439,7 +1427,7 @@ scan_number_done:
token_type convert_number(token_type number_type, std::size_t mantissa_end) token_type convert_number(token_type number_type, std::size_t mantissa_end)
{ {
// accept() only needs to know whether the input is valid, so it sets // accept() only needs to know whether the input is valid, so it sets
// discard_values (see json.hpp), and an integer token whose // discard_number_values (see json.hpp), and an integer token whose
// digit count shows that it fits is reported without calling // digit count shows that it fits is reported without calling
// convert_integer(). A number with up to 18 digits always fits into // convert_integer(). A number with up to 18 digits always fits into
// both std::uint64_t and std::int64_t (18 nines is about 1e18, below // both std::uint64_t and std::int64_t (18 nines is about 1e18, below
@@ -1454,7 +1442,7 @@ scan_number_done:
// json_sax_acceptor, whose number callbacks discard their argument and // json_sax_acceptor, whose number callbacks discard their argument and
// return true, and the parser rejects neither integers nor finite // return true, and the parser rejects neither integers nor finite
// floats. value_unsigned/value_integer are left unset here, so a caller // floats. value_unsigned/value_integer are left unset here, so a caller
// that reads the converted value must not set discard_values. // that reads the converted value must not set discard_number_values.
// //
// On contiguous input, scan_number_bulk_contiguous() converts integer // On contiguous input, scan_number_bulk_contiguous() converts integer
// tokens itself and does not pass them to this function, unless // tokens itself and does not pass them to this function, unless
@@ -1462,7 +1450,7 @@ scan_number_done:
// reached for input without bulk access (e.g. streams), with // reached for input without bulk access (e.g. streams), with
// JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous() // JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous()
// falls back to scan_number(). // falls back to scan_number().
if (discard_values()) if (discard_number_values)
{ {
constexpr std::size_t safe_digit_count = 18; constexpr std::size_t safe_digit_count = 18;
if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count) if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count)
@@ -1713,7 +1701,7 @@ scan_number_done:
// w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a // w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a
// value that underflows is a finite number as well. Only a token that // value that underflows is a finite number as well. Only a token that
// may overflow has to be converted to find out. // may overflow has to be converted to find out.
if (JSON_HEDLEY_UNLIKELY(discard_values() && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10)) if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
{ {
value_float = 0; value_float = 0;
return true; return true;
@@ -2045,34 +2033,6 @@ scan_number_done:
token_buffer.push_back(static_cast<typename string_t::value_type>(c)); token_buffer.push_back(static_cast<typename string_t::value_type>(c));
} }
/// add a character of a string token; accept() checks the string but does
/// not keep its value (@a keep is false)
void add_string_char(std::true_type /*keep*/, char_int_type c)
{
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
}
static void add_string_char(std::false_type /*keep*/, char_int_type /*c*/) noexcept {}
/// add the UTF-8 bytes of a code point from a \\u escape to a string token
void add_codepoint(std::true_type /*keep*/, int codepoint)
{
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
}
static void add_codepoint(std::false_type /*keep*/, int /*codepoint*/) noexcept {}
/// append a run of ordinary string characters (see get_after_string_run())
void append_string_run(std::true_type /*keep*/, const unsigned char* data, std::size_t n)
{
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), n);
}
static void append_string_run(std::false_type /*keep*/, const unsigned char* /*data*/, std::size_t /*n*/) noexcept {}
public: public:
///////////////////// /////////////////////
// value getters // value getters
@@ -2467,9 +2427,7 @@ scan_number_done:
// string // string
case '\"': case '\"':
// accept() (DiscardValues) checks strings without copying them return scan_string();
// into token_buffer
return scan_string(keep_strings {});
// number // number
case '-': case '-':
@@ -2554,19 +2512,11 @@ scan_number_done:
std::size_t decimal_point_position = std::string::npos; std::size_t decimal_point_position = std::string::npos;
/// whether the caller only needs the token types and never looks at the /// whether the caller only needs the token types and never looks at the
/// values of number and string tokens; DiscardValues, set only by accept(), which parses /// converted numeric values; set only by accept(), which parses through
/// through json_sax_acceptor. When set, convert_number() skips converting /// json_sax_acceptor. When set, convert_number() skips converting integer
/// integer tokens whose digit count guarantees that they fit into 64 bits /// tokens whose digit count guarantees that they fit into 64 bits (see
/// (see there), floats are converted only when they may overflow (see /// there)
/// convert_accumulated_float()), and strings are checked without being const bool discard_number_values = false;
/// copied into token_buffer (see scan())
static bool discard_values() noexcept
{
return DiscardValues;
}
/// the tag of scan_string(): whether string tokens are kept
using keep_strings = typename std::conditional<DiscardValues, std::false_type, std::true_type>::type;
}; };
} // namespace detail } // namespace detail
+5 -4
View File
@@ -72,14 +72,14 @@ This class implements a parser for JSON text. Nested arrays and objects are trac
stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read
is reported as SAX events. is reported as SAX events.
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false> template<typename BasicJsonType, typename InputAdapterType>
class parser class parser
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
using number_unsigned_t = typename BasicJsonType::number_unsigned_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
using number_float_t = typename BasicJsonType::number_float_t; using number_float_t = typename BasicJsonType::number_float_t;
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
using lexer_t = lexer<BasicJsonType, InputAdapterType, DiscardValues>; using lexer_t = lexer<BasicJsonType, InputAdapterType>;
using token_type = typename lexer_t::token_type; using token_type = typename lexer_t::token_type;
public: public:
@@ -88,9 +88,10 @@ class parser
parser_callback_t<BasicJsonType> cb = nullptr, parser_callback_t<BasicJsonType> cb = nullptr,
const bool allow_exceptions_ = true, const bool allow_exceptions_ = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas_ = false) const bool ignore_trailing_commas_ = false,
const bool discard_number_values_ = false)
: callback(std::move(cb)) : callback(std::move(cb))
, m_lexer(std::move(adapter), ignore_comments) , m_lexer(std::move(adapter), ignore_comments, discard_number_values_)
, allow_exceptions(allow_exceptions_) , allow_exceptions(allow_exceptions_)
, ignore_trailing_commas(ignore_trailing_commas_) , ignore_trailing_commas(ignore_trailing_commas_)
{ {
@@ -2088,7 +2088,8 @@ class binary_writer
/// arrays: the first index with one more digit; only maintained by /// arrays: the first index with one more digit; only maintained by
/// @ref calc_bson_sizes /// @ref calc_bson_sizes
std::uint64_t index_boundary = 10; std::uint64_t index_boundary = 10;
/// @ref calc_bson_sizes only: where its size goes in the table /// @ref calc_bson_sizes: where its size goes in the table; @ref
/// write_bson_document_single_pass: the position of its size field
std::size_t size_slot; std::size_t size_slot;
/// @ref calc_bson_sizes only: the size of its entries seen so far /// @ref calc_bson_sizes only: the size of its entries seen so far
std::size_t entries_size = 0; std::size_t entries_size = 0;
@@ -2339,6 +2340,197 @@ class binary_writer
} }
} }
template<typename SinkType>
using sink_written_t = decltype(std::declval<const SinkType&>().written());
/*!
@brief whether BSON documents can be written in a single pass: the sink
can overwrite what it has written (output_vector_sink), and every
integer fits the 64 bits BSON has, so that writing an array of
numbers needs no range check
*/
using bson_single_pass_sink = std::integral_constant < bool,
is_detected<sink_written_t, OutputSinkType>::value
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::digits <= std::numeric_limits<std::int64_t>::digits
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::digits <= std::numeric_limits<std::uint64_t>::digits >;
/// write a placeholder for the size of an object or array whose entries
/// follow; @return its position, for @ref patch_bson_size
std::size_t write_bson_size_placeholder()
{
const std::size_t pos = oa.written();
write_number<std::int32_t>(0, true);
return pos;
}
/// end the object or array whose size is at @a pos and write its size
/// there: everything from that field to the terminating null byte
/// @return false (and the size is not written) if it does not fit
bool close_bson_container(const std::size_t pos)
{
oa.write_character(to_char_type(0x00));
const std::size_t size = oa.written() - pos;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
{
return false;
}
std::array<CharType, sizeof(std::int32_t)> bytes{};
store_little_endian<std::int32_t>(bytes.data(), static_cast<std::int32_t>(size));
oa.overwrite(pos, bytes.data(), bytes.size());
return true;
}
/*!
@brief check the value @a j of a BSON entry as @ref calc_bson_value_size
does, for @ref write_bson_document_single_pass
@return at least the size of the value
A number, boolean, or null needs no check there (its integers have at most
64 bits) and has at most 8 characters; a discarded value is rejected with
the same exception when it is written.
*/
std::size_t check_bson_value(const BasicJsonType& j)
{
return (j.is_string() || j.is_binary()) ? calc_bson_value_size(j) : 8;
}
/// whether an entry of @a entry_size characters keeps the document that
/// begins at @a start, closed by @a open_containers null bytes, within
/// the 32-bit size of BSON
bool bson_entry_fits(const std::size_t start, const std::size_t entry_size, const std::size_t open_containers) const noexcept
{
const auto limit = static_cast<std::size_t>((std::numeric_limits<std::int32_t>::max)());
const std::size_t written = oa.written() - start;
return written <= limit && entry_size <= limit - written && open_containers <= limit - written - entry_size;
}
/*!
@brief Serializes the JSON object @a document as a BSON document in a
single pass, writing the size of each object and array once it is
complete
Checks every key and value as @ref calc_bson_sizes does, in the same
order, so the same exception is thrown; the output written up to then is
discarded with the vector it was written to.
@return false as soon as the document or a string, binary value, object,
or array in it would be larger than the 32-bit BSON sizes allow;
nothing large has been written then, and the caller writes the
document in two passes, which reports that error
*/
template < typename SinkType = OutputSinkType,
typename std::enable_if < is_detected<sink_written_t, SinkType>::value, int >::type = 0 >
bool write_bson_document_single_pass(const BasicJsonType& document)
{
const std::size_t start = oa.written();
// the object or array whose entries are being written (with the
// position of its size), and the ones it is in
bson_frame current(&document, write_bson_size_placeholder());
std::vector<bson_frame> parents;
while (true)
{
const BasicJsonType* nested = nullptr;
std::size_t nested_size_pos = 0;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
const std::size_t header_size = calc_bson_entry_header_size(el.first, el.second);
const std::size_t value_size = el.second.is_structured() ? 0 : check_bson_value(el.second);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, header_size + value_size, parents.size() + 1)))
{
return false;
}
if (el.second.is_structured())
{
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el.second;
}
else
{
write_bson_value(el.first, el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
if (is_bson_fixed_size(array[current.index]))
{
write_bson_array_fixed_entries(current);
if (current.index == array.size())
{
break;
}
}
// a string, binary value, object or array
const BasicJsonType& el = array[current.index];
const string_t index_name(current.index_name.data(), current.index_name_size);
current.next_index();
const std::size_t value_size = el.is_structured() ? 0 : check_bson_value(el);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, 2 + index_name.size() + value_size, parents.size() + 1)))
{
return false;
}
if (el.is_structured())
{
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el;
}
else
{
write_bson_value(index_name, el);
}
}
}
if (nested != nullptr)
{
if (nested->is_array())
{
// an array of booleans, numbers and null is written without a frame on the stack
bson_frame leaf(nested, nested_size_pos);
write_bson_array_fixed_entries(leaf);
if (leaf.index == nested->m_data.m_value.array->size())
{
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos)))
{
return false;
}
continue;
}
parents.push_back(std::move(current));
current = std::move(leaf);
continue;
}
parents.push_back(std::move(current));
current = bson_frame(nested, nested_size_pos);
continue;
}
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot)))
{
return false;
}
if (parents.empty())
{
return true;
}
current = std::move(parents.back());
parents.pop_back();
}
}
/*! /*!
@brief Serializes the JSON object @a document as a BSON document @brief Serializes the JSON object @a document as a BSON document
@@ -2350,6 +2542,39 @@ class binary_writer
@pre document.type() == value_t::object @pre document.type() == value_t::object
*/ */
void write_bson_document(const BasicJsonType& document) void write_bson_document(const BasicJsonType& document)
{
write_bson_document(document, bson_single_pass_sink());
}
/// the sink can overwrite: one pass, unless the error handler replaces or
/// drops ill-formed UTF-8, which changes the sizes the checks compute
void write_bson_document(const BasicJsonType& document, std::true_type /*single_pass*/)
{
const std::size_t start = oa.written();
if ((error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)
&& write_bson_document_single_pass(document))
{
return;
}
// a length does not fit: start over, so that it is reported as the
// two passes report it, before anything is written
oa.rewind(start);
write_bson_document_two_pass(document);
}
void write_bson_document(const BasicJsonType& document, std::false_type /*single_pass*/)
{
write_bson_document_two_pass(document);
}
/*!
@brief Serializes the JSON object @a document as a BSON document: the
sizes of all objects and arrays first, then the entries
@param[in] document the JSON object to serialize
@pre document.type() == value_t::object
*/
void write_bson_document_two_pass(const BasicJsonType& document)
{ {
std::vector<std::size_t> nested_sizes; std::vector<std::size_t> nested_sizes;
const std::size_t document_size = calc_bson_sizes(document, nested_sizes); const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
@@ -227,6 +227,29 @@ class output_vector_sink
void flush() noexcept void flush() noexcept
{} {}
/// the number of characters written so far, the position of the next one
std::size_t written() const noexcept
{
return position();
}
/// @brief replace @a length characters written before, from position @a pos
///
/// Lets a writer put a length in front of data it has written since, such
/// as the size of a BSON document, without computing it in advance.
void overwrite(const std::size_t pos, const CharType* s, const std::size_t length) noexcept
{
JSON_ASSERT(pos + length <= position());
std::memcpy(v->data() + pos, s, length * sizeof(CharType));
}
/// drop the characters written from position @a pos on
void rewind(const std::size_t pos) noexcept
{
JSON_ASSERT(pos <= position());
cursor = v->data() + pos;
}
private: private:
/// number of characters of *v written so far /// number of characters of *v written so far
std::size_t position() const noexcept std::size_t position() const noexcept
+5 -6
View File
@@ -150,7 +150,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// can be restored when json_pointer backwards compatibility is removed // can be restored when json_pointer backwards compatibility is removed
// friend ::nlohmann::json_pointer<StringType>; // friend ::nlohmann::json_pointer<StringType>;
template<typename BasicJsonType, typename InputType, bool DiscardValues> template<typename BasicJsonType, typename InputType>
friend class ::nlohmann::detail::parser; friend class ::nlohmann::detail::parser;
friend ::nlohmann::detail::serializer<basic_json>; friend ::nlohmann::detail::serializer<basic_json>;
template<typename BasicJsonType> template<typename BasicJsonType>
@@ -183,11 +183,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::parser_callback_t<basic_json>cb = nullptr, detail::parser_callback_t<basic_json>cb = nullptr,
const bool allow_exceptions = true, const bool allow_exceptions = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas = false const bool ignore_trailing_commas = false,
const bool discard_number_values = false
) )
{ {
return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter), return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter),
std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas, discard_number_values);
} }
private: private:
@@ -3116,9 +3117,7 @@ public:
const bool ignore_comments, const bool ignore_comments,
const bool ignore_trailing_commas) const bool ignore_trailing_commas)
{ {
// the parser and lexer that discard the values of tokens (see lexer) return parser(std::forward<InputAdapterType>(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true);
return ::nlohmann::detail::parser<basic_json, typename std::decay<InputAdapterType>::type, true>(std::forward<InputAdapterType>(adapter),
nullptr, false, ignore_comments, ignore_trailing_commas).accept(true);
} }
/// generate SAX events from the input behind an already created adapter /// generate SAX events from the input behind an already created adapter
+301 -103
View File
@@ -13065,13 +13065,8 @@ constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
@brief lexical analysis @brief lexical analysis
This class organizes the lexical analysis during JSON deserialization. This class organizes the lexical analysis during JSON deserialization.
DiscardValues: the caller only needs the token types and never looks at the values of number and string tokens (only
accept(), which parses through json_sax_acceptor). A template parameter rather than a member, so that the lexer of
parse() is compiled without the paths of accept(): with both string scans in one scan(), parse() was up to 2 % slower
on Apple M1.
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false> template<typename BasicJsonType, typename InputAdapterType>
class lexer : public lexer_base<BasicJsonType> class lexer : public lexer_base<BasicJsonType>
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
@@ -13129,9 +13124,10 @@ class lexer : public lexer_base<BasicJsonType>
public: public:
using token_type = typename lexer_base<BasicJsonType>::token_type; using token_type = typename lexer_base<BasicJsonType>::token_type;
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false) noexcept explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false, bool discard_number_values_ = false) noexcept
: ia(std::move(adapter)) : ia(std::move(adapter))
, ignore_comments(ignore_comments_) , ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{ {
// the token string of an error before the first token is everything // the token string of an error before the first token is everything
// read so far // read so far
@@ -13248,8 +13244,6 @@ class lexer : public lexer_base<BasicJsonType>
checks if it is inside the range. If a violation was detected, set up an checks if it is inside the range. If a violation was detected, set up an
error message and return false. Otherwise, return true. error message and return false. Otherwise, return true.
@param[in] keep whether the bytes are added to token_buffer (false for
accept(), see scan())
@param[in] ranges list of integers; interpreted as list of pairs of @param[in] ranges list of integers; interpreted as list of pairs of
inclusive lower and upper bound, respectively inclusive lower and upper bound, respectively
@@ -13258,18 +13252,17 @@ class lexer : public lexer_base<BasicJsonType>
@return true if and only if no range violation was detected @return true if and only if no range violation was detected
*/ */
template<bool Keep> bool next_byte_in_range(std::initializer_list<char_int_type> ranges)
bool next_byte_in_range(std::integral_constant<bool, Keep> keep, std::initializer_list<char_int_type> ranges)
{ {
JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6); JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
add_string_char(keep, current); add(current);
for (auto range = ranges.begin(); range != ranges.end(); ++range) for (auto range = ranges.begin(); range != ranges.end(); ++range)
{ {
get(); get();
if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions) if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions)
{ {
add_string_char(keep, current); add(current);
} }
else else
{ {
@@ -13282,16 +13275,14 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// contiguous input: bulk-append the run of ordinary characters and complete /// contiguous input: bulk-append the run of ordinary characters and complete
/// well-formed UTF-8 sequences starting at the current read position (only /// well-formed UTF-8 sequences starting at the current read position, then
/// skip it if @a keep is false), then
/// read the first byte that needs individual handling (the closing quote, an /// read the first byte that needs individual handling (the closing quote, an
/// escape, a control character, or an ill-formed UTF-8 byte) like get(). /// escape, a control character, or an ill-formed UTF-8 byte) like get().
/// That byte is taken from the buffer that was just scanned. Through get(), /// That byte is taken from the buffer that was just scanned. Through get(),
/// Apple Clang merged this read with the other get() calls of the scan /// Apple Clang merged this read with the other get() calls of the scan
/// function, which load the read position again right after it was stored /// function, which load the read position again right after it was stored
/// (3% of json::accept() on jeopardy.json on Apple M1). /// (3% of json::accept() on jeopardy.json on Apple M1).
template<bool Keep> char_int_type get_after_string_run(std::true_type /*bulk*/)
char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep)
{ {
// a pending unget must be consumed through the normal path first // a pending unget must be consumed through the normal path first
if (next_unget) if (next_unget)
@@ -13309,7 +13300,7 @@ class lexer : public lexer_base<BasicJsonType>
const std::size_t pos = lexer_string_bulk_run(data, remaining); const std::size_t pos = lexer_string_bulk_run(data, remaining);
if (pos != 0) if (pos != 0)
{ {
append_string_run(keep, data, pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
bulk_consume(reinterpret_cast<const char*>(data), pos); bulk_consume(reinterpret_cast<const char*>(data), pos);
} }
if (pos == remaining) if (pos == remaining)
@@ -13353,8 +13344,7 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
template<bool Keep> char_int_type get_after_string_run(std::false_type /*bulk*/)
char_int_type get_after_string_run(std::false_type /*bulk*/, std::integral_constant<bool, Keep> /*keep*/)
{ {
return get(); return get();
} }
@@ -13368,18 +13358,13 @@ class lexer : public lexer_base<BasicJsonType>
may contain \0 bytes), and token_buffer.size() is the number of bytes in the may contain \0 bytes), and token_buffer.size() is the number of bytes in the
string. string.
@param[in] keep whether the string is copied into token_buffer; if false
(accept(), see scan()), it is only checked, with the same
errors, and token_buffer stays empty
@return token_type::value_string if string could be successfully scanned, @return token_type::value_string if string could be successfully scanned,
token_type::parse_error otherwise token_type::parse_error otherwise
@note In case of errors, variable error_message contains a textual @note In case of errors, variable error_message contains a textual
description. description.
*/ */
template<bool Keep> token_type scan_string()
token_type scan_string(std::integral_constant<bool, Keep> keep)
{ {
// reset token_buffer (ignore opening quote) // reset token_buffer (ignore opening quote)
reset(); reset();
@@ -13391,7 +13376,7 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// bulk-consume ordinary characters from contiguous input, then // bulk-consume ordinary characters from contiguous input, then
// get the next character, the one that ends the run // get the next character, the one that ends the run
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>(), keep)) switch (get_after_string_run(std::integral_constant<bool, bulk_scan>()))
{ {
// end of file while parsing the string // end of file while parsing the string
case char_traits<char_type>::eof(): case char_traits<char_type>::eof():
@@ -13413,35 +13398,35 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// quotation mark // quotation mark
case '\"': case '\"':
add_string_char(keep, '\"'); add('\"');
break; break;
// reverse solidus // reverse solidus
case '\\': case '\\':
add_string_char(keep, '\\'); add('\\');
break; break;
// solidus // solidus
case '/': case '/':
add_string_char(keep, '/'); add('/');
break; break;
// backspace // backspace
case 'b': case 'b':
add_string_char(keep, '\b'); add('\b');
break; break;
// form feed // form feed
case 'f': case 'f':
add_string_char(keep, '\f'); add('\f');
break; break;
// line feed // line feed
case 'n': case 'n':
add_string_char(keep, '\n'); add('\n');
break; break;
// carriage return // carriage return
case 'r': case 'r':
add_string_char(keep, '\r'); add('\r');
break; break;
// tab // tab
case 't': case 't':
add_string_char(keep, '\t'); add('\t');
break; break;
// unicode escapes // unicode escapes
@@ -13509,7 +13494,10 @@ class lexer : public lexer_base<BasicJsonType>
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF); JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
// translate codepoint into bytes // translate codepoint into bytes
add_codepoint(keep, codepoint); encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
break; break;
} }
@@ -13692,7 +13680,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0x7E: case 0x7E:
case 0x7F: case 0x7F:
{ {
add_string_char(keep, current); add(current);
break; break;
} }
@@ -13728,7 +13716,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xDE: case 0xDE:
case 0xDF: case 0xDF:
{ {
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range(keep, {0x80, 0xBF}))) if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF})))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13738,7 +13726,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+0800..U+0FFF: bytes E0 A0..BF 80..BF // U+0800..U+0FFF: bytes E0 A0..BF 80..BF
case 0xE0: case 0xE0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0xA0, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13762,7 +13750,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xEE: case 0xEE:
case 0xEF: case 0xEF:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13772,7 +13760,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+D000..U+D7FF: bytes ED 80..9F 80..BF // U+D000..U+D7FF: bytes ED 80..9F 80..BF
case 0xED: case 0xED:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x9F, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13782,7 +13770,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+10000..U+3FFFF F0 90..BF 80..BF 80..BF // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
case 0xF0: case 0xF0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13794,7 +13782,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xF2: case 0xF2:
case 0xF3: case 0xF3:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -13804,7 +13792,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+100000..U+10FFFF F4 80..8F 80..BF 80..BF // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
case 0xF4: case 0xF4:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -14295,7 +14283,7 @@ scan_number_done:
token_type convert_number(token_type number_type, std::size_t mantissa_end) token_type convert_number(token_type number_type, std::size_t mantissa_end)
{ {
// accept() only needs to know whether the input is valid, so it sets // accept() only needs to know whether the input is valid, so it sets
// discard_values (see json.hpp), and an integer token whose // discard_number_values (see json.hpp), and an integer token whose
// digit count shows that it fits is reported without calling // digit count shows that it fits is reported without calling
// convert_integer(). A number with up to 18 digits always fits into // convert_integer(). A number with up to 18 digits always fits into
// both std::uint64_t and std::int64_t (18 nines is about 1e18, below // both std::uint64_t and std::int64_t (18 nines is about 1e18, below
@@ -14310,7 +14298,7 @@ scan_number_done:
// json_sax_acceptor, whose number callbacks discard their argument and // json_sax_acceptor, whose number callbacks discard their argument and
// return true, and the parser rejects neither integers nor finite // return true, and the parser rejects neither integers nor finite
// floats. value_unsigned/value_integer are left unset here, so a caller // floats. value_unsigned/value_integer are left unset here, so a caller
// that reads the converted value must not set discard_values. // that reads the converted value must not set discard_number_values.
// //
// On contiguous input, scan_number_bulk_contiguous() converts integer // On contiguous input, scan_number_bulk_contiguous() converts integer
// tokens itself and does not pass them to this function, unless // tokens itself and does not pass them to this function, unless
@@ -14318,7 +14306,7 @@ scan_number_done:
// reached for input without bulk access (e.g. streams), with // reached for input without bulk access (e.g. streams), with
// JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous() // JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous()
// falls back to scan_number(). // falls back to scan_number().
if (discard_values()) if (discard_number_values)
{ {
constexpr std::size_t safe_digit_count = 18; constexpr std::size_t safe_digit_count = 18;
if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count) if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count)
@@ -14569,7 +14557,7 @@ scan_number_done:
// w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a // w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a
// value that underflows is a finite number as well. Only a token that // value that underflows is a finite number as well. Only a token that
// may overflow has to be converted to find out. // may overflow has to be converted to find out.
if (JSON_HEDLEY_UNLIKELY(discard_values() && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10)) if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
{ {
value_float = 0; value_float = 0;
return true; return true;
@@ -14901,34 +14889,6 @@ scan_number_done:
token_buffer.push_back(static_cast<typename string_t::value_type>(c)); token_buffer.push_back(static_cast<typename string_t::value_type>(c));
} }
/// add a character of a string token; accept() checks the string but does
/// not keep its value (@a keep is false)
void add_string_char(std::true_type /*keep*/, char_int_type c)
{
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
}
static void add_string_char(std::false_type /*keep*/, char_int_type /*c*/) noexcept {}
/// add the UTF-8 bytes of a code point from a \\u escape to a string token
void add_codepoint(std::true_type /*keep*/, int codepoint)
{
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
}
static void add_codepoint(std::false_type /*keep*/, int /*codepoint*/) noexcept {}
/// append a run of ordinary string characters (see get_after_string_run())
void append_string_run(std::true_type /*keep*/, const unsigned char* data, std::size_t n)
{
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), n);
}
static void append_string_run(std::false_type /*keep*/, const unsigned char* /*data*/, std::size_t /*n*/) noexcept {}
public: public:
///////////////////// /////////////////////
// value getters // value getters
@@ -15323,9 +15283,7 @@ scan_number_done:
// string // string
case '\"': case '\"':
// accept() (DiscardValues) checks strings without copying them return scan_string();
// into token_buffer
return scan_string(keep_strings {});
// number // number
case '-': case '-':
@@ -15410,19 +15368,11 @@ scan_number_done:
std::size_t decimal_point_position = std::string::npos; std::size_t decimal_point_position = std::string::npos;
/// whether the caller only needs the token types and never looks at the /// whether the caller only needs the token types and never looks at the
/// values of number and string tokens; DiscardValues, set only by accept(), which parses /// converted numeric values; set only by accept(), which parses through
/// through json_sax_acceptor. When set, convert_number() skips converting /// json_sax_acceptor. When set, convert_number() skips converting integer
/// integer tokens whose digit count guarantees that they fit into 64 bits /// tokens whose digit count guarantees that they fit into 64 bits (see
/// (see there), floats are converted only when they may overflow (see /// there)
/// convert_accumulated_float()), and strings are checked without being const bool discard_number_values = false;
/// copied into token_buffer (see scan())
static bool discard_values() noexcept
{
return DiscardValues;
}
/// the tag of scan_string(): whether string tokens are kept
using keep_strings = typename std::conditional<DiscardValues, std::false_type, std::true_type>::type;
}; };
} // namespace detail } // namespace detail
@@ -22614,14 +22564,14 @@ This class implements a parser for JSON text. Nested arrays and objects are trac
stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read
is reported as SAX events. is reported as SAX events.
*/ */
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false> template<typename BasicJsonType, typename InputAdapterType>
class parser class parser
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
using number_unsigned_t = typename BasicJsonType::number_unsigned_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
using number_float_t = typename BasicJsonType::number_float_t; using number_float_t = typename BasicJsonType::number_float_t;
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
using lexer_t = lexer<BasicJsonType, InputAdapterType, DiscardValues>; using lexer_t = lexer<BasicJsonType, InputAdapterType>;
using token_type = typename lexer_t::token_type; using token_type = typename lexer_t::token_type;
public: public:
@@ -22630,9 +22580,10 @@ class parser
parser_callback_t<BasicJsonType> cb = nullptr, parser_callback_t<BasicJsonType> cb = nullptr,
const bool allow_exceptions_ = true, const bool allow_exceptions_ = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas_ = false) const bool ignore_trailing_commas_ = false,
const bool discard_number_values_ = false)
: callback(std::move(cb)) : callback(std::move(cb))
, m_lexer(std::move(adapter), ignore_comments) , m_lexer(std::move(adapter), ignore_comments, discard_number_values_)
, allow_exceptions(allow_exceptions_) , allow_exceptions(allow_exceptions_)
, ignore_trailing_commas(ignore_trailing_commas_) , ignore_trailing_commas(ignore_trailing_commas_)
{ {
@@ -26919,6 +26870,29 @@ class output_vector_sink
void flush() noexcept void flush() noexcept
{} {}
/// the number of characters written so far, the position of the next one
std::size_t written() const noexcept
{
return position();
}
/// @brief replace @a length characters written before, from position @a pos
///
/// Lets a writer put a length in front of data it has written since, such
/// as the size of a BSON document, without computing it in advance.
void overwrite(const std::size_t pos, const CharType* s, const std::size_t length) noexcept
{
JSON_ASSERT(pos + length <= position());
std::memcpy(v->data() + pos, s, length * sizeof(CharType));
}
/// drop the characters written from position @a pos on
void rewind(const std::size_t pos) noexcept
{
JSON_ASSERT(pos <= position());
cursor = v->data() + pos;
}
private: private:
/// number of characters of *v written so far /// number of characters of *v written so far
std::size_t position() const noexcept std::size_t position() const noexcept
@@ -29363,7 +29337,8 @@ class binary_writer
/// arrays: the first index with one more digit; only maintained by /// arrays: the first index with one more digit; only maintained by
/// @ref calc_bson_sizes /// @ref calc_bson_sizes
std::uint64_t index_boundary = 10; std::uint64_t index_boundary = 10;
/// @ref calc_bson_sizes only: where its size goes in the table /// @ref calc_bson_sizes: where its size goes in the table; @ref
/// write_bson_document_single_pass: the position of its size field
std::size_t size_slot; std::size_t size_slot;
/// @ref calc_bson_sizes only: the size of its entries seen so far /// @ref calc_bson_sizes only: the size of its entries seen so far
std::size_t entries_size = 0; std::size_t entries_size = 0;
@@ -29614,6 +29589,197 @@ class binary_writer
} }
} }
template<typename SinkType>
using sink_written_t = decltype(std::declval<const SinkType&>().written());
/*!
@brief whether BSON documents can be written in a single pass: the sink
can overwrite what it has written (output_vector_sink), and every
integer fits the 64 bits BSON has, so that writing an array of
numbers needs no range check
*/
using bson_single_pass_sink = std::integral_constant < bool,
is_detected<sink_written_t, OutputSinkType>::value
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::digits <= std::numeric_limits<std::int64_t>::digits
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::digits <= std::numeric_limits<std::uint64_t>::digits >;
/// write a placeholder for the size of an object or array whose entries
/// follow; @return its position, for @ref patch_bson_size
std::size_t write_bson_size_placeholder()
{
const std::size_t pos = oa.written();
write_number<std::int32_t>(0, true);
return pos;
}
/// end the object or array whose size is at @a pos and write its size
/// there: everything from that field to the terminating null byte
/// @return false (and the size is not written) if it does not fit
bool close_bson_container(const std::size_t pos)
{
oa.write_character(to_char_type(0x00));
const std::size_t size = oa.written() - pos;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
{
return false;
}
std::array<CharType, sizeof(std::int32_t)> bytes{};
store_little_endian<std::int32_t>(bytes.data(), static_cast<std::int32_t>(size));
oa.overwrite(pos, bytes.data(), bytes.size());
return true;
}
/*!
@brief check the value @a j of a BSON entry as @ref calc_bson_value_size
does, for @ref write_bson_document_single_pass
@return at least the size of the value
A number, boolean, or null needs no check there (its integers have at most
64 bits) and has at most 8 characters; a discarded value is rejected with
the same exception when it is written.
*/
std::size_t check_bson_value(const BasicJsonType& j)
{
return (j.is_string() || j.is_binary()) ? calc_bson_value_size(j) : 8;
}
/// whether an entry of @a entry_size characters keeps the document that
/// begins at @a start, closed by @a open_containers null bytes, within
/// the 32-bit size of BSON
bool bson_entry_fits(const std::size_t start, const std::size_t entry_size, const std::size_t open_containers) const noexcept
{
const auto limit = static_cast<std::size_t>((std::numeric_limits<std::int32_t>::max)());
const std::size_t written = oa.written() - start;
return written <= limit && entry_size <= limit - written && open_containers <= limit - written - entry_size;
}
/*!
@brief Serializes the JSON object @a document as a BSON document in a
single pass, writing the size of each object and array once it is
complete
Checks every key and value as @ref calc_bson_sizes does, in the same
order, so the same exception is thrown; the output written up to then is
discarded with the vector it was written to.
@return false as soon as the document or a string, binary value, object,
or array in it would be larger than the 32-bit BSON sizes allow;
nothing large has been written then, and the caller writes the
document in two passes, which reports that error
*/
template < typename SinkType = OutputSinkType,
typename std::enable_if < is_detected<sink_written_t, SinkType>::value, int >::type = 0 >
bool write_bson_document_single_pass(const BasicJsonType& document)
{
const std::size_t start = oa.written();
// the object or array whose entries are being written (with the
// position of its size), and the ones it is in
bson_frame current(&document, write_bson_size_placeholder());
std::vector<bson_frame> parents;
while (true)
{
const BasicJsonType* nested = nullptr;
std::size_t nested_size_pos = 0;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
const std::size_t header_size = calc_bson_entry_header_size(el.first, el.second);
const std::size_t value_size = el.second.is_structured() ? 0 : check_bson_value(el.second);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, header_size + value_size, parents.size() + 1)))
{
return false;
}
if (el.second.is_structured())
{
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el.second;
}
else
{
write_bson_value(el.first, el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
if (is_bson_fixed_size(array[current.index]))
{
write_bson_array_fixed_entries(current);
if (current.index == array.size())
{
break;
}
}
// a string, binary value, object or array
const BasicJsonType& el = array[current.index];
const string_t index_name(current.index_name.data(), current.index_name_size);
current.next_index();
const std::size_t value_size = el.is_structured() ? 0 : check_bson_value(el);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, 2 + index_name.size() + value_size, parents.size() + 1)))
{
return false;
}
if (el.is_structured())
{
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el;
}
else
{
write_bson_value(index_name, el);
}
}
}
if (nested != nullptr)
{
if (nested->is_array())
{
// an array of booleans, numbers and null is written without a frame on the stack
bson_frame leaf(nested, nested_size_pos);
write_bson_array_fixed_entries(leaf);
if (leaf.index == nested->m_data.m_value.array->size())
{
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos)))
{
return false;
}
continue;
}
parents.push_back(std::move(current));
current = std::move(leaf);
continue;
}
parents.push_back(std::move(current));
current = bson_frame(nested, nested_size_pos);
continue;
}
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot)))
{
return false;
}
if (parents.empty())
{
return true;
}
current = std::move(parents.back());
parents.pop_back();
}
}
/*! /*!
@brief Serializes the JSON object @a document as a BSON document @brief Serializes the JSON object @a document as a BSON document
@@ -29625,6 +29791,39 @@ class binary_writer
@pre document.type() == value_t::object @pre document.type() == value_t::object
*/ */
void write_bson_document(const BasicJsonType& document) void write_bson_document(const BasicJsonType& document)
{
write_bson_document(document, bson_single_pass_sink());
}
/// the sink can overwrite: one pass, unless the error handler replaces or
/// drops ill-formed UTF-8, which changes the sizes the checks compute
void write_bson_document(const BasicJsonType& document, std::true_type /*single_pass*/)
{
const std::size_t start = oa.written();
if ((error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)
&& write_bson_document_single_pass(document))
{
return;
}
// a length does not fit: start over, so that it is reported as the
// two passes report it, before anything is written
oa.rewind(start);
write_bson_document_two_pass(document);
}
void write_bson_document(const BasicJsonType& document, std::false_type /*single_pass*/)
{
write_bson_document_two_pass(document);
}
/*!
@brief Serializes the JSON object @a document as a BSON document: the
sizes of all objects and arrays first, then the entries
@param[in] document the JSON object to serialize
@pre document.type() == value_t::object
*/
void write_bson_document_two_pass(const BasicJsonType& document)
{ {
std::vector<std::size_t> nested_sizes; std::vector<std::size_t> nested_sizes;
const std::size_t document_size = calc_bson_sizes(document, nested_sizes); const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
@@ -36995,7 +37194,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// can be restored when json_pointer backwards compatibility is removed // can be restored when json_pointer backwards compatibility is removed
// friend ::nlohmann::json_pointer<StringType>; // friend ::nlohmann::json_pointer<StringType>;
template<typename BasicJsonType, typename InputType, bool DiscardValues> template<typename BasicJsonType, typename InputType>
friend class ::nlohmann::detail::parser; friend class ::nlohmann::detail::parser;
friend ::nlohmann::detail::serializer<basic_json>; friend ::nlohmann::detail::serializer<basic_json>;
template<typename BasicJsonType> template<typename BasicJsonType>
@@ -37028,11 +37227,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::parser_callback_t<basic_json>cb = nullptr, detail::parser_callback_t<basic_json>cb = nullptr,
const bool allow_exceptions = true, const bool allow_exceptions = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas = false const bool ignore_trailing_commas = false,
const bool discard_number_values = false
) )
{ {
return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter), return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter),
std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas, discard_number_values);
} }
private: private:
@@ -39961,9 +40161,7 @@ public:
const bool ignore_comments, const bool ignore_comments,
const bool ignore_trailing_commas) const bool ignore_trailing_commas)
{ {
// the parser and lexer that discard the values of tokens (see lexer) return parser(std::forward<InputAdapterType>(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true);
return ::nlohmann::detail::parser<basic_json, typename std::decay<InputAdapterType>::type, true>(std::forward<InputAdapterType>(adapter),
nullptr, false, ignore_comments, ignore_trailing_commas).accept(true);
} }
/// generate SAX events from the input behind an already created adapter /// generate SAX events from the input behind an already created adapter
+151
View File
@@ -19,6 +19,7 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <array>
#include <cstdint> #include <cstdint>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
@@ -2267,3 +2268,153 @@ TEST_CASE("BSON large strings and binaries (chunked reader)")
} }
} }
} }
TEST_CASE("BSON written in one pass and in two passes")
{
// to_bson() into a returned vector writes each size once the object or
// array is complete; through an output adapter, all sizes are computed
// first. Both must produce the same bytes and throw the same exceptions.
const auto two_pass = [](const json & j, const json::error_handler_t handler)
{
std::vector<std::uint8_t> result;
json::to_bson(j, result, handler);
return result;
};
const auto check_same = [&](const json & j, const json::error_handler_t handler)
{
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
CAPTURE(static_cast<int>(handler))
#if !defined(JSON_NOEXCEPTION)
std::string one_pass_error;
std::string two_pass_error;
std::vector<std::uint8_t> one;
std::vector<std::uint8_t> two;
try
{
one = json::to_bson(j, handler);
}
catch (const json::exception& e)
{
one_pass_error = e.what();
}
try
{
two = two_pass(j, handler);
}
catch (const json::exception& e)
{
two_pass_error = e.what();
}
CHECK(one_pass_error == two_pass_error);
CHECK(one == two);
#else
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
#endif
};
const std::array<json::error_handler_t, 4> handlers = {{
json::error_handler_t::keep, json::error_handler_t::strict,
json::error_handler_t::replace, json::error_handler_t::ignore
}
};
SECTION("corpus")
{
for (const auto& value : utils::round_trip_corpus::values())
{
const json document = value.is_object() ? value : json{{"value", value}};
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
SECTION("nesting and arrays of every length class")
{
json document = json::object();
document["empty object"] = json::object();
document["empty array"] = json::array();
for (const std::size_t n :
{
1u, 9u, 10u, 11u, 99u, 100u, 101u, 1000u
})
{
json numbers = json::array();
json mixed = json::array();
for (std::size_t i = 0; i < n; ++i)
{
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
switch (i % 4)
{
case 0:
mixed.push_back(json("s" + std::to_string(i)));
break;
case 1:
mixed.push_back(json::array({i, json::object({{"k", i}})}));
break;
case 2:
mixed.push_back(json(nullptr));
break;
default:
mixed.push_back(json(i % 2 == 0));
break;
}
}
document["numbers " + std::to_string(n)] = numbers;
document["mixed " + std::to_string(n)] = mixed;
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
}
document["binary"] = json::binary({1, 2, 3}, 0x80);
document["binary old"] = json::binary({1, 2, 3}, 0x02);
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
#if !defined(JSON_NOEXCEPTION)
// every entry here throws, which aborts without exceptions
SECTION("errors at every position")
{
// an entry that cannot be written, at each place among valid ones
const std::string bad_utf8 = "\xC3\x28";
const std::array<json, 7> bad_entries =
{
{
json::object({{std::string("a\0b", 3), 1}}),
json::object({{"s", bad_utf8}}),
json::object({{bad_utf8, 1}}),
json::array({1, 2, bad_utf8}),
json::object({{"b", json::binary({1}, 0x100)}}),
json::object({{"d", json(json::value_t::discarded)}}),
json::array({json(json::value_t::discarded)}),
}
};
for (const auto& bad : bad_entries)
{
for (std::size_t pos = 0; pos < 4; ++pos)
{
json document = json::object();
json list = json::array();
for (std::size_t i = 0; i < 4; ++i)
{
if (i == pos)
{
document["x" + std::to_string(i)] = bad;
list.push_back(bad);
}
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
list.push_back(i);
}
document["list"] = list;
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
}
#endif
}
-61
View File
@@ -2982,67 +2982,6 @@ TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
} }
} }
TEST_CASE("accept() agrees with parse() on strings, which it checks without copying them")
{
// accept() scans strings and object keys with a separate instantiation of
// the string scanner that does not copy them into the token buffer; it
// must accept and reject exactly what parse() does
const std::vector<std::string> pieces =
{
"", "abc", R"(\"\\\/\b\f\n\r\t)", R"(\u0041\u00e4\u20AC)", "\\ud83d\\ude00", "\\uD83D\\uDE00",
"\\ud800", "\\udc00", "\\ud800\\u0041", "\\ud800x", "\\udbff\\udfff", "\\u12", "\\u12g4", "\\x", "\\",
"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf", "\xc0\xaf", "\xed\xa0\x80", "\xf4\x90\x80\x80",
"\xff", "\xe2\x82", "\xc3", "\x01", "\x1f", "\x7f", "\n", std::string(1, '\0')
};
for (std::size_t run = 0; run <= 70; ++run)
{
for (const std::string& piece : pieces)
{
// a run of ordinary characters puts the piece at every offset of the bulk scanners
const std::string str = std::string(run, 'a') + piece;
std::string opened = "\"";
opened += str;
std::string closed = opened;
closed += '"';
std::vector<std::string> texts;
texts.push_back(closed);
texts.emplace_back("[");
texts.back() += closed;
texts.back() += ']';
texts.emplace_back("{");
texts.back() += closed;
texts.back() += ":1}";
texts.emplace_back("{\"k\":");
texts.back() += closed;
texts.back() += ',';
texts.back() += closed;
texts.back() += ":[";
texts.back() += closed;
texts.back() += "]}";
texts.emplace_back("[");
texts.back() += opened;
for (const std::string& text : texts)
{
CAPTURE(text)
for (int options = 0; options < 4; ++options)
{
const bool ignore_comments = (options & 1) != 0;
const bool ignore_trailing_commas = (options & 2) != 0;
const bool parses = !json::parse(text, nullptr, false, ignore_comments, ignore_trailing_commas).is_discarded();
CHECK(json::accept(text, ignore_comments, ignore_trailing_commas) == parses);
std::istringstream stream(text);
CHECK(json::accept(stream, ignore_comments, ignore_trailing_commas) == parses);
const std::list<char> chars(text.begin(), text.end());
CHECK(json::accept(chars.begin(), chars.end(), ignore_comments, ignore_trailing_commas) == parses);
}
}
}
}
}
namespace namespace
{ {
// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example // a DOM parser that reports parse errors without throwing, as in the sax_no_exception example