Skip to content

Commit 83c87cb

Browse files
authored
Read binary strings/blobs in bulk chunks with a memcpy fast path (#5233)
1 parent eed1587 commit 83c87cb

4 files changed

Lines changed: 248 additions & 52 deletions

File tree

include/nlohmann/detail/input/binary_reader.hpp

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#include <array> // array
1313
#include <cmath> // ldexp
1414
#include <cstddef> // size_t
15-
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t
15+
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t
1616
#include <cstdio> // snprintf
1717
#include <cstring> // memcpy
1818
#include <iterator> // back_inserter
@@ -2908,18 +2908,7 @@ class binary_reader
29082908
const NumberType len,
29092909
string_t& result)
29102910
{
2911-
bool success = true;
2912-
for (NumberType i = 0; i < len; i++)
2913-
{
2914-
get();
2915-
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string")))
2916-
{
2917-
success = false;
2918-
break;
2919-
}
2920-
result.push_back(static_cast<typename string_t::value_type>(current));
2921-
}
2922-
return success;
2911+
return get_bytes(format, len, "string", result);
29232912
}
29242913

29252914
/*!
@@ -2941,18 +2930,66 @@ class binary_reader
29412930
const NumberType len,
29422931
binary_t& result)
29432932
{
2944-
bool success = true;
2945-
for (NumberType i = 0; i < len; i++)
2946-
{
2947-
get();
2948-
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
2949-
{
2950-
success = false;
2951-
break;
2952-
}
2953-
result.push_back(static_cast<typename binary_t::value_type>(current));
2933+
return get_bytes(format, len, "binary", result);
2934+
}
2935+
2936+
/*!
2937+
@brief read @a len bytes from the input into a string or byte container
2938+
2939+
@tparam NumberType the type of the length
2940+
@tparam ContainerType the destination container (string_t or binary_t)
2941+
@param[in] format the current format (for diagnostics)
2942+
@param[in] len number of bytes to read
2943+
@param[in] context further context information (for diagnostics)
2944+
@param[out] result container the bytes are appended to
2945+
2946+
@return whether reading completed
2947+
2948+
@note We cannot reserve @a len bytes for the result up front, because
2949+
@a len may be far larger than the actual input. Instead we read in
2950+
bounded chunks, so the peak allocation is capped regardless of the
2951+
claimed length while the per-byte loop is replaced by block copies
2952+
(a std::memcpy for contiguous inputs). @ref unexpect_eof() still
2953+
detects a premature end of input.
2954+
*/
2955+
template<typename NumberType, typename ContainerType>
2956+
bool get_bytes(const input_format_t format,
2957+
NumberType len,
2958+
const char* context,
2959+
ContainerType& result)
2960+
{
2961+
// upper bound on the number of bytes read (and allocated) per chunk
2962+
constexpr std::size_t chunk_size = 4096;
2963+
2964+
while (len > 0)
2965+
{
2966+
// number of bytes to read this iteration: min(chunk_size, len),
2967+
// computed without truncating chunk_size to a narrow NumberType
2968+
const std::size_t wanted = (static_cast<std::uintmax_t>(len) < static_cast<std::uintmax_t>(chunk_size))
2969+
? static_cast<std::size_t>(len)
2970+
: chunk_size;
2971+
const std::size_t old_size = result.size();
2972+
result.resize(old_size + wanted);
2973+
// resize() is required to make size() exactly old_size + wanted;
2974+
// that is the room get_elements() is allowed to write into
2975+
JSON_ASSERT(result.size() == old_size + wanted);
2976+
const std::size_t bytes_read = ia.get_elements(&result[old_size], wanted);
2977+
chars_read += bytes_read;
2978+
if (JSON_HEDLEY_UNLIKELY(bytes_read < wanted))
2979+
{
2980+
// premature end of input: shrink to what was actually read and
2981+
// report the failure at the first missing byte (same position
2982+
// accounting as get_to() for partial number reads)
2983+
result.resize(old_size + bytes_read);
2984+
++chars_read;
2985+
current = char_traits<char_type>::eof();
2986+
return unexpect_eof(format, context);
2987+
}
2988+
// a full chunk was read; get_elements() never returns more than requested
2989+
JSON_ASSERT(bytes_read == wanted);
2990+
len = static_cast<NumberType>(len - static_cast<NumberType>(wanted));
29542991
}
2955-
return success;
2992+
return true;
29562993
}
29572994

