mirror of
https://github.com/nlohmann/json.git
synced 2026-07-21 19:23:03 +04:00
Read binary strings/blobs in bulk chunks with a memcpy fast path (#5233)
This commit is contained in:
@@ -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++)
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@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)
|
||||||
{
|
{
|
||||||
get();
|
// number of bytes to read this iteration: min(chunk_size, len),
|
||||||
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
|
// 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))
|
||||||
{
|
{
|
||||||
success = false;
|
// premature end of input: shrink to what was actually read and
|
||||||
break;
|
// 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);
|
||||||
}
|
}
|
||||||
result.push_back(static_cast<typename binary_t::value_type>(current));
|
// 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 success;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|||||||
@@ -203,9 +203,51 @@ class iterator_input_adapter
|
|||||||
out.insert(out.end(), from, to);
|
out.insert(out.end(), from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one
|
// 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)
|
||||||
@@ -223,7 +265,6 @@ class iterator_input_adapter
|
|||||||
return count * sizeof(T);
|
return count * sizeof(T);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
IteratorType begin;
|
IteratorType begin;
|
||||||
IteratorType current;
|
IteratorType current;
|
||||||
IteratorType end;
|
IteratorType end;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7035,9 +7035,51 @@ class iterator_input_adapter
|
|||||||
out.insert(out.end(), from, to);
|
out.insert(out.end(), from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one
|
// 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)
|
||||||
@@ -7055,7 +7097,6 @@ class iterator_input_adapter
|
|||||||
return count * sizeof(T);
|
return count * sizeof(T);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
IteratorType begin;
|
IteratorType begin;
|
||||||
IteratorType current;
|
IteratorType current;
|
||||||
IteratorType end;
|
IteratorType end;
|
||||||
@@ -13193,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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@@ -13226,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++)
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@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)
|
||||||
{
|
{
|
||||||
get();
|
// number of bytes to read this iteration: min(chunk_size, len),
|
||||||
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
|
// 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))
|
||||||
{
|
{
|
||||||
success = false;
|
// premature end of input: shrink to what was actually read and
|
||||||
break;
|
// 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);
|
||||||
}
|
}
|
||||||
result.push_back(static_cast<typename binary_t::value_type>(current));
|
// 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 success;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user