Compare commits

...

4 Commits

Author SHA1 Message Date
Niels Lohmann acf076a677 Fix ADL leak of nlohmann::detail through basic_json's default base class (#5238) 2026-07-06 12:47:24 +02:00
Paul Dreik 33edc9751c fix unit-algorithms test reliance on implementation specific behaviour (#5236)
the standard only specifies that the first elements are sorted.

this caused my experimental C++ standard library implementation to fail.

Signed-off-by: Paul Dreik <github@pauldreik.se>
2026-07-06 08:25:30 +02:00
Niels Lohmann 83c87cb9e0 Read binary strings/blobs in bulk chunks with a memcpy fast path (#5233) 2026-07-05 19:26:02 +02:00
Niels Lohmann eed1587000 Reconstruct lexer diagnostics lazily for seekable input (#120) (#5234)
`lexer::get()` copied every scanned character into `token_string` on the
whole successful-parse hot path, yet that buffer is consumed only by
`get_token_string()` when rendering the "last read" fragment of a parse
error. On well-formed input the per-byte copy (plus the `unget()` pop)
is pure overhead that is always discarded.

For seekable input adapters - random-access, single-byte iterators such
as those backing `std::string`, `const char*`, and `std::vector<char>` -
the offending token is now reconstructed on demand from the input when
an error is reported, using a saved start offset, and the eager copy is
skipped. Streaming adapters (file, istream, wide-string, and user-defined
adapters) keep the eager copy; the strategy is chosen at compile time via
`input_adapter_supports_seek`, so adapters without the capability are
unaffected.

Error messages are byte-for-byte identical across all adapters, verified
by a new parity regression test. Microbenchmark (4 MB mixed JSON, parsed
from a std::string): ~149 -> ~160 MB/s, about +8%.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 10:42:48 +02:00
9 changed files with 741 additions and 69 deletions
+60 -23
View File
@@ -12,7 +12,7 @@
#include <array> // array #include <array> // array
#include <cmath> // ldexp #include <cmath> // ldexp
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t #include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcpy #include <cstring> // memcpy
#include <iterator> // back_inserter #include <iterator> // back_inserter
@@ -2908,18 +2908,7 @@ class binary_reader
const NumberType len, const NumberType len,
string_t& result) string_t& result)
{ {
bool success = true; return get_bytes(format, len, "string", result);
for (NumberType i = 0; i < len; i++)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string")))
{
success = false;
break;
}
result.push_back(static_cast<typename string_t::value_type>(current));
}
return success;
} }
/*! /*!
@@ -2941,18 +2930,66 @@ class binary_reader
const NumberType len, const NumberType len,
binary_t& result) binary_t& result)
{ {
bool success = true; return get_bytes(format, len, "binary", result);
for (NumberType i = 0; i < len; i++)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
{
success = false;
break;
} }
result.push_back(static_cast<typename binary_t::value_type>(current));
/*!
@brief read @a len bytes from the input into a string or byte container
@tparam NumberType the type of the length
@tparam ContainerType the destination container (string_t or binary_t)
@param[in] format the current format (for diagnostics)
@param[in] len number of bytes to read
@param[in] context further context information (for diagnostics)
@param[out] result container the bytes are appended to
@return whether reading completed
@note We cannot reserve @a len bytes for the result up front, because
@a len may be far larger than the actual input. Instead we read in
bounded chunks, so the peak allocation is capped regardless of the
claimed length while the per-byte loop is replaced by block copies
(a std::memcpy for contiguous inputs). @ref unexpect_eof() still
detects a premature end of input.
*/
template<typename NumberType, typename ContainerType>
bool get_bytes(const input_format_t format,
NumberType len,
const char* context,
ContainerType& result)
{
// upper bound on the number of bytes read (and allocated) per chunk
constexpr std::size_t chunk_size = 4096;
while (len > 0)
{
// number of bytes to read this iteration: min(chunk_size, len),
// computed without truncating chunk_size to a narrow NumberType
const std::size_t wanted = (static_cast<std::uintmax_t>(len) < static_cast<std::uintmax_t>(chunk_size))
? static_cast<std::size_t>(len)
: chunk_size;
const std::size_t old_size = result.size();
result.resize(old_size + wanted);
// resize() is required to make size() exactly old_size + wanted;
// that is the room get_elements() is allowed to write into
JSON_ASSERT(result.size() == old_size + wanted);
const std::size_t bytes_read = ia.get_elements(&result[old_size], wanted);
chars_read += bytes_read;
if (JSON_HEDLEY_UNLIKELY(bytes_read < wanted))
{
// premature end of input: shrink to what was actually read and
// report the failure at the first missing byte (same position
// accounting as get_to() for partial number reads)
result.resize(old_size + bytes_read);
++chars_read;
current = char_traits<char_type>::eof();
return unexpect_eof(format, context);
} }
return success; // a full chunk was read; get_elements() never returns more than requested
JSON_ASSERT(bytes_read == wanted);
len = static_cast<NumberType>(len - static_cast<NumberType>(wanted));
}
return true;
} }
/*! /*!
@@ -161,8 +161,18 @@ class iterator_input_adapter
public: public:
using char_type = typename std::iterator_traits<IteratorType>::value_type; using char_type = typename std::iterator_traits<IteratorType>::value_type;
// Whether the lexer may reconstruct already-consumed input on demand (for
// diagnostics) instead of copying every scanned character eagerly. This is
// only sound for multi-pass, randomly-addressable byte input: the iterator
// must be random-access (so the consumed prefix can be revisited in O(1))
// and each element must map 1:1 to an input byte (wide inputs are wrapped
// in wide_string_input_adapter, which does not expose this).
static constexpr bool supports_seek =
std::is_same<typename std::iterator_traits<IteratorType>::iterator_category, std::random_access_iterator_tag>::value
&& sizeof(char_type) == 1;
iterator_input_adapter(IteratorType first, IteratorType last) iterator_input_adapter(IteratorType first, IteratorType last)
: current(std::move(first)), end(std::move(last)) : begin(first), current(std::move(first)), end(std::move(last))
{} {}
typename char_traits<char_type>::int_type get_character() typename char_traits<char_type>::int_type get_character()
@@ -177,9 +187,67 @@ class iterator_input_adapter
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one // number of characters consumed from the input so far
std::size_t get_consumed_count() const
{
return static_cast<std::size_t>(std::distance(begin, current));
}
// append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType>
void copy_consumed_range(std::size_t first_index, std::size_t last_index, ContainerType& out) const
{
const auto from = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(first_index));
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
out.insert(out.end(), from, to);
}
// Copy up to count * sizeof(T) bytes into dest, returning the number of
// bytes actually read. For contiguous iterators (e.g. pointers) this is a
// single std::memcpy; for general iterators we fall back to processing the
// range one-by-one.
template<class T> template<class T>
std::size_t get_elements(T* dest, std::size_t count = 1) std::size_t get_elements(T* dest, std::size_t count = 1)
{
return get_elements_impl(dest, count, std::integral_constant<bool, iterator_is_contiguous> {});
}
private:
// whether IteratorType refers to a contiguous range and therefore supports
// a std::memcpy fast path (pointers always do; in C++20 we can also detect
// library iterators such as those of std::vector and std::string)
static constexpr bool iterator_is_contiguous =
#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20)
std::contiguous_iterator<IteratorType> ||
#endif
std::is_pointer<IteratorType>::value;
// contiguous fast path: bulk copy the remaining range with std::memcpy
template<class T>
std::size_t get_elements_impl(T* dest, std::size_t count, std::true_type /*contiguous*/)
{
const std::size_t wanted = count * sizeof(T);
const std::size_t available = static_cast<std::size_t>(std::distance(current, end)) * sizeof(char_type);
const std::size_t copied = (std::min)(wanted, available);
if (JSON_HEDLEY_LIKELY(copied != 0))
{
// the copy must stay within both buffers: the caller-provided
// destination holds `wanted` bytes and the remaining input range
// holds `available` bytes, and `copied` is the minimum of the two
JSON_ASSERT(copied <= wanted); // does not overrun the destination
JSON_ASSERT(copied <= available); // does not read past the input end
// &*current yields the raw address for both raw pointers and
// non-pointer contiguous iterators (e.g. std::vector's iterator)
std::memcpy(dest, &*current, copied);
std::advance(current, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(copied / sizeof(char_type)));
}
return copied;
}
// general fallback: copy the range one element at a time
template<class T>
std::size_t get_elements_impl(T* dest, std::size_t count, std::false_type /*contiguous*/)
{ {
auto* ptr = reinterpret_cast<char*>(dest); auto* ptr = reinterpret_cast<char*>(dest);
for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index) for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index)
@@ -197,7 +265,7 @@ class iterator_input_adapter
return count * sizeof(T); return count * sizeof(T);
} }
private: IteratorType begin;
IteratorType current; IteratorType current;
IteratorType end; IteratorType end;
+100 -7
View File
@@ -103,6 +103,28 @@ class lexer_base
} }
} }
}; };
// Detect whether an input adapter can reconstruct already-consumed input on
// demand (see iterator_input_adapter::supports_seek). Adapters that do not
// expose the flag - e.g. file, stream, wide-string, and user-defined adapters -
// are treated as non-seekable streaming input, for which the lexer keeps
// copying every scanned character eagerly. The value is read via tag dispatch
// on is_detected so the flag is only referenced for adapters that provide it.
template<typename InputAdapterType>
using detect_supports_seek = decltype(InputAdapterType::supports_seek);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_seek(std::true_type /*detected*/)
{
return InputAdapterType::supports_seek;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_seek(std::false_type /*detected*/)
{
return false;
}
/*! /*!
@brief lexical analysis @brief lexical analysis
@@ -118,6 +140,12 @@ class lexer : public lexer_base<BasicJsonType>
using char_type = typename InputAdapterType::char_type; using char_type = typename InputAdapterType::char_type;
using char_int_type = typename char_traits<char_type>::int_type; using char_int_type = typename char_traits<char_type>::int_type;
/// whether the last read token can be reconstructed from the input adapter
/// on demand (in error paths) instead of being copied on every scanned
/// character; see input_adapter_supports_seek
static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
public: public:
using token_type = typename lexer_base<BasicJsonType>::token_type; using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -1327,8 +1355,24 @@ scan_number_done:
void reset() noexcept void reset() noexcept
{ {
token_buffer.clear(); token_buffer.clear();
token_string.clear();
decimal_point_position = std::string::npos; decimal_point_position = std::string::npos;
note_token_start(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: remember where the current token starts so it can be
/// reconstructed from the input on error; current has already been
/// consumed, hence the -1
void note_token_start(std::true_type /*lazy*/) noexcept
{
token_string_start = ia.get_consumed_count() - 1;
}
/// streaming adapter: start copying the token eagerly, beginning with the
/// already-read first character
void note_token_start(std::false_type /*lazy*/) noexcept
{
token_string.clear();
token_string.push_back(char_traits<char_type>::to_char_type(current)); token_string.push_back(char_traits<char_type>::to_char_type(current));
} }
@@ -1357,10 +1401,9 @@ scan_number_done:
current = ia.get_character(); current = ia.get_character();
} }
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof())) // seekable adapters reconstruct the token lazily on error (see
{ // get_token_string), so the eager per-character copy is skipped
token_string.push_back(char_traits<char_type>::to_char_type(current)); capture_char(std::integral_constant<bool, lazy_token_string> {});
}
if (current == '\n') if (current == '\n')
{ {
@@ -1371,6 +1414,18 @@ scan_number_done:
return current; return current;
} }
/// seekable adapter: nothing to capture, the token is rebuilt on error
void capture_char(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: copy the scanned character into token_string
void capture_char(std::false_type /*lazy*/)
{
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
{
token_string.push_back(char_traits<char_type>::to_char_type(current));
}
}
/*! /*!
@brief unget current character (read it again on next get) @brief unget current character (read it again on next get)
@@ -1398,6 +1453,15 @@ scan_number_done:
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: nothing was captured, so nothing to undo
void uncapture_char(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: drop the character copied by the matching get()
void uncapture_char(std::false_type /*lazy*/)
{
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof())) if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
{ {
JSON_ASSERT(!token_string.empty()); JSON_ASSERT(!token_string.empty());
@@ -1455,14 +1519,38 @@ scan_number_done:
return position; return position;
} }
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
// a pending unget of a real (non-EOF) character means that character
// was consumed from the input but is not part of the token; EOF is
// never consumed, so it must not be subtracted (mirrors unget())
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
const std::size_t stop = ia.get_consumed_count() - (pending_real_unget ? 1u : 0u);
if (JSON_HEDLEY_LIKELY(stop >= token_string_start))
{
ia.copy_consumed_range(token_string_start, stop, out);
}
return out;
}
/// streaming adapter: the token was copied eagerly while scanning
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& /*out*/, std::false_type /*lazy*/) const
{
return token_string;
}
/// return the last read token (for errors only). Will never contain EOF /// return the last read token (for errors only). Will never contain EOF
/// (an arbitrary value that is not a valid char value, often -1), because /// (an arbitrary value that is not a valid char value, often -1), because
/// 255 may legitimately occur. May contain NUL, which should be escaped. /// 255 may legitimately occur. May contain NUL, which should be escaped.
std::string get_token_string() const std::string get_token_string() const
{ {
std::vector<char_type> reconstructed;
const std::vector<char_type>& chars = collect_token_chars(reconstructed, std::integral_constant<bool, lazy_token_string> {});
// escape control characters // escape control characters
std::string result; std::string result;
for (const auto c : token_string) for (const auto c : chars)
{ {
if (static_cast<unsigned char>(c) <= '\x1F') if (static_cast<unsigned char>(c) <= '\x1F')
{ {
@@ -1623,9 +1711,14 @@ scan_number_done:
/// the start position of the current token /// the start position of the current token
position_t position {}; position_t position {};
/// raw input token string (for error messages) /// raw input token string for error messages; only populated for streaming
/// adapters (seekable adapters reconstruct it lazily via token_string_start)
std::vector<char_type> token_string {}; std::vector<char_type> token_string {};
/// start offset of the current token within the input, used to reconstruct
/// the last read token on error for seekable adapters (see collect_token_chars)
std::size_t token_string_start = 0;
/// buffer for variable-length tokens (numbers, strings) /// buffer for variable-length tokens (numbers, strings)
string_t token_buffer {}; string_t token_buffer {};
@@ -13,8 +13,6 @@
#include <nlohmann/detail/abi_macros.hpp> #include <nlohmann/detail/abi_macros.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
/*! /*!
@brief Default base class of the @ref basic_json class. @brief Default base class of the @ref basic_json class.
@@ -25,13 +23,27 @@ of @ref basic_json do not require complex case distinctions
@ref basic_json always has a base class. @ref basic_json always has a base class.
By default, this class is used because it is empty and thus has no effect By default, this class is used because it is empty and thus has no effect
on the behavior of @ref basic_json. on the behavior of @ref basic_json.
@note This class intentionally lives in namespace @ref nlohmann rather than
@ref nlohmann::detail. Every @ref basic_json specialization derives from
it (via @ref detail::json_base_class) unless a custom base class is
supplied, which makes its namespace an associated namespace of
@ref basic_json for the purpose of argument-dependent lookup (ADL). If
it lived in `nlohmann::detail`, that namespace - and with it the
library's internal `to_json`/`from_json` overloads - would leak into
ADL for any unqualified `to_json`/`from_json` call a user makes
involving a @ref basic_json argument, silently shadowing the user's own
overloads in some cases.
*/ */
struct json_default_base {}; struct json_default_base {};
namespace detail
{
template<class T> template<class T>
using json_base_class = typename std::conditional < using json_base_class = typename std::conditional <
std::is_same<T, void>::value, std::is_same<T, void>::value,
json_default_base, ::nlohmann::json_default_base,
T T
>::type; >::type;
+246 -36
View File
@@ -6811,7 +6811,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <array> // array #include <array> // array
#include <cmath> // ldexp #include <cmath> // ldexp
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t #include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcpy #include <cstring> // memcpy
#include <iterator> // back_inserter #include <iterator> // back_inserter
@@ -6993,8 +6993,18 @@ class iterator_input_adapter
public: public:
using char_type = typename std::iterator_traits<IteratorType>::value_type; using char_type = typename std::iterator_traits<IteratorType>::value_type;
// Whether the lexer may reconstruct already-consumed input on demand (for
// diagnostics) instead of copying every scanned character eagerly. This is
// only sound for multi-pass, randomly-addressable byte input: the iterator
// must be random-access (so the consumed prefix can be revisited in O(1))
// and each element must map 1:1 to an input byte (wide inputs are wrapped
// in wide_string_input_adapter, which does not expose this).
static constexpr bool supports_seek =
std::is_same<typename std::iterator_traits<IteratorType>::iterator_category, std::random_access_iterator_tag>::value
&& sizeof(char_type) == 1;
iterator_input_adapter(IteratorType first, IteratorType last) iterator_input_adapter(IteratorType first, IteratorType last)
: current(std::move(first)), end(std::move(last)) : begin(first), current(std::move(first)), end(std::move(last))
{} {}
typename char_traits<char_type>::int_type get_character() typename char_traits<char_type>::int_type get_character()
@@ -7009,9 +7019,67 @@ class iterator_input_adapter
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one // number of characters consumed from the input so far
std::size_t get_consumed_count() const
{
return static_cast<std::size_t>(std::distance(begin, current));
}
// append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType>
void copy_consumed_range(std::size_t first_index, std::size_t last_index, ContainerType& out) const
{
const auto from = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(first_index));
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
out.insert(out.end(), from, to);
}
// Copy up to count * sizeof(T) bytes into dest, returning the number of
// bytes actually read. For contiguous iterators (e.g. pointers) this is a
// single std::memcpy; for general iterators we fall back to processing the
// range one-by-one.
template<class T> template<class T>
std::size_t get_elements(T* dest, std::size_t count = 1) std::size_t get_elements(T* dest, std::size_t count = 1)
{
return get_elements_impl(dest, count, std::integral_constant<bool, iterator_is_contiguous> {});
}
private:
// whether IteratorType refers to a contiguous range and therefore supports
// a std::memcpy fast path (pointers always do; in C++20 we can also detect
// library iterators such as those of std::vector and std::string)
static constexpr bool iterator_is_contiguous =
#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20)
std::contiguous_iterator<IteratorType> ||
#endif
std::is_pointer<IteratorType>::value;
// contiguous fast path: bulk copy the remaining range with std::memcpy
template<class T>
std::size_t get_elements_impl(T* dest, std::size_t count, std::true_type /*contiguous*/)
{
const std::size_t wanted = count * sizeof(T);
const std::size_t available = static_cast<std::size_t>(std::distance(current, end)) * sizeof(char_type);
const std::size_t copied = (std::min)(wanted, available);
if (JSON_HEDLEY_LIKELY(copied != 0))
{
// the copy must stay within both buffers: the caller-provided
// destination holds `wanted` bytes and the remaining input range
// holds `available` bytes, and `copied` is the minimum of the two
JSON_ASSERT(copied <= wanted); // does not overrun the destination
JSON_ASSERT(copied <= available); // does not read past the input end
// &*current yields the raw address for both raw pointers and
// non-pointer contiguous iterators (e.g. std::vector's iterator)
std::memcpy(dest, &*current, copied);
std::advance(current, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(copied / sizeof(char_type)));
}
return copied;
}
// general fallback: copy the range one element at a time
template<class T>
std::size_t get_elements_impl(T* dest, std::size_t count, std::false_type /*contiguous*/)
{ {
auto* ptr = reinterpret_cast<char*>(dest); auto* ptr = reinterpret_cast<char*>(dest);
for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index) for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index)
@@ -7029,7 +7097,7 @@ class iterator_input_adapter
return count * sizeof(T); return count * sizeof(T);
} }
private: IteratorType begin;
IteratorType current; IteratorType current;
IteratorType end; IteratorType end;
@@ -7510,6 +7578,28 @@ class lexer_base
} }
} }
}; };
// Detect whether an input adapter can reconstruct already-consumed input on
// demand (see iterator_input_adapter::supports_seek). Adapters that do not
// expose the flag - e.g. file, stream, wide-string, and user-defined adapters -
// are treated as non-seekable streaming input, for which the lexer keeps
// copying every scanned character eagerly. The value is read via tag dispatch
// on is_detected so the flag is only referenced for adapters that provide it.
template<typename InputAdapterType>
using detect_supports_seek = decltype(InputAdapterType::supports_seek);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_seek(std::true_type /*detected*/)
{
return InputAdapterType::supports_seek;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_seek(std::false_type /*detected*/)
{
return false;
}
/*! /*!
@brief lexical analysis @brief lexical analysis
@@ -7525,6 +7615,12 @@ class lexer : public lexer_base<BasicJsonType>
using char_type = typename InputAdapterType::char_type; using char_type = typename InputAdapterType::char_type;
using char_int_type = typename char_traits<char_type>::int_type; using char_int_type = typename char_traits<char_type>::int_type;
/// whether the last read token can be reconstructed from the input adapter
/// on demand (in error paths) instead of being copied on every scanned
/// character; see input_adapter_supports_seek
static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
public: public:
using token_type = typename lexer_base<BasicJsonType>::token_type; using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -8734,8 +8830,24 @@ scan_number_done:
void reset() noexcept void reset() noexcept
{ {
token_buffer.clear(); token_buffer.clear();
token_string.clear();
decimal_point_position = std::string::npos; decimal_point_position = std::string::npos;
note_token_start(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: remember where the current token starts so it can be
/// reconstructed from the input on error; current has already been
/// consumed, hence the -1
void note_token_start(std::true_type /*lazy*/) noexcept
{
token_string_start = ia.get_consumed_count() - 1;
}
/// streaming adapter: start copying the token eagerly, beginning with the
/// already-read first character
void note_token_start(std::false_type /*lazy*/) noexcept
{
token_string.clear();
token_string.push_back(char_traits<char_type>::to_char_type(current)); token_string.push_back(char_traits<char_type>::to_char_type(current));
} }
@@ -8764,10 +8876,9 @@ scan_number_done:
current = ia.get_character(); current = ia.get_character();
} }
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof())) // seekable adapters reconstruct the token lazily on error (see
{ // get_token_string), so the eager per-character copy is skipped
token_string.push_back(char_traits<char_type>::to_char_type(current)); capture_char(std::integral_constant<bool, lazy_token_string> {});
}
if (current == '\n') if (current == '\n')
{ {
@@ -8778,6 +8889,18 @@ scan_number_done:
return current; return current;
} }
/// seekable adapter: nothing to capture, the token is rebuilt on error
void capture_char(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: copy the scanned character into token_string
void capture_char(std::false_type /*lazy*/)
{
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
{
token_string.push_back(char_traits<char_type>::to_char_type(current));
}
}
/*! /*!
@brief unget current character (read it again on next get) @brief unget current character (read it again on next get)
@@ -8805,6 +8928,15 @@ scan_number_done:
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: nothing was captured, so nothing to undo
void uncapture_char(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: drop the character copied by the matching get()
void uncapture_char(std::false_type /*lazy*/)
{
if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof())) if (JSON_HEDLEY_LIKELY(current != char_traits<char_type>::eof()))
{ {
JSON_ASSERT(!token_string.empty()); JSON_ASSERT(!token_string.empty());
@@ -8862,14 +8994,38 @@ scan_number_done:
return position; return position;
} }
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
// a pending unget of a real (non-EOF) character means that character
// was consumed from the input but is not part of the token; EOF is
// never consumed, so it must not be subtracted (mirrors unget())
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
const std::size_t stop = ia.get_consumed_count() - (pending_real_unget ? 1u : 0u);
if (JSON_HEDLEY_LIKELY(stop >= token_string_start))
{
ia.copy_consumed_range(token_string_start, stop, out);
}
return out;
}
/// streaming adapter: the token was copied eagerly while scanning
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& /*out*/, std::false_type /*lazy*/) const
{
return token_string;
}
/// return the last read token (for errors only). Will never contain EOF /// return the last read token (for errors only). Will never contain EOF
/// (an arbitrary value that is not a valid char value, often -1), because /// (an arbitrary value that is not a valid char value, often -1), because
/// 255 may legitimately occur. May contain NUL, which should be escaped. /// 255 may legitimately occur. May contain NUL, which should be escaped.
std::string get_token_string() const std::string get_token_string() const
{ {
std::vector<char_type> reconstructed;
const std::vector<char_type>& chars = collect_token_chars(reconstructed, std::integral_constant<bool, lazy_token_string> {});
// escape control characters // escape control characters
std::string result; std::string result;
for (const auto c : token_string) for (const auto c : chars)
{ {
if (static_cast<unsigned char>(c) <= '\x1F') if (static_cast<unsigned char>(c) <= '\x1F')
{ {
@@ -9030,9 +9186,14 @@ scan_number_done:
/// the start position of the current token /// the start position of the current token
position_t position {}; position_t position {};
/// raw input token string (for error messages) /// raw input token string for error messages; only populated for streaming
/// adapters (seekable adapters reconstruct it lazily via token_string_start)
std::vector<char_type> token_string {}; std::vector<char_type> token_string {};
/// start offset of the current token within the input, used to reconstruct
/// the last read token on error for seekable adapters (see collect_token_chars)
std::size_t token_string_start = 0;
/// buffer for variable-length tokens (numbers, strings) /// buffer for variable-length tokens (numbers, strings)
string_t token_buffer {}; string_t token_buffer {};
@@ -13073,18 +13234,7 @@ class binary_reader
const NumberType len, const NumberType len,
string_t& result) string_t& result)
{ {
bool success = true; return get_bytes(format, len, "string", result);
for (NumberType i = 0; i < len; i++)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string")))
{
success = false;
break;
}
result.push_back(static_cast<typename string_t::value_type>(current));
}
return success;
} }
/*! /*!
@@ -13106,18 +13256,66 @@ class binary_reader
const NumberType len, const NumberType len,
binary_t& result) binary_t& result)
{ {
bool success = true; return get_bytes(format, len, "binary", result);
for (NumberType i = 0; i < len; i++)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
{
success = false;
break;
} }
result.push_back(static_cast<typename binary_t::value_type>(current));
/*!
@brief read @a len bytes from the input into a string or byte container
@tparam NumberType the type of the length
@tparam ContainerType the destination container (string_t or binary_t)
@param[in] format the current format (for diagnostics)
@param[in] len number of bytes to read
@param[in] context further context information (for diagnostics)
@param[out] result container the bytes are appended to
@return whether reading completed
@note We cannot reserve @a len bytes for the result up front, because
@a len may be far larger than the actual input. Instead we read in
bounded chunks, so the peak allocation is capped regardless of the
claimed length while the per-byte loop is replaced by block copies
(a std::memcpy for contiguous inputs). @ref unexpect_eof() still
detects a premature end of input.
*/
template<typename NumberType, typename ContainerType>
bool get_bytes(const input_format_t format,
NumberType len,
const char* context,
ContainerType& result)
{
// upper bound on the number of bytes read (and allocated) per chunk
constexpr std::size_t chunk_size = 4096;
while (len > 0)
{
// number of bytes to read this iteration: min(chunk_size, len),
// computed without truncating chunk_size to a narrow NumberType
const std::size_t wanted = (static_cast<std::uintmax_t>(len) < static_cast<std::uintmax_t>(chunk_size))
? static_cast<std::size_t>(len)
: chunk_size;
const std::size_t old_size = result.size();
result.resize(old_size + wanted);
// resize() is required to make size() exactly old_size + wanted;
// that is the room get_elements() is allowed to write into
JSON_ASSERT(result.size() == old_size + wanted);
const std::size_t bytes_read = ia.get_elements(&result[old_size], wanted);
chars_read += bytes_read;
if (JSON_HEDLEY_UNLIKELY(bytes_read < wanted))
{
// premature end of input: shrink to what was actually read and
// report the failure at the first missing byte (same position
// accounting as get_to() for partial number reads)
result.resize(old_size + bytes_read);
++chars_read;
current = char_traits<char_type>::eof();
return unexpect_eof(format, context);
} }
return success; // a full chunk was read; get_elements() never returns more than requested
JSON_ASSERT(bytes_read == wanted);
len = static_cast<NumberType>(len - static_cast<NumberType>(wanted));
}
return true;
} }
/*! /*!
@@ -14896,8 +15094,6 @@ NLOHMANN_JSON_NAMESPACE_END
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
/*! /*!
@brief Default base class of the @ref basic_json class. @brief Default base class of the @ref basic_json class.
@@ -14908,13 +15104,27 @@ of @ref basic_json do not require complex case distinctions
@ref basic_json always has a base class. @ref basic_json always has a base class.
By default, this class is used because it is empty and thus has no effect By default, this class is used because it is empty and thus has no effect
on the behavior of @ref basic_json. on the behavior of @ref basic_json.
@note This class intentionally lives in namespace @ref nlohmann rather than
@ref nlohmann::detail. Every @ref basic_json specialization derives from
it (via @ref detail::json_base_class) unless a custom base class is
supplied, which makes its namespace an associated namespace of
@ref basic_json for the purpose of argument-dependent lookup (ADL). If
it lived in `nlohmann::detail`, that namespace - and with it the
library's internal `to_json`/`from_json` overloads - would leak into
ADL for any unqualified `to_json`/`from_json` call a user makes
involving a @ref basic_json argument, silently shadowing the user's own
overloads in some cases.
*/ */
struct json_default_base {}; struct json_default_base {};
namespace detail
{
template<class T> template<class T>
using json_base_class = typename std::conditional < using json_base_class = typename std::conditional <
std::is_same<T, void>::value, std::is_same<T, void>::value,
json_default_base, ::nlohmann::json_default_base,
T T
>::type; >::type;
+4 -1
View File
@@ -229,7 +229,10 @@ TEST_CASE("algorithms")
{ {
json j = {13, 29, 3, {{"one", 1}, {"two", 2}}, true, false, {1, 2, 3}, "foo", "baz", nullptr}; json j = {13, 29, 3, {{"one", 1}, {"two", 2}}, true, false, {1, 2, 3}, "foo", "baz", nullptr};
std::partial_sort(j.begin(), j.begin() + 4, j.end()); std::partial_sort(j.begin(), j.begin() + 4, j.end());
CHECK(j == json({nullptr, false, true, 3, {{"one", 1}, {"two", 2}}, 29, {1, 2, 3}, "foo", "baz", 13})); // only the first four elements are expected to be sorted, the rest are
// unspecified by the standard
const json expected({nullptr, false, true, 3});
CHECK(std::equal(j.begin(), j.begin() + 4, begin(expected)));
} }
} }
+40
View File
@@ -2778,3 +2778,43 @@ TEST_CASE("Tagged values")
CHECK(!jb["binary"].get_binary().has_subtype()); CHECK(!jb["binary"].get_binary().has_subtype());
} }
} }
TEST_CASE("CBOR large strings and binaries (chunked reader)")
{
// The binary reader reads strings and byte arrays in bounded chunks; make
// sure roundtripping is correct for lengths around and beyond the internal
// chunk size (4096 bytes), for both vector (iterator) and pointer inputs.
for (const std::size_t len :
{
std::size_t{0}, std::size_t{1}, std::size_t{4095}, std::size_t{4096},
std::size_t{4097}, std::size_t{8192}, std::size_t{100000}
})
{
CAPTURE(len);
// text string
const json j_string = std::string(len, 'x');
const std::vector<std::uint8_t> v_string = json::to_cbor(j_string);
CHECK(json::from_cbor(v_string) == j_string);
// pointer input exercises the std::memcpy fast path
CHECK(json::from_cbor(reinterpret_cast<const char*>(v_string.data()),
reinterpret_cast<const char*>(v_string.data()) + v_string.size()) == j_string);
// byte string
const json j_binary = json::binary(std::vector<std::uint8_t>(len, 0xCD));
const std::vector<std::uint8_t> v_binary = json::to_cbor(j_binary);
CHECK(json::from_cbor(v_binary) == j_binary);
CHECK(json::from_cbor(reinterpret_cast<const char*>(v_binary.data()),
reinterpret_cast<const char*>(v_binary.data()) + v_binary.size()) == j_binary);
// a truncated payload must still be reported as an error, never crash
// or loop, regardless of the (large) announced length
if (len > 16)
{
std::vector<std::uint8_t> truncated = v_string;
truncated.resize(truncated.size() - 8);
json _;
CHECK_THROWS_AS(_ = json::from_cbor(truncated), json::parse_error);
}
}
}
+113
View File
@@ -16,6 +16,12 @@ using nlohmann::json;
#endif #endif
#include <valarray> #include <valarray>
#include <algorithm>
#include <list>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace namespace
{ {
@@ -1725,3 +1731,110 @@ TEST_CASE("parser class")
CHECK_THROWS_WITH_AS(_ = json::parse("/*", nullptr, true, true), "[json.exception.parse_error.101] parse error at line 1, column 3: syntax error while parsing value - invalid comment; missing closing '*/'; last read: '/*<U+0000>'", json::parse_error); CHECK_THROWS_WITH_AS(_ = json::parse("/*", nullptr, true, true), "[json.exception.parse_error.101] parse error at line 1, column 3: syntax error while parsing value - invalid comment; missing closing '*/'; last read: '/*<U+0000>'", json::parse_error);
} }
} }
// this test relies on parse errors being thrown, so it is skipped when
// exceptions are disabled (json::parse aborts instead of throwing there)
#if !defined(JSON_NOEXCEPTION)
namespace
{
// Return the exception message from parsing @a input, or a "<no error ...>"
// sentinel if the parse unexpectedly succeeds. json::parse is nodiscard, so the
// result is consumed (via size()) to keep -Wunused-result / -Werror happy.
template<typename InputType>
std::string parse_error_message(InputType&& input)
{
try
{
const json j = json::parse(std::forward<InputType>(input));
return "<no error, size " + std::to_string(j.size()) + ">";
}
catch (const json::exception& e)
{
return e.what();
}
}
template<typename IteratorType>
std::string parse_error_message_range(IteratorType first, IteratorType last)
{
try
{
const json j = json::parse(first, last);
return "<no error, size " + std::to_string(j.size()) + ">";
}
catch (const json::exception& e)
{
return e.what();
}
}
} // namespace
TEST_CASE("last-read diagnostics are identical across input adapters")
{
// The lexer reconstructs the "last read" token lazily for seekable adapters
// (contiguous byte input) and copies it eagerly for streaming adapters.
// Both strategies must yield byte-for-byte identical error messages.
// a selection of malformed inputs that exercise different token kinds,
// whitespace/structural accumulation, number overflow, and control-char
// escaping in the reconstructed "last read" token
const std::vector<std::string> inputs =
{
"[1,2,x]",
" \n @",
"{\"a\": }",
"1.18973e+4932",
"\"\t\"",
"tru",
"[1 2]",
"\xEF\xBB\xBF nul",
};
for (const auto& s : inputs)
{
CAPTURE(s);
// reference: contiguous std::string -> seekable (lazy) path
const std::string reference = parse_error_message(s);
// every input is malformed, so parsing must fail (error messages start
// with '['; the success sentinel returned above starts with '<')
CHECK(reference.front() == '[');
// const char* -> also seekable
CHECK(parse_error_message(s.c_str()) == reference);
// std::vector<char> iterators -> seekable (random-access)
{
const std::vector<char> v(s.begin(), s.end());
CHECK(parse_error_message_range(v.begin(), v.end()) == reference);
}
// std::list iterators -> non-seekable (bidirectional) eager path
{
const std::list<char> l(s.begin(), s.end());
CHECK(parse_error_message_range(l.begin(), l.end()) == reference);
}
// std::istringstream -> non-seekable streaming eager path
{
std::istringstream ss(s);
CHECK(parse_error_message(ss) == reference);
}
// wide strings -> wide_string_input_adapter eager path; only comparable
// for ASCII input, as non-ASCII bytes are transcoded to different UTF-8
const bool is_ascii = std::all_of(s.begin(), s.end(), [](char c)
{
return static_cast<unsigned char>(c) < 0x80;
});
if (is_ascii)
{
const std::u16string w16(s.begin(), s.end());
CHECK(parse_error_message(w16) == reference);
const std::u32string w32(s.begin(), s.end());
CHECK(parse_error_message(w32) == reference);
}
}
}
#endif // !defined(JSON_NOEXCEPTION)
+96
View File
@@ -1358,4 +1358,100 @@ TEST_CASE("regression test #5122 - nlohmann::ordered_map move-assignment transfe
CHECK(src.begin()->first == "after-move"); CHECK(src.begin()->first == "after-move");
} }
// Stand-in for a third-party library (e.g., Eigen as of 3.4, which added
// STL-compatible begin()/end() to its vector types), living in its own
// namespace with its own to_json overload for its vector type.
namespace issue_4320_eigen
{
// "array-compatible" from the library's point of view (it has begin()/end()),
// but for which this (fake) third-party namespace provides its own to_json.
struct vector3
{
double v[3]; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-use-default-member-init,modernize-use-default-member-init)
vector3(double x, double y, double z) : v{x, y, z} {} // NOLINT(hicpp-member-init,cppcoreguidelines-pro-type-member-init)
double x() const
{
return v[0];
}
double y() const
{
return v[1];
}
double z() const
{
return v[2];
}
double* begin()
{
return v;
}
double* end()
{
return v + 3;
}
const double* begin() const
{
return v;
}
const double* end() const
{
return v + 3;
}
};
inline void to_json(json& j, const vector3& v) // NOLINT(misc-use-internal-linkage)
{
j = {{"x", v.x()}, {"y", v.y()}, {"z", v.z()}};
}
} // namespace issue_4320_eigen
// The user's own namespace, using the (fake) Eigen type as an implementation
// detail behind a payload type that has nothing to do with vectors/arrays.
namespace issue_4320
{
// Publicly derives from issue_4320_eigen::vector3 but does *not* define its
// own to_json - it is only ever used as a temporary to reach the base
// class's to_json via ADL.
struct vector3_wrapper : issue_4320_eigen::vector3
{
using issue_4320_eigen::vector3::vector3;
};
struct payload
{
double x, y, z;
};
inline vector3_wrapper to_eigen(const payload& p) // NOLINT(misc-use-internal-linkage)
{
return {p.x, p.y, p.z};
}
inline void to_json(json& j, const payload& p) // NOLINT(misc-use-internal-linkage)
{
// Unqualified call, passing a *derived* vector3_wrapper: relies on ADL
// finding issue_4320_eigen::to_json(json&, const vector3&) through the
// vector3 base class, via a derived-to-base conversion. Must NOT resolve
// to the library's own generic array-compatible to_json (an exact-match
// template for vector3_wrapper, since it also has begin()/end()), which
// would serialize this as [x, y, z] instead of {"x":x, "y":y, "z":z}.
to_json(j, to_eigen(p));
}
} // namespace issue_4320
TEST_CASE("issue #4320 - custom base class must not leak nlohmann::detail into ADL")
{
// Before the fix, basic_json unconditionally derived from a type living in
// nlohmann::detail (json_default_base), which made nlohmann::detail an
// associated namespace of every basic_json for ADL purposes. That leaked
// the library's internal generic-array to_json overload into unqualified
// to_json() calls made from user code, silently bypassing user-defined
// to_json overloads reached via a derived-to-base conversion.
const issue_4320::payload p{1.0, 2.0, 3.0};
json j;
to_json(j, p);
CHECK(j == json({{"x", 1.0}, {"y", 2.0}, {"z", 3.0}}));
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_CLANG_SUPPRESS_WARNING_POP