29582995
/*!

include/nlohmann/detail/input/input_adapters.hpp

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,51 @@ class iterator_input_adapter
203203
out.insert(out.end(), from, to);
204204
}
205205

206-
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one
206+
// Copy up to count * sizeof(T) bytes into dest, returning the number of
207+
// bytes actually read. For contiguous iterators (e.g. pointers) this is a
208+
// single std::memcpy; for general iterators we fall back to processing the
209+
// range one-by-one.
207210
template<class T>
208211
std::size_t get_elements(T* dest, std::size_t count = 1)
212+
{
213+
return get_elements_impl(dest, count, std::integral_constant<bool, iterator_is_contiguous> {});
214+
}
215+
216+
private:
217+
// whether IteratorType refers to a contiguous range and therefore supports
218+
// a std::memcpy fast path (pointers always do; in C++20 we can also detect
219+
// library iterators such as those of std::vector and std::string)
220+
static constexpr bool iterator_is_contiguous =
221+
#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20)
222+
std::contiguous_iterator<IteratorType> ||
223+
#endif
224+
std::is_pointer<IteratorType>::value;
225+
226+
// contiguous fast path: bulk copy the remaining range with std::memcpy
227+
template<class T>
228+
std::size_t get_elements_impl(T* dest, std::size_t count, std::true_type /*contiguous*/)
229+
{
230+
const std::size_t wanted = count * sizeof(T);
231+
const std::size_t available = static_cast<std::size_t>(std::distance(current, end)) * sizeof(char_type);
232+
const std::size_t copied = (std::min)(wanted, available);
233+
if (JSON_HEDLEY_LIKELY(copied != 0))
234+
{
235+
// the copy must stay within both buffers: the caller-provided
236+
// destination holds `wanted` bytes and the remaining input range
237+
// holds `available` bytes, and `copied` is the minimum of the two
238+
JSON_ASSERT(copied <= wanted); // does not overrun the destination
239+
JSON_ASSERT(copied <= available); // does not read past the input end
240+
// &*current yields the raw address for both raw pointers and
241+
// non-pointer contiguous iterators (e.g. std::vector's iterator)
242+
std::memcpy(dest, &*current, copied);
243+
std::advance(current, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(copied / sizeof(char_type)));
244+
}
245+
return copied;
246+
}
247+
248+
// general fallback: copy the range one element at a time
249+
template<class T>
250+
std::size_t get_elements_impl(T* dest, std::size_t count, std::false_type /*contiguous*/)
209251
{
210252
auto* ptr = reinterpret_cast<char*>(dest);
211253
for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index)
@@ -223,7 +265,6 @@ class iterator_input_adapter
223265
return count * sizeof(T);
224266
}
225267

226-
private:
227268
IteratorType begin;
228269
IteratorType current;
229270
IteratorType end;

single_include/nlohmann/json.hpp

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6811,7 +6811,7 @@ NLOHMANN_JSON_NAMESPACE_END
68116811
#include <array> // array
68126812
#include <cmath> // ldexp
68136813
#include <cstddef> // size_t
6814-
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t
6814+
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t
68156815
#include <cstdio> // snprintf
68166816
#include <cstring> // memcpy
68176817
#include <iterator> // back_inserter
@@ -7035,9 +7035,51 @@ class iterator_input_adapter
70357035
out.insert(out.end(), from, to);
70367036
}
70377037

7038-
// for general iterators, we cannot really do something better than falling back to processing the range one-by-one
7038+
// Copy up to count * sizeof(T) bytes into dest, returning the number of
7039+
// bytes actually read. For contiguous iterators (e.g. pointers) this is a
7040+
// single std::memcpy; for general iterators we fall back to processing the
7041+
// range one-by-one.
70397042
template<class T>
70407043
std::size_t get_elements(T* dest, std::size_t count = 1)
7044+
{
7045+
return get_elements_impl(dest, count, std::integral_constant<bool, iterator_is_contiguous> {});
7046+
}
7047+
7048+
private:
7049+
// whether IteratorType refers to a contiguous range and therefore supports
7050+
// a std::memcpy fast path (pointers always do; in C++20 we can also detect
7051+
// library iterators such as those of std::vector and std::string)
7052+
static constexpr bool iterator_is_contiguous =
7053+
#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20)
7054+
std::contiguous_iterator<IteratorType> ||
7055+
#endif
7056+
std::is_pointer<IteratorType>::value;
7057+
7058+
// contiguous fast path: bulk copy the remaining range with std::memcpy
7059+
template<class T>
7060+
std::size_t get_elements_impl(T* dest, std::size_t count, std::true_type /*contiguous*/)
7061+
{
7062+
const std::size_t wanted = count * sizeof(T);
7063+
const std::size_t available = static_cast<std::size_t>(std::distance(current, end)) * sizeof(char_type);
7064+
const std::size_t copied = (std::min)(wanted, available);
7065+
if (JSON_HEDLEY_LIKELY(copied != 0))
7066+
{
7067+
// the copy must stay within both buffers: the caller-provided
7068+
// destination holds `wanted` bytes and the remaining input range
7069+
// holds `available` bytes, and `copied` is the minimum of the two
7070+
JSON_ASSERT(copied <= wanted); // does not overrun the destination
7071+
JSON_ASSERT(copied <= available); // does not read past the input end
7072+
// &*current yields the raw address for both raw pointers and
7073+
// non-pointer contiguous iterators (e.g. std::vector's iterator)
7074+
std::memcpy(dest, &*current, copied);
7075+
std::advance(current, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(copied / sizeof(char_type)));
7076+
}
7077+
return copied;
7078+
}
7079+
7080+
// general fallback: copy the range one element at a time
7081+
template<class T>
7082+
std::size_t get_elements_impl(T* dest, std::size_t count, std::false_type /*contiguous*/)
70417083
{
70427084
auto* ptr = reinterpret_cast<char*>(dest);
70437085
for (std::size_t read_index = 0; read_index < count * sizeof(T); ++read_index)
@@ -7055,7 +7097,6 @@ class iterator_input_adapter
70557097
return count * sizeof(T);
70567098
}
70577099

7058-
private:
70597100
IteratorType begin;
70607101
IteratorType current;
70617102
IteratorType end;
@@ -13193,18 +13234,7 @@ class binary_reader
1319313234
const NumberType len,
1319413235
string_t& result)
1319513236
{
13196-
bool success = true;
13197-
for (NumberType i = 0; i < len; i++)
13198-
{
13199-
get();
13200-
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string")))
13201-
{
13202-
success = false;
13203-
break;
13204-
}
13205-
result.push_back(static_cast<typename string_t::value_type>(current));
13206-
}
13207-
return success;
13237+
return get_bytes(format, len, "string", result);
1320813238
}
1320913239

1321013240
/*!
@@ -13226,18 +13256,66 @@ class binary_reader
1322613256
const NumberType len,
1322713257
binary_t& result)
1322813258
{
13229-
bool success = true;
13230-
for (NumberType i = 0; i < len; i++)
13231-
{
13232-
get();
13233-
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary")))
13234-
{
13235-
success = false;
13236-
break;
13237-
}
13238-
result.push_back(static_cast<typename binary_t::value_type>(current));
13259+
return get_bytes(format, len, "binary", result);
13260+
}
13261+
13262+
/*!
13263+
@brief read @a len bytes from the input into a string or byte container
13264+
13265+
@tparam NumberType the type of the length
13266+
@tparam ContainerType the destination container (string_t or binary_t)
13267+
@param[in] format the current format (for diagnostics)
13268+
@param[in] len number of bytes to read
13269+
@param[in] context further context information (for diagnostics)
13270+
@param[out] result container the bytes are appended to
13271+
13272+
@return whether reading completed
13273+
13274+
@note We cannot reserve @a len bytes for the result up front, because
13275+
@a len may be far larger than the actual input. Instead we read in
13276+
bounded chunks, so the peak allocation is capped regardless of the
13277+
claimed length while the per-byte loop is replaced by block copies
13278+
(a std::memcpy for contiguous inputs). @ref unexpect_eof() still
13279+
detects a premature end of input.
13280+
*/
13281+
template<typename NumberType, typename ContainerType>
13282+
bool get_bytes(const input_format_t format,
13283+
NumberType len,
13284+
const char* context,
13285+
ContainerType& result)
13286+
{
13287+
// upper bound on the number of bytes read (and allocated) per chunk
13288+
constexpr std::size_t chunk_size = 4096;
13289+
13290+
while (len > 0)
13291+
{
13292+
// number of bytes to read this iteration: min(chunk_size, len),
13293+
// computed without truncating chunk_size to a narrow NumberType
13294+
const std::size_t wanted = (static_cast<std::uintmax_t>(len) < static_cast<std::uintmax_t>(chunk_size))
13295+
? static_cast<std::size_t>(len)
13296+
: chunk_size;
13297+
const std::size_t old_size = result.size();
13298+
result.resize(old_size + wanted);
13299+
// resize() is required to make size() exactly old_size + wanted;
13300+
// that is the room get_elements() is allowed to write into
13301+
JSON_ASSERT(result.size() == old_size + wanted);
13302+
const std::size_t bytes_read = ia.get_elements(&result[old_size], wanted);
13303+
chars_read += bytes_read;
13304+
if (JSON_HEDLEY_UNLIKELY(bytes_read < wanted))
13305+
{
13306+
// premature end of input: shrink to what was actually read and
13307+
// report the failure at the first missing byte (same position
13308+
// accounting as get_to() for partial number reads)
13309+
result.resize(old_size + bytes_read);
13310+
++chars_read;
13311+
current = char_traits<char_type>::eof();
13312+
return unexpect_eof(format, context);
13313+
}
13314+
// a full chunk was read; get_elements() never returns more than requested
13315+
JSON_ASSERT(bytes_read == wanted);
13316+
len = static_cast<NumberType>(len - static_cast<NumberType>(wanted));
1323913317
}
13240-
return success;
13318+
return true;
1324113319
}
1324213320

1324313321
/*!

tests/src/unit-cbor.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,3 +2778,43 @@ TEST_CASE("Tagged values")
27782778
CHECK(!jb["binary"].get_binary().has_subtype());
27792779
}
27802780
}
2781+
2782+
TEST_CASE("CBOR large strings and binaries (chunked reader)")
2783+
{
2784+
// The binary reader reads strings and byte arrays in bounded chunks; make
2785+
// sure roundtripping is correct for lengths around and beyond the internal
2786+
// chunk size (4096 bytes), for both vector (iterator) and pointer inputs.
2787+
for (const std::size_t len :
2788+
{
2789+
std::size_t{0}, std::size_t{1}, std::size_t{4095}, std::size_t{4096},
2790+
std::size_t{4097}, std::size_t{8192}, std::size_t{100000}
2791+
})
2792+
{
2793+
CAPTURE(len);
2794+
2795+
// text string
2796+
const json j_string = std::string(len, 'x');
2797+
const std::vector<std::uint8_t> v_string = json::to_cbor(j_string);
2798+
CHECK(json::from_cbor(v_string) == j_string);
2799+
// pointer input exercises the std::memcpy fast path
2800+
CHECK(json::from_cbor(reinterpret_cast<const char*>(v_string.data()),
2801+
reinterpret_cast<const char*>(v_string.data()) + v_string.size()) == j_string);
2802+
2803+
// byte string
2804+
const json j_binary = json::binary(std::vector<std::uint8_t>(len, 0xCD));
2805+
const std::vector<std::uint8_t> v_binary = json::to_cbor(j_binary);
2806+
CHECK(json::from_cbor(v_binary) == j_binary);
2807+
CHECK(json::from_cbor(reinterpret_cast<const char*>(v_binary.data()),
2808+
reinterpret_cast<const char*>(v_binary.data()) + v_binary.size()) == j_binary);
2809+
2810+
// a truncated payload must still be reported as an error, never crash
2811+
// or loop, regardless of the (large) announced length
2812+
if (len > 16)
2813+
{
2814+
std::vector<std::uint8_t> truncated = v_string;
2815+
truncated.resize(truncated.size() - 8);
2816+
json _;
2817+
CHECK_THROWS_AS(_ = json::from_cbor(truncated), json::parse_error);
2818+
}
2819+
}
2820+
}

0 commit comments

Comments
 (0)