Compare commits

..
Author SHA1 Message Date
Niels Lohmann ba05d2c8c2 Fix CI: clang-tidy, unused result, Infer false positive in tests
- unit-deserialization.cpp: raw string literals, and assign the result
  of parse() in CHECK_THROWS_AS (-Wunused-result)
- unit-bson.cpp: mark Infer's NULLPTR_DEREFERENCE on from_bson() with an
  iterator pair as a false positive

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-06 12:10:44 +01:00
Niels Lohmann 8220c48141 Read iterator pairs of std::string and std::vector as pointer ranges
Before C++20, a pair of iterators of a contiguous container cannot be told
from any other random-access iterator, so json::parse(s.begin(), s.end())
on a std::string or std::vector<char> was read one character at a time,
without the bulk scans of strings, numbers and indentation that the
container itself gets (the container overload of input_adapter() already
maps it to a pointer range). The iterators of std::string and of a
std::vector of 1-byte integral elements are now matched exactly and mapped
to the pointer range they cover; an empty range is not dereferenced. The
character type stays the same, so the results and error positions do not
change.

parse(first, last) of a std::string gets 1.7-2.0x as fast in C++17 on an
Apple M1 Max, accept(first, last) 2.7-3.1x, and from_cbor(first, last) of a
std::vector 8-23% faster.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-06 06:47:12 +01:00
9 changed files with 173 additions and 504 deletions

No files matched your search

@@ -5,9 +5,7 @@
```
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, as in version 3.12.0 and
earlier, instead of locking the file once and reading it with the unlocked stdio functions. This also turns off the
scan of the `FILE`'s buffer in bulk (see below), which [`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)
does not affect.
earlier, instead of locking the file once and reading it with the unlocked stdio functions.
It does not change any result: values, exceptions, and the position of the file after parsing are the same either way.
Only the speed and the locking of the file differ (see the notes on locking below).
@@ -39,17 +37,9 @@ The default value is `0`: the file is locked once and read with the unlocked std
| Microsoft (MSVC, clang-cl) | `_lock_file`, `_fgetc_nolock`, `_unlock_file` |
| others (e.g., musl, MinGW, newlib) | `std::fgetc`, as before |
With glibc and the Apple C library, the parser also scans the characters that the `FILE` already holds in its
buffer in bulk, as it scans the get area of a stream (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)): it reads the same fields of the `FILE` that
the C library's own `getc_unlocked` reads, and it consumes exactly the characters that `getc_unlocked` would have
returned, so the file is left where it was left before.
Locking the file once makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3
times as fast with glibc, whose locking is cheaper. Scanning the buffer in bulk makes `parse()` another 1.7 to 2.6
times as fast (and `accept()` 2.9 to 4.4 times), about as fast as parsing from a `std::ifstream`. The binary
formats ([`from_cbor`](../basic_json/from_cbor.md), [`from_msgpack`](../basic_json/from_msgpack.md), ...) read
from a `FILE*` the same way.
This makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3 times as fast with
glibc, whose locking is cheaper. The binary formats ([`from_cbor`](../basic_json/from_cbor.md),
[`from_msgpack`](../basic_json/from_msgpack.md), ...) read from a `FILE*` the same way.
!!! note "Locking"
+1 -1
View File
@@ -77,7 +77,7 @@ them faster than others:
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time |
| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up |
| `#!cpp FILE*` | with glibc and the Apple C library, the characters the `FILE` holds in its buffer are scanned in bulk like a stream's; elsewhere one character at a time; the file is locked once for the whole parse |
| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse |
| other iterator ranges (e.g., `#!cpp std::list<char>`, `#!cpp std::istreambuf_iterator<char>`), wide strings | one character at a time |
Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
+2 -2
View File
@@ -27,8 +27,8 @@ A `#!cpp std::istream` (including `#!cpp std::ifstream`) gets the same bulk scan
characters its stream buffer already holds (the *get area*) and asks the stream buffer for more only when they are used
up. Nothing is read ahead, so the stream is left exactly where it would be after reading one character at a time, and
[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get
area is read one character at a time. A `#!cpp FILE*` is locked once per parse instead of once per character, and with
glibc and the Apple C library, the characters in its buffer are scanned in bulk as a stream's get area is. See
area is read one character at a time. A `#!cpp FILE*` is still read one character at a time, but `file_input_adapter`
locks the file once per parse instead of once per character. See
[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the
description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of
parsing an equivalent `#!cpp std::string`.
+51 -177
View File
@@ -58,19 +58,6 @@
#define JSON_UNLOCKED_FILE_IO 0
#endif
#undef JSON_UNLOCKED_FILE_IO_MSAN
// Whether file_input_adapter scans the buffer of the FILE directly, as
// getc_unlocked() reads it: the fields it uses are those that the C
// library's own getc_unlocked() macro uses (glibc: _IO_read_ptr and
// _IO_read_end; Apple: _p and _r). It needs the file locked for the whole
// parse, as JSON_UNLOCKED_FILE_IO == 1 has it (so JSON_DISABLE_UNLOCKED_FILE_IO
// and MemorySanitizer builds turn it off).
// (uClibc defines __GLIBC__ as well, but its FILE has other fields)
#if JSON_UNLOCKED_FILE_IO == 1 && ((defined(__GLIBC__) && !defined(__UCLIBC__)) || defined(__APPLE__))
#define JSON_FILE_WINDOW 1
#else
#define JSON_FILE_WINDOW 0
#endif
#endif // JSON_NO_IO
NLOHMANN_JSON_NAMESPACE_BEGIN
@@ -192,12 +179,8 @@ class token_carry_buffer
char* m_capacity_end = nullptr;
};
/*!
Input adapter for stdio file access. The file is locked for the lifetime of the
adapter and read with the unlocked stdio functions (see
JSON_DISABLE_UNLOCKED_FILE_IO). With glibc and the Apple C library, the
characters in the buffer of the FILE are also scanned in bulk, as the get area
of a stream is (see input_stream_adapter): exactly the characters getc_unlocked()
would return are consumed, so the file is left where it was left before.
Input adapter for stdio file access. This adapter read only 1 byte and do not use any
buffer. This adapter is a very low level adapter.
*/
class file_input_adapter
{
@@ -216,12 +199,6 @@ class file_input_adapter
file_input_adapter(const file_input_adapter&) = delete;
file_input_adapter(file_input_adapter&& rhs) noexcept
: m_file(rhs.m_file)
#if JSON_FILE_WINDOW
, token_carry(std::move(rhs.token_carry))
, token_segment(rhs.token_segment)
, current_in_buffer(rhs.current_in_buffer)
, tracking_token(rhs.tracking_token)
#endif
{
// the lock is owned by the thread, not the adapter: hand it over
rhs.m_file = nullptr;
@@ -233,20 +210,6 @@ class file_input_adapter
unlock();
}
#if JSON_FILE_WINDOW
std::char_traits<char>::int_type get_character()
{
// what getc_unlocked() does, while the buffer has characters
const char* const next = buffer_ptr();
if (JSON_HEDLEY_LIKELY(next != buffer_end()))
{
current_in_buffer = true;
buffer_bump(1);
return std::char_traits<char>::to_int_type(*next);
}
return get_character_slow();
}
#else
std::char_traits<char>::int_type get_character() noexcept
{
// the file is locked for the lifetime of the adapter, so the
@@ -259,7 +222,6 @@ class file_input_adapter
return std::fgetc(m_file);
#endif
}
#endif
// returns the number of characters successfully read
template<class T>
@@ -268,134 +230,7 @@ class file_input_adapter
return fread(dest, 1, sizeof(T) * count, m_file);
}
#if JSON_FILE_WINDOW
// The lexer may scan the buffer of the FILE directly, as the window of
// input_stream_adapter: the buffer ends wherever the C library's buffer
// ends, and is refilled by the next get_character().
static constexpr bool supports_bulk_scan = true;
static constexpr bool bulk_window = true;
// Pointer to the next unread character; only valid when bulk_remaining() > 0.
const char* bulk_data() const
{
return buffer_ptr();
}
// Number of unread characters in the buffer.
std::size_t bulk_remaining() const
{
if (!current_in_buffer)
{
return 0;
}
return static_cast<std::size_t>(buffer_end() - buffer_ptr());
}
// Consume @a n characters previously inspected via bulk_data().
void bulk_skip(std::size_t n)
{
JSON_ASSERT(n <= bulk_remaining());
buffer_bump(n);
}
// The adapter keeps the current token for the lexer's error messages, as
// input_stream_adapter does.
static constexpr bool supports_token_tracking = true;
void begin_token() noexcept
{
tracking_token = true;
token_carry.clear();
token_segment = buffer_ptr();
}
void mark_token_start(std::char_traits<char>::int_type c)
{
tracking_token = true;
token_carry.clear();
if (current_in_buffer)
{
token_segment = buffer_ptr() - 1;
}
else
{
token_segment = nullptr;
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
void copy_token(std::vector<char>& out, bool drop_last) const
{
token_carry.copy_to(out);
if (token_segment != nullptr)
{
out.insert(out.end(), token_segment, buffer_ptr());
}
if (drop_last && !out.empty())
{
out.pop_back();
}
}
#endif
private:
#if JSON_FILE_WINDOW
// the buffer of the FILE: [buffer_ptr(), buffer_end()) are the characters
// getc_unlocked() would return next
#if defined(__GLIBC__)
const char* buffer_ptr() const noexcept
{
return m_file->_IO_read_ptr;
}
const char* buffer_end() const noexcept
{
return m_file->_IO_read_end;
}
void buffer_bump(std::size_t n) noexcept
{
m_file->_IO_read_ptr += n;
}
#else
const char* buffer_ptr() const noexcept
{
return reinterpret_cast<const char*>(m_file->_p);
}
const char* buffer_end() const noexcept
{
return m_file->_r > 0 ? reinterpret_cast<const char*>(m_file->_p) + m_file->_r : reinterpret_cast<const char*>(m_file->_p);
}
void buffer_bump(std::size_t n) noexcept
{
m_file->_p += n;
m_file->_r -= static_cast<int>(n);
}
#endif
// the buffer is exhausted: getc_unlocked() refills it
JSON_HEDLEY_NEVER_INLINE std::char_traits<char>::int_type get_character_slow()
{
if (token_segment != nullptr)
{
token_carry.append(token_segment, buffer_end());
token_segment = nullptr;
}
const auto res = getc_unlocked(m_file);
if (res == EOF)
{
current_in_buffer = false;
return std::char_traits<char>::eof();
}
// the character was taken from the refilled buffer, at buffer_ptr() - 1
current_in_buffer = true;
if (tracking_token)
{
token_segment = buffer_ptr() - 1;
}
return res;
}
#endif
// Lock the file once for the whole parse instead of once per character.
// The lock is recursive, so fread() and the user's own stdio calls on the
// same thread keep working.
@@ -424,16 +259,6 @@ class file_input_adapter
/// the file pointer to read from
std::FILE* m_file;
#if JSON_FILE_WINDOW
/// the part of the current token that was in earlier buffers
token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init)
/// start of the current token's part in the buffer, or nullptr
const char* token_segment = nullptr;
/// whether the character last returned by get_character() is in the buffer
bool current_in_buffer = false;
/// whether a token is tracked at all; the binary readers never start one
bool tracking_token = false;
#endif
};
/*!
@@ -1348,6 +1173,55 @@ struct iterator_input_adapter_factory<IteratorType, SentinelType, enable_if_t<is
}
};
/*!
@brief whether @a IteratorType is an iterator of std::string or of a
std::vector of 1-byte elements
Their elements are contiguous, but before C++20 that cannot be detected from
the iterator type (std::contiguous_iterator), so an iterator pair of them would
be read one character at a time through the iterators. They are read as the
pointer range they are instead, as the containers themselves are (see the
container overload of input_adapter()). The types are matched exactly, so no
other iterator can be mistaken for one. (std::basic_string is only matched for
char: other character types need std::char_traits specializations that the
standard library need not have.)
*/
template < typename IteratorType, typename CharType, bool IsByte = sizeof(CharType) == 1 && std::is_integral<CharType>::value && !std::is_same<CharType, bool>::value >
struct is_std_contiguous_byte_iterator_impl : std::false_type {};
// (the containers are only named for byte types)
template<typename IteratorType, typename CharType>
struct is_std_contiguous_byte_iterator_impl<IteratorType, CharType, true>
: std::integral_constant < bool, !std::is_pointer<IteratorType>::value
&& (std::is_same<IteratorType, typename std::vector<CharType>::iterator>::value
|| std::is_same<IteratorType, typename std::vector<CharType>::const_iterator>::value
|| std::is_same<IteratorType, std::string::iterator>::value
|| std::is_same<IteratorType, std::string::const_iterator>::value) > {};
template<typename IteratorType>
struct is_std_contiguous_byte_iterator
: is_std_contiguous_byte_iterator_impl<IteratorType, typename std::remove_cv<typename std::iterator_traits<IteratorType>::value_type>::type> {};
template<typename IteratorType>
struct iterator_input_adapter_factory<IteratorType, IteratorType, enable_if_t<is_std_contiguous_byte_iterator<IteratorType>::value>>
{
using iterator_type = IteratorType;
using sentinel_type = IteratorType;
using char_type = typename std::remove_cv<typename std::iterator_traits<iterator_type>::value_type>::type;
using adapter_type = iterator_input_adapter<const char_type*>;
static adapter_type create(IteratorType first, IteratorType last)
{
if (first == last)
{
// (an empty range must not be dereferenced)
return adapter_type(nullptr, nullptr);
}
const char_type* const begin = std::addressof(*first);
return adapter_type(begin, begin + (last - first));
}
};
// General purpose iterator-based input (iterator+sentinel pair; SentinelType
// defaults to IteratorType for the common same-type case, but may differ for
// C++20 ranges-style iterator+sentinel pairs). Only enable for types that can
@@ -22,7 +22,6 @@
#undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_UNLOCKED_FILE_IO
#undef JSON_FILE_WINDOW
#undef JSON_DTOA_SSE2
#undef JSON_DTOA_NEON
#undef JSON_LITTLE_ENDIAN_TARGET
+51 -178
View File
@@ -9167,19 +9167,6 @@ NLOHMANN_JSON_NAMESPACE_END
#define JSON_UNLOCKED_FILE_IO 0
#endif
#undef JSON_UNLOCKED_FILE_IO_MSAN
// Whether file_input_adapter scans the buffer of the FILE directly, as
// getc_unlocked() reads it: the fields it uses are those that the C
// library's own getc_unlocked() macro uses (glibc: _IO_read_ptr and
// _IO_read_end; Apple: _p and _r). It needs the file locked for the whole
// parse, as JSON_UNLOCKED_FILE_IO == 1 has it (so JSON_DISABLE_UNLOCKED_FILE_IO
// and MemorySanitizer builds turn it off).
// (uClibc defines __GLIBC__ as well, but its FILE has other fields)
#if JSON_UNLOCKED_FILE_IO == 1 && ((defined(__GLIBC__) && !defined(__UCLIBC__)) || defined(__APPLE__))
#define JSON_FILE_WINDOW 1
#else
#define JSON_FILE_WINDOW 0
#endif
#endif // JSON_NO_IO
NLOHMANN_JSON_NAMESPACE_BEGIN
@@ -9301,12 +9288,8 @@ class token_carry_buffer
char* m_capacity_end = nullptr;
};
/*!
Input adapter for stdio file access. The file is locked for the lifetime of the
adapter and read with the unlocked stdio functions (see
JSON_DISABLE_UNLOCKED_FILE_IO). With glibc and the Apple C library, the
characters in the buffer of the FILE are also scanned in bulk, as the get area
of a stream is (see input_stream_adapter): exactly the characters getc_unlocked()
would return are consumed, so the file is left where it was left before.
Input adapter for stdio file access. This adapter read only 1 byte and do not use any
buffer. This adapter is a very low level adapter.
*/
class file_input_adapter
{
@@ -9325,12 +9308,6 @@ class file_input_adapter
file_input_adapter(const file_input_adapter&) = delete;
file_input_adapter(file_input_adapter&& rhs) noexcept
: m_file(rhs.m_file)
#if JSON_FILE_WINDOW
, token_carry(std::move(rhs.token_carry))
, token_segment(rhs.token_segment)
, current_in_buffer(rhs.current_in_buffer)
, tracking_token(rhs.tracking_token)
#endif
{
// the lock is owned by the thread, not the adapter: hand it over
rhs.m_file = nullptr;
@@ -9342,20 +9319,6 @@ class file_input_adapter
unlock();
}
#if JSON_FILE_WINDOW
std::char_traits<char>::int_type get_character()
{
// what getc_unlocked() does, while the buffer has characters
const char* const next = buffer_ptr();
if (JSON_HEDLEY_LIKELY(next != buffer_end()))
{
current_in_buffer = true;
buffer_bump(1);
return std::char_traits<char>::to_int_type(*next);
}
return get_character_slow();
}
#else
std::char_traits<char>::int_type get_character() noexcept
{
// the file is locked for the lifetime of the adapter, so the
@@ -9368,7 +9331,6 @@ class file_input_adapter
return std::fgetc(m_file);
#endif
}
#endif
// returns the number of characters successfully read
template<class T>
@@ -9377,134 +9339,7 @@ class file_input_adapter
return fread(dest, 1, sizeof(T) * count, m_file);
}
#if JSON_FILE_WINDOW
// The lexer may scan the buffer of the FILE directly, as the window of
// input_stream_adapter: the buffer ends wherever the C library's buffer
// ends, and is refilled by the next get_character().
static constexpr bool supports_bulk_scan = true;
static constexpr bool bulk_window = true;
// Pointer to the next unread character; only valid when bulk_remaining() > 0.
const char* bulk_data() const
{
return buffer_ptr();
}
// Number of unread characters in the buffer.
std::size_t bulk_remaining() const
{
if (!current_in_buffer)
{
return 0;
}
return static_cast<std::size_t>(buffer_end() - buffer_ptr());
}
// Consume @a n characters previously inspected via bulk_data().
void bulk_skip(std::size_t n)
{
JSON_ASSERT(n <= bulk_remaining());
buffer_bump(n);
}
// The adapter keeps the current token for the lexer's error messages, as
// input_stream_adapter does.
static constexpr bool supports_token_tracking = true;
void begin_token() noexcept
{
tracking_token = true;
token_carry.clear();
token_segment = buffer_ptr();
}
void mark_token_start(std::char_traits<char>::int_type c)
{
tracking_token = true;
token_carry.clear();
if (current_in_buffer)
{
token_segment = buffer_ptr() - 1;
}
else
{
token_segment = nullptr;
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
void copy_token(std::vector<char>& out, bool drop_last) const
{
token_carry.copy_to(out);
if (token_segment != nullptr)
{
out.insert(out.end(), token_segment, buffer_ptr());
}
if (drop_last && !out.empty())
{
out.pop_back();
}
}
#endif
private:
#if JSON_FILE_WINDOW
// the buffer of the FILE: [buffer_ptr(), buffer_end()) are the characters
// getc_unlocked() would return next
#if defined(__GLIBC__)
const char* buffer_ptr() const noexcept
{
return m_file->_IO_read_ptr;
}
const char* buffer_end() const noexcept
{
return m_file->_IO_read_end;
}
void buffer_bump(std::size_t n) noexcept
{
m_file->_IO_read_ptr += n;
}
#else
const char* buffer_ptr() const noexcept
{
return reinterpret_cast<const char*>(m_file->_p);
}
const char* buffer_end() const noexcept
{
return m_file->_r > 0 ? reinterpret_cast<const char*>(m_file->_p) + m_file->_r : reinterpret_cast<const char*>(m_file->_p);
}
void buffer_bump(std::size_t n) noexcept
{
m_file->_p += n;
m_file->_r -= static_cast<int>(n);
}
#endif
// the buffer is exhausted: getc_unlocked() refills it
JSON_HEDLEY_NEVER_INLINE std::char_traits<char>::int_type get_character_slow()
{
if (token_segment != nullptr)
{
token_carry.append(token_segment, buffer_end());
token_segment = nullptr;
}
const auto res = getc_unlocked(m_file);
if (res == EOF)
{
current_in_buffer = false;
return std::char_traits<char>::eof();
}
// the character was taken from the refilled buffer, at buffer_ptr() - 1
current_in_buffer = true;
if (tracking_token)
{
token_segment = buffer_ptr() - 1;
}
return res;
}
#endif
// Lock the file once for the whole parse instead of once per character.
// The lock is recursive, so fread() and the user's own stdio calls on the
// same thread keep working.
@@ -9533,16 +9368,6 @@ class file_input_adapter
/// the file pointer to read from
std::FILE* m_file;
#if JSON_FILE_WINDOW
/// the part of the current token that was in earlier buffers
token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init)
/// start of the current token's part in the buffer, or nullptr
const char* token_segment = nullptr;
/// whether the character last returned by get_character() is in the buffer
bool current_in_buffer = false;
/// whether a token is tracked at all; the binary readers never start one
bool tracking_token = false;
#endif
};
/*!
@@ -10457,6 +10282,55 @@ struct iterator_input_adapter_factory<IteratorType, SentinelType, enable_if_t<is
}
};
/*!
@brief whether @a IteratorType is an iterator of std::string or of a
std::vector of 1-byte elements
Their elements are contiguous, but before C++20 that cannot be detected from
the iterator type (std::contiguous_iterator), so an iterator pair of them would
be read one character at a time through the iterators. They are read as the
pointer range they are instead, as the containers themselves are (see the
container overload of input_adapter()). The types are matched exactly, so no
other iterator can be mistaken for one. (std::basic_string is only matched for
char: other character types need std::char_traits specializations that the
standard library need not have.)
*/
template < typename IteratorType, typename CharType, bool IsByte = sizeof(CharType) == 1 && std::is_integral<CharType>::value && !std::is_same<CharType, bool>::value >
struct is_std_contiguous_byte_iterator_impl : std::false_type {};
// (the containers are only named for byte types)
template<typename IteratorType, typename CharType>
struct is_std_contiguous_byte_iterator_impl<IteratorType, CharType, true>
: std::integral_constant < bool, !std::is_pointer<IteratorType>::value
&& (std::is_same<IteratorType, typename std::vector<CharType>::iterator>::value
|| std::is_same<IteratorType, typename std::vector<CharType>::const_iterator>::value
|| std::is_same<IteratorType, std::string::iterator>::value
|| std::is_same<IteratorType, std::string::const_iterator>::value) > {};
template<typename IteratorType>
struct is_std_contiguous_byte_iterator
: is_std_contiguous_byte_iterator_impl<IteratorType, typename std::remove_cv<typename std::iterator_traits<IteratorType>::value_type>::type> {};
template<typename IteratorType>
struct iterator_input_adapter_factory<IteratorType, IteratorType, enable_if_t<is_std_contiguous_byte_iterator<IteratorType>::value>>
{
using iterator_type = IteratorType;
using sentinel_type = IteratorType;
using char_type = typename std::remove_cv<typename std::iterator_traits<iterator_type>::value_type>::type;
using adapter_type = iterator_input_adapter<const char_type*>;
static adapter_type create(IteratorType first, IteratorType last)
{
if (first == last)
{
// (an empty range must not be dereferenced)
return adapter_type(nullptr, nullptr);
}
const char_type* const begin = std::addressof(*first);
return adapter_type(begin, begin + (last - first));
}
};
// General purpose iterator-based input (iterator+sentinel pair; SentinelType
// defaults to IteratorType for the common same-type case, but may differ for
// C++20 ranges-style iterator+sentinel pairs). Only enable for types that can
@@ -46523,7 +46397,6 @@ struct formatter<nlohmann::NLOHMANN_BASIC_JSON_TPL, char> // NOLINT(cert-dcl58-c
#undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_UNLOCKED_FILE_IO
#undef JSON_FILE_WINDOW
#undef JSON_DTOA_SSE2
#undef JSON_DTOA_NEON
#undef JSON_LITTLE_ENDIAN_TARGET
+2
View File
@@ -1674,6 +1674,8 @@ TEST_CASE("BSON input that cannot be read is discarded by every overload")
json _;
CHECK_THROWS_AS(_ = json::from_bson(input.begin(), input.end()), json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
// false positive: Infer takes std::integral_constant<bool, false>::value for a pointer
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(json::from_bson(input.begin(), input.end(), true, false).is_discarded());
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded());
+62
View File
@@ -32,6 +32,7 @@ using nlohmann::json;
#include <iostream>
#include <iterator>
#include <list>
#include <sstream>
#include <string>
#include <valarray>
@@ -1409,3 +1410,64 @@ TEST_CASE("indentation runs of every length around the 8-byte skip")
CHECK(parse_result(indented("[\n~1]", std::string(17, ' ')), true) == "[1]");
}
#endif
#if !defined(JSON_NOEXCEPTION)
namespace
{
/// the value of parsing [first, last), or the message of the exception
template<typename Iterator>
std::string outcome_of(Iterator first, Iterator last)
{
try
{
return json::parse(first, last).dump();
}
catch (const json::exception& e)
{
return e.what();
}
}
/// a std::string, std::vector<char>, <unsigned char> and <signed char> as
/// iterator pairs: the same as the byte path (std::list iterators)
void check_iterator_pairs(const std::string& text)
{
CAPTURE(text)
const std::list<char> bytes(text.begin(), text.end());
const std::string expected = outcome_of(bytes.begin(), bytes.end());
const bool expected_accept = json::accept(bytes.begin(), bytes.end());
std::string s = text;
CHECK(outcome_of(s.begin(), s.end()) == expected);
CHECK(outcome_of(s.cbegin(), s.cend()) == expected);
CHECK(json::accept(s.cbegin(), s.cend()) == expected_accept);
const std::vector<char> vc(text.begin(), text.end());
CHECK(outcome_of(vc.begin(), vc.end()) == expected);
const std::vector<unsigned char> vu(text.begin(), text.end());
CHECK(outcome_of(vu.begin(), vu.end()) == expected);
CHECK(json::accept(vu.begin(), vu.end()) == expected_accept);
const std::vector<signed char> vs(text.begin(), text.end());
CHECK(outcome_of(vs.begin(), vs.end()) == expected);
}
} // namespace
TEST_CASE("deserialization from iterator pairs of std::string and std::vector")
{
// these are read as the pointer range they are, also before C++20
for (const char* text :
{
"", " ", R"([1, 2.5, "a", true, null, {"k": ["v"]}])", "[1, 2", R"({"a" 1})", "\"\xC3\xA4\"", "\"\xC3\"", "[1]x", "1e400"
})
{
check_iterator_pairs(text);
}
check_iterator_pairs(std::string("[1]" "\0" "x", 5));
// an empty range is not dereferenced
std::string empty;
CHECK_FALSE(json::accept(empty.begin(), empty.end()));
json _;
CHECK_THROWS_AS(_ = json::parse(empty.begin(), empty.end()), json::parse_error&);
}
#endif
-131
View File
@@ -49,14 +49,6 @@ file_ptr open_with(const char* name, const std::string& content)
return {std::fopen(name, "rb"), &std::fclose};
}
// the smallest size setvbuf() accepts for _IOFBF: the Microsoft C runtime
// rejects sizes below 2 as an invalid parameter, which ends the process
#if defined(_WIN32)
constexpr int min_full_buffer = 2;
#else
constexpr int min_full_buffer = 1;
#endif
// The stdio lock is recursive, so whether parsing released it can only be
// seen from another thread: ftrylockfile() fails there while this thread
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
@@ -233,129 +225,6 @@ TEST_CASE("parsing from a FILE*")
check_unlocked(f.get());
}
#if !defined(JSON_NOEXCEPTION)
SECTION("tokens across the edges of the file's buffer")
{
// the adapter reads the buffer of the FILE directly where it can (see
// JSON_FILE_WINDOW): with buffers of 1 to 70 bytes and none, every
// token crosses an edge somewhere; the results, the error messages
// and their positions must be those of the string
const std::vector<std::string> documents =
{
input,
R"({"long": "a string that is longer than the buffer of the file, with \"escapes\" and \u00e4", "n": [123456789012345678901, -0.000123e-45, 1.5E+300, 0, -0]})",
"[\n \"indented\",\n \"caf\xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80\",\n {\n \"key\": 1\n }\n]",
R"({"a": [1, 2, 3], "b": "unterminated)",
R"([1, 2, 3, 1e999])",
R"({"a": "invalid escape \x"})",
"[\"invalid UTF-8 \xc3\x28\"]",
"[1, 2, 3] trailing",
R"([truex])",
};
for (const auto& document : documents)
{
std::string expected;
try
{
expected = json::parse(document).dump();
}
catch (const json::exception& e)
{
expected = e.what();
}
const bool expected_accept = json::accept(document);
for (int size = 0; size <= 70; ++size)
{
if (size != 0 && size < min_full_buffer)
{
continue;
}
CAPTURE(document)
CAPTURE(size)
std::vector<char> buffer(static_cast<std::size_t>(size) + 1);
for (int pass = 0; pass < 2; ++pass)
{
const file_ptr f = open_with(name, document);
REQUIRE(f != nullptr);
// size 0: unbuffered
REQUIRE(std::setvbuf(f.get(), size == 0 ? nullptr : buffer.data(), size == 0 ? _IONBF : _IOFBF, static_cast<std::size_t>(size)) == 0);
if (pass == 0)
{
std::string actual;
try
{
actual = json::parse(f.get()).dump();
}
catch (const json::exception& e)
{
actual = e.what();
}
CHECK(actual == expected);
}
else
{
CHECK(json::accept(f.get()) == expected_accept);
}
check_unlocked(f.get());
}
}
}
}
#endif
#if !defined(JSON_NOEXCEPTION)
SECTION("characters pushed back with ungetc() before parsing")
{
// (glibc and Apple keep them in a separate buffer)
const file_ptr f = open_with(name, "2, 3]");
REQUIRE(f != nullptr);
REQUIRE(std::ungetc(',', f.get()) == ',');
REQUIRE(std::ungetc('1', f.get()) == '1');
REQUIRE(std::ungetc('[', f.get()) == '[');
CHECK(json::parse(f.get()) == json({1, 2, 3}));
}
#endif
SECTION("SAX events across the edges of the file's buffer")
{
const std::string document = R"({"key": "a value that is longer than the buffer", "n": [1, -2.5e3, true, null]})";
std::vector<long> expected;
{
const file_ptr f = open_with(name, document);
REQUIRE(f != nullptr);
position_reading_sax sax(f.get());
CHECK(json::sax_parse(f.get(), &sax));
expected = sax.positions();
}
for (int size = min_full_buffer; size <= 20; ++size)
{
CAPTURE(size)
std::vector<char> buffer(static_cast<std::size_t>(size));
const file_ptr f = open_with(name, document);
REQUIRE(f != nullptr);
REQUIRE(std::setvbuf(f.get(), buffer.data(), _IOFBF, buffer.size()) == 0);
position_reading_sax sax(f.get());
CHECK(json::sax_parse(f.get(), &sax));
// the events happen at the same positions of the file
CHECK(sax.positions() == expected);
}
}
SECTION("binary strings across the edges of the file's buffer")
{
const json value = {{"a string that is longer than the buffer of the file", "and a value that is longer, too"}, {"n", {1, 2}}};
const std::vector<std::uint8_t> cbor = json::to_cbor(value);
for (int size = min_full_buffer; size <= 20; ++size)
{
CAPTURE(size)
std::vector<char> buffer(static_cast<std::size_t>(size));
const file_ptr f = open_with(name, std::string(cbor.begin(), cbor.end()));
REQUIRE(f != nullptr);
REQUIRE(std::setvbuf(f.get(), buffer.data(), _IOFBF, buffer.size()) == 0);
CHECK(json::from_cbor(f.get()) == value);
}
}
SECTION("binary formats read from a FILE*")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};