diff --git a/docs/mkdocs/docs/api/macros/json_disable_unlocked_file_io.md b/docs/mkdocs/docs/api/macros/json_disable_unlocked_file_io.md index 2c266ba2d..031566cfe 100644 --- a/docs/mkdocs/docs/api/macros/json_disable_unlocked_file_io.md +++ b/docs/mkdocs/docs/api/macros/json_disable_unlocked_file_io.md @@ -5,7 +5,9 @@ ``` 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. +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. 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). @@ -37,9 +39,17 @@ 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 | - 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. + 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. !!! note "Locking" diff --git a/docs/mkdocs/docs/features/parsing/index.md b/docs/mkdocs/docs/features/parsing/index.md index 93a02b501..fa2bec462 100644 --- a/docs/mkdocs/docs/features/parsing/index.md +++ b/docs/mkdocs/docs/features/parsing/index.md @@ -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*` | one character at a time, with the file locked once for the whole parse | +| `#!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 | | other iterator ranges (e.g., `#!cpp std::list`, `#!cpp std::istreambuf_iterator`), 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 diff --git a/docs/mkdocs/docs/features/performance.md b/docs/mkdocs/docs/features/performance.md index 8f4508057..3d0714ca9 100644 --- a/docs/mkdocs/docs/features/performance.md +++ b/docs/mkdocs/docs/features/performance.md @@ -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 still read one character at a time, but `file_input_adapter` -locks the file once per parse instead of once per character. See +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 [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`. diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 1d925588f..7b7222f06 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -58,6 +58,19 @@ #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 @@ -179,8 +192,12 @@ class token_carry_buffer char* m_capacity_end = nullptr; }; /*! -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. +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. */ class file_input_adapter { @@ -199,6 +216,12 @@ 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; @@ -210,6 +233,20 @@ class file_input_adapter unlock(); } +#if JSON_FILE_WINDOW + std::char_traits::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::to_int_type(*next); + } + return get_character_slow(); + } +#else std::char_traits::int_type get_character() noexcept { // the file is locked for the lifetime of the adapter, so the @@ -222,6 +259,7 @@ class file_input_adapter return std::fgetc(m_file); #endif } +#endif // returns the number of characters successfully read template @@ -230,7 +268,134 @@ 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(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::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::to_char_type(c)); + } + } + + void copy_token(std::vector& 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(m_file->_p); + } + const char* buffer_end() const noexcept + { + return m_file->_r > 0 ? reinterpret_cast(m_file->_p) + m_file->_r : reinterpret_cast(m_file->_p); + } + void buffer_bump(std::size_t n) noexcept + { + m_file->_p += n; + m_file->_r -= static_cast(n); + } +#endif + + // the buffer is exhausted: getc_unlocked() refills it + JSON_HEDLEY_NEVER_INLINE std::char_traits::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::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. @@ -259,6 +424,16 @@ 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 }; /*! diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index d3cdb0d3f..b61db5dc6 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -22,6 +22,7 @@ #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 diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index eedb9c561..fa5cede0a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -9167,6 +9167,19 @@ 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 @@ -9288,8 +9301,12 @@ class token_carry_buffer char* m_capacity_end = nullptr; }; /*! -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. +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. */ class file_input_adapter { @@ -9308,6 +9325,12 @@ 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; @@ -9319,6 +9342,20 @@ class file_input_adapter unlock(); } +#if JSON_FILE_WINDOW + std::char_traits::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::to_int_type(*next); + } + return get_character_slow(); + } +#else std::char_traits::int_type get_character() noexcept { // the file is locked for the lifetime of the adapter, so the @@ -9331,6 +9368,7 @@ class file_input_adapter return std::fgetc(m_file); #endif } +#endif // returns the number of characters successfully read template @@ -9339,7 +9377,134 @@ 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(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::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::to_char_type(c)); + } + } + + void copy_token(std::vector& 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(m_file->_p); + } + const char* buffer_end() const noexcept + { + return m_file->_r > 0 ? reinterpret_cast(m_file->_p) + m_file->_r : reinterpret_cast(m_file->_p); + } + void buffer_bump(std::size_t n) noexcept + { + m_file->_p += n; + m_file->_r -= static_cast(n); + } +#endif + + // the buffer is exhausted: getc_unlocked() refills it + JSON_HEDLEY_NEVER_INLINE std::char_traits::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::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. @@ -9368,6 +9533,16 @@ 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 }; /*! @@ -46348,6 +46523,7 @@ struct formatter // 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 diff --git a/tests/src/unit-file_input.cpp b/tests/src/unit-file_input.cpp index e88b6f4d0..0dc03d6ad 100644 --- a/tests/src/unit-file_input.cpp +++ b/tests/src/unit-file_input.cpp @@ -225,6 +225,125 @@ 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 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) + { + CAPTURE(document) + CAPTURE(size) + std::vector buffer(static_cast(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(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 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 = 1; size <= 20; ++size) + { + CAPTURE(size) + std::vector buffer(static_cast(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 cbor = json::to_cbor(value); + for (int size = 1; size <= 20; ++size) + { + CAPTURE(size) + std::vector buffer(static_cast(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}};