Compare commits

..
Author SHA1 Message Date
Niels Lohmann b2666c334e Merge branch 'develop' into issue4552-ignore
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 20:58:34 +02:00
Niels Lohmann ad39cda092 Mention error_handler_t::keep in the README
The README's two notes on dump() throwing for non-UTF-8 strings listed
only the replace and ignore handlers. The FAQ already mentions keep.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 18:40:41 +02:00
Niels Lohmann 8aabb9981f Add error_handler_t::keep to copy invalid UTF-8 bytes unchanged
error_handler_t::ignore drops invalid bytes although its docs promised to copy them (#4552). Add keep, which copies each ill-formed subsequence byte-for-byte while still escaping valid characters.

Fixes #4552

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-26 07:37:05 +02:00
26 changed files with 2633 additions and 1802 deletions
+6 -1
View File
@@ -83,4 +83,9 @@ build_script:
- cmake --build . --config "%configuration%" --parallel 2
test_script:
- ctest -C "%configuration%" --parallel 2 --output-on-failure
- if "%configuration%"=="Release" ctest -C "%configuration%" --parallel 2 --output-on-failure
# On Debug builds, skip test-unicode_all
# as it is extremely slow to run and cause
# occasional timeouts on AppVeyor.
# More info: https://github.com/nlohmann/json/pull/1570
- if "%configuration%"=="Debug" ctest --exclude-regex "test-unicode" -C "%configuration%" --parallel 2 --output-on-failure
+2 -2
View File
@@ -174,7 +174,7 @@ jobs:
- name: Build
run: cmake --build build --parallel 10
- name: Test
run: cd build ; ctest -j 10 -C Debug --output-on-failure
run: cd build ; ctest -j 10 -C Debug --exclude-regex "test-unicode" --output-on-failure
clang-cl-12:
runs-on: windows-2022
@@ -191,7 +191,7 @@ jobs:
- name: Build
run: cmake --build build --config Debug --parallel 10
- name: Test
run: cd build ; ctest -j 10 -C Debug --output-on-failure
run: cd build ; ctest -j 10 -C Debug --exclude-regex "test-unicode" --output-on-failure
ci_module_cpp20:
runs-on: windows-2022
+2 -2
View File
@@ -363,7 +363,7 @@ std::cout << j_string << " == " << serialized_string << std::endl;
[`.dump()`](https://json.nlohmann.me/api/basic_json/dump/) returns the originally stored string value.
Note the library only supports UTF-8. When you store strings with different encodings in the library, calling [`dump()`](https://json.nlohmann.me/api/basic_json/dump/) may throw an exception unless `json::error_handler_t::replace` or `json::error_handler_t::ignore` are used as error handlers.
Note the library only supports UTF-8. When you store strings with different encodings in the library, calling [`dump()`](https://json.nlohmann.me/api/basic_json/dump/) may throw an exception unless `json::error_handler_t::replace`, `json::error_handler_t::ignore`, or `json::error_handler_t::keep` are used as error handlers.
#### To/from streams (e.g., files, string streams)
@@ -1915,7 +1915,7 @@ The library supports **Unicode input** as follows:
- [Unicode noncharacters](https://www.unicode.org/faq/private_use.html#nonchar1) will not be replaced by the library.
- Invalid surrogates (e.g., incomplete pairs such as `\uDEAD`) will yield parse errors.
- The strings stored in the library are UTF-8 encoded. When using the default string type (`std::string`), note that its length/size functions return the number of stored bytes rather than the number of characters or glyphs.
- When you store strings with different encodings in the library, calling [`dump()`](https://json.nlohmann.me/api/basic_json/dump/) may throw an exception unless `json::error_handler_t::replace` or `json::error_handler_t::ignore` are used as error handlers.
- When you store strings with different encodings in the library, calling [`dump()`](https://json.nlohmann.me/api/basic_json/dump/) may throw an exception unless `json::error_handler_t::replace`, `json::error_handler_t::ignore`, or `json::error_handler_t::keep` are used as error handlers.
- To store wide strings (e.g., `std::wstring`), you need to convert them to a UTF-8 encoded `std::string` before, see [an example](https://json.nlohmann.me/home/faq/#wide-string-handling).
### Comments in JSON
+6 -7
View File
@@ -413,14 +413,13 @@ add_custom_target(ci_test_single_header
# Valgrind.
###############################################################################
# The Unicode test (~17M assertions) is too slow under Valgrind.
add_custom_target(ci_test_valgrind
COMMAND CXX=${GCC_TOOL} ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja
-DJSON_BuildTests=ON -DJSON_Valgrind=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_valgrind
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_valgrind
COMMAND cd ${PROJECT_BINARY_DIR}/build_valgrind && ${CMAKE_CTEST_COMMAND} -L valgrind --exclude-regex "test-unicode" --parallel ${N} --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_valgrind && ${CMAKE_CTEST_COMMAND} -L valgrind --parallel ${N} --output-on-failure
COMMENT "Compile and test with Valgrind"
)
@@ -718,7 +717,7 @@ foreach(COMPILER g++-4.8 g++-4.9 g++-5 g++-6 g++-7 g++-8 g++-9 g++-10 g++-11 cla
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_compiler_${COMPILER}
${ADDITIONAL_FLAGS}
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_compiler_${COMPILER}
COMMAND cd ${PROJECT_BINARY_DIR}/build_compiler_${COMPILER} && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_compiler_${COMPILER} && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-unicode" --output-on-failure
COMMENT "Compile and test with ${COMPILER}"
)
endif()
@@ -732,7 +731,7 @@ add_custom_target(ci_test_compiler_default
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_compiler_default
${ADDITIONAL_FLAGS}
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_compiler_default --parallel ${N}
COMMAND cd ${PROJECT_BINARY_DIR}/build_compiler_default && ${CMAKE_CTEST_COMMAND} --parallel ${N} -LE git_required --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_compiler_default && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-unicode" -LE git_required --output-on-failure
COMMENT "Compile and test with default C++ compiler"
)
@@ -770,7 +769,7 @@ add_custom_target(ci_icpc
-DJSON_BuildTests=ON -DJSON_FastTests=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_icpc
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_icpc
COMMAND cd ${PROJECT_BINARY_DIR}/build_icpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_icpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-unicode" --output-on-failure
COMMENT "Compile and test with ICPC"
)
@@ -781,7 +780,7 @@ add_custom_target(ci_icpx
-DJSON_BuildTests=ON -DJSON_FastTests=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_icpx
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_icpx
COMMAND cd ${PROJECT_BINARY_DIR}/build_icpx && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_icpx && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-unicode" --output-on-failure
COMMENT "Compile and test with ICPX (Intel oneAPI DPC++/C++)"
)
@@ -817,7 +816,7 @@ add_custom_target(ci_nvhpc
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_nvhpc
# the pipes are escaped so the surrounding shell passes them to ctest verbatim
# instead of treating them as shell pipe operators
COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-comparison_cpp20\\|test-comparison_legacy_cpp20\\|test-constructor1_cpp11\\|test-deserialization_cpp20" --output-on-failure
COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-unicode\\|test-comparison_cpp20\\|test-comparison_legacy_cpp20\\|test-constructor1_cpp11\\|test-deserialization_cpp20" --output-on-failure
COMMENT "Compile and test with NVIDIA HPC SDK (nvc++)"
)
+10 -4
View File
@@ -25,10 +25,15 @@ and `ensure_ascii` parameters.
result consists of ASCII characters only.
`error_handler` (in)
: how to react on decoding errors; there are three possible values (see [`error_handler_t`](error_handler_t.md):
`strict` (throws an exception in case a decoding error occurs; default), `replace` (replace invalid UTF-8 sequences
with U+FFFD), and `ignore` (ignore invalid UTF-8 sequences during serialization; all valid bytes are copied to the
output unchanged, and invalid bytes are dropped)).
: how to react on decoding errors; there are four possible values (see [`error_handler_t`](error_handler_t.md)):
- `strict`: throw a [`type_error`](../../home/exceptions.md#type-errors) exception in case a decoding error occurs
(default),
- `replace`: replace invalid UTF-8 sequences with U+FFFD (� REPLACEMENT CHARACTER),
- `ignore`: ignore invalid UTF-8 sequences during serialization; all valid bytes are copied to the output unchanged,
and invalid bytes are dropped, and
- `keep`: keep invalid UTF-8 sequences during serialization; all bytes are copied to the output unchanged, so the
result is not valid UTF-8.
## Return value
@@ -94,3 +99,4 @@ Binary values are serialized as an object containing two keys:
- Indentation character `indent_char`, option `ensure_ascii` and exceptions added in version 3.0.0.
- Error handlers added in version 3.4.0.
- Serialization of binary values added in version 3.8.0.
- Error handler value `keep` added in version 3.13.0.
@@ -4,12 +4,13 @@
enum class error_handler_t {
strict,
replace,
ignore
ignore,
keep
};
```
This enumeration is used in the [`dump`](dump.md) function to choose how to treat decoding errors while serializing a
`basic_json` value. Three values are differentiated:
`basic_json` value. Four values are differentiated:
strict
: throw a `type_error` exception in case of invalid UTF-8
@@ -20,6 +21,12 @@ replace
ignore
: ignore invalid UTF-8 sequences; all valid bytes are copied to the output unchanged, and invalid bytes are dropped
keep
: keep invalid UTF-8 sequences; all bytes are copied to the output unchanged. Valid characters are still escaped as
usual (e.g., `"`, `\\`, and control characters), so the result has valid JSON syntax, but it is not valid UTF-8.
In particular, [`parse`](parse.md) rejects it, and with `ensure_ascii` set to `true`, the invalid bytes are the
only non-ASCII bytes of the output.
## Examples
??? example
@@ -40,3 +47,4 @@ ignore
## Version history
- Added in version 3.4.0.
- Added value `keep` in version 3.13.0.
@@ -1,3 +1,4 @@
#include <iomanip>
#include <iostream>
#include <nlohmann/json.hpp>
@@ -21,4 +22,12 @@ int main()
<< "\nstring with ignored invalid characters: "
<< j_invalid.dump(-1, ' ', false, json::error_handler_t::ignore)
<< '\n';
// the invalid byte is kept; print the result byte-wise to make it visible
std::cout << "string with kept invalid characters:";
for (const unsigned char c : j_invalid.dump(-1, ' ', false, json::error_handler_t::keep))
{
std::cout << ' ' << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c);
}
std::cout << '\n';
}
@@ -1,3 +1,4 @@
[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9
string with replaced invalid characters: "ä�ü"
string with ignored invalid characters: "äü"
string with kept invalid characters: 22 c3 a4 a9 c3 bc 22
@@ -64,6 +64,7 @@ serialization fails by default. The fourth argument of `dump` selects an
- `strict` (default) — throw a [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) exception.
- `replace` — replace invalid bytes with the Unicode replacement character U+FFFD (`�`).
- `ignore` — silently drop invalid bytes.
- `keep` — copy invalid bytes to the output unchanged; the result is not valid UTF-8.
??? example
+1
View File
@@ -755,6 +755,7 @@ The `dump()` function only works with UTF-8 encoded strings; that is, if you ass
- Pass an error handler as last parameter to the `dump()` function to avoid this exception:
- `json::error_handler_t::replace` will replace invalid bytes sequences with `U+FFFD`
- `json::error_handler_t::ignore` will silently ignore invalid byte sequences
- `json::error_handler_t::keep` will copy invalid byte sequences to the output unchanged
### json.exception.type_error.317
+1 -1
View File
@@ -85,7 +85,7 @@ The library supports **Unicode input** as follows:
- The library will not replace [Unicode noncharacters](http://www.unicode.org/faq/private_use.html#nonchar1).
- Invalid surrogates (e.g., incomplete pairs such as `\uDEAD`) will yield parse errors.
- The strings stored in the library are UTF-8 encoded. When using the default string type (`std::string`), note that its length/size functions return the number of stored bytes rather than the number of characters or glyphs.
- When you store strings with different encodings in the library, calling [`dump()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a50ec80b02d0f3f51130d4abb5d1cfdc5.html#a50ec80b02d0f3f51130d4abb5d1cfdc5) may throw an exception unless `json::error_handler_t::replace` or `json::error_handler_t::ignore` are used as error handlers.
- When you store strings with different encodings in the library, calling [`dump()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a50ec80b02d0f3f51130d4abb5d1cfdc5.html#a50ec80b02d0f3f51130d4abb5d1cfdc5) may throw an exception unless `json::error_handler_t::replace`, `json::error_handler_t::ignore`, or `json::error_handler_t::keep` are used as error handlers.
In most cases, the parser is right to complain, because the input is not UTF-8 encoded. This is especially true for Microsoft Windows, where Latin-1 or ISO 8859-1 is often the standard encoding.
+52 -1
View File
@@ -48,7 +48,8 @@ enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
ignore, ///< ignore invalid UTF-8 sequences
keep ///< keep invalid UTF-8 sequences; their bytes are copied unchanged
};
template<typename BasicJsonType>
@@ -1019,6 +1020,47 @@ class serializer
break;
}
case error_handler_t::keep:
{
// drop whatever the incomplete sequence left in
// the buffer (only copied if !EnsureAscii) and copy
// the ill-formed bytes from the input instead
bytes = bytes_after_last_accept;
if (undumped_chars > 0)
{
// the pending bytes of the incomplete sequence
// are ill-formed; the current byte may be OK for
// itself, so we would like to read it again
for (std::size_t j = i - undumped_chars; j < i; ++j)
{
string_buffer[bytes++] = s[j];
}
--i;
}
else
{
// the current byte cannot start any sequence
string_buffer[bytes++] = s[i];
}
// write buffer and reset index; there must be 13 bytes
// left, as this is the maximal number of bytes to be
// written ("\uxxxx\uxxxx\0") for one code point
if (string_buffer.size() - bytes < 13)
{
put_buffer(string_buffer, bytes);
bytes = 0;
}
bytes_after_last_accept = bytes;
undumped_chars = 0;
// continue processing the string
state = UTF8_ACCEPT;
break;
}
default: // LCOV_EXCL_LINE
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
}
@@ -1064,6 +1106,15 @@ class serializer
break;
}
case error_handler_t::keep:
{
// write all accepted bytes
put_buffer(string_buffer, bytes_after_last_accept);
// copy the bytes of the incomplete sequence unchanged
put_string(s, s.size() - undumped_chars, s.size());
break;
}
case error_handler_t::replace:
{
// write all accepted bytes
+52 -1
View File
@@ -23931,7 +23931,8 @@ enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
ignore, ///< ignore invalid UTF-8 sequences
keep ///< keep invalid UTF-8 sequences; their bytes are copied unchanged
};
template<typename BasicJsonType>
@@ -24902,6 +24903,47 @@ class serializer
break;
}
case error_handler_t::keep:
{
// drop whatever the incomplete sequence left in
// the buffer (only copied if !EnsureAscii) and copy
// the ill-formed bytes from the input instead
bytes = bytes_after_last_accept;
if (undumped_chars > 0)
{
// the pending bytes of the incomplete sequence
// are ill-formed; the current byte may be OK for
// itself, so we would like to read it again
for (std::size_t j = i - undumped_chars; j < i; ++j)
{
string_buffer[bytes++] = s[j];
}
--i;
}
else
{
// the current byte cannot start any sequence
string_buffer[bytes++] = s[i];
}
// write buffer and reset index; there must be 13 bytes
// left, as this is the maximal number of bytes to be
// written ("\uxxxx\uxxxx\0") for one code point
if (string_buffer.size() - bytes < 13)
{
put_buffer(string_buffer, bytes);
bytes = 0;
}
bytes_after_last_accept = bytes;
undumped_chars = 0;
// continue processing the string
state = UTF8_ACCEPT;
break;
}
default: // LCOV_EXCL_LINE
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
}
@@ -24947,6 +24989,15 @@ class serializer
break;
}
case error_handler_t::keep:
{
// write all accepted bytes
put_buffer(string_buffer, bytes_after_last_accept);
// copy the bytes of the incomplete sequence unchanged
put_string(s, s.size() - undumped_chars, s.size());
break;
}
case error_handler_t::replace:
{
// write all accepted bytes
+3
View File
@@ -129,6 +129,9 @@ json_test_set_test_options(test-disabled_exceptions
#$<$<CXX_COMPILER_ID:MSVC>:/EH>
)
# raise timeout of expensive Unicode test
json_test_set_test_options(test-unicode4 TEST_PROPERTIES TIMEOUT 3000)
#############################################################################
# add unit tests
#############################################################################
-28
View File
@@ -8,7 +8,6 @@
#pragma once
#include <array> // array
#include <cstdint> // uint8_t
#include <cstddef> // size_t
#include <fstream> // ifstream, istreambuf_iterator, ios
@@ -43,33 +42,6 @@ T next_integer_sample(T i, T last, T stride)
return n < last ? n : last;
}
// UTF-8 continuation bytes in [lo, hi] that stand in for all of them in the
// ill-formed UTF-8 tests. Both the lexer's range checks and the serializer's
// decoder (detail::decode) only distinguish the classes 0x80..0x8F, 0x90..0x9F,
// and 0xA0..0xBF, so the first and last byte of each class within [lo, hi]
// exercise every behavior while a test sweeps another byte position through
// all 256 values (#5418). Define JSON_TEST_UTF8_EXHAUSTIVE to get every byte.
inline std::vector<int> utf8_continuation_bytes(int lo, int hi)
{
std::vector<int> result;
#ifdef JSON_TEST_UTF8_EXHAUSTIVE
for (int byte = lo; byte <= hi; ++byte)
{
result.push_back(byte);
}
#else
static const std::array<int, 6> class_ends = {{0x80, 0x8F, 0x90, 0x9F, 0xA0, 0xBF}};
for (const int byte : class_ends)
{
if (lo <= byte && byte <= hi)
{
result.push_back(byte);
}
}
#endif
return result;
}
inline std::vector<std::uint8_t> read_binary_file(const std::string& filename)
{
std::ifstream file(filename, std::ios::binary);
+43 -45
View File
@@ -14,7 +14,7 @@ using nlohmann::json;
#include <fstream>
#include "make_test_data_available.hpp"
TEST_CASE("Binary Formats")
TEST_CASE("Binary Formats" * doctest::skip())
{
SECTION("canada.json")
{
@@ -142,6 +142,48 @@ TEST_CASE("Binary Formats")
CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(84.963));
}
SECTION("jeopardy.json")
{
const auto* filename = TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json";
json j = json::parse(std::ifstream(filename));
const auto json_size = j.dump().size();
const auto bjdata_1_size = json::to_bjdata(j).size();
const auto bjdata_2_size = json::to_bjdata(j, true).size();
const auto bjdata_3_size = json::to_bjdata(j, true, true).size();
const auto bon8_size = json::to_bon8(j).size();
const auto bson_size = json::to_bson({{"", j}}).size(); // wrap array in object for BSON
const auto cbor_size = json::to_cbor(j).size();
const auto msgpack_size = json::to_msgpack(j).size();
const auto ubjson_1_size = json::to_ubjson(j).size();
const auto ubjson_2_size = json::to_ubjson(j, true).size();
const auto ubjson_3_size = json::to_ubjson(j, true, true).size();
CHECK(json_size == 52508728);
CHECK(bjdata_1_size == 50710965);
CHECK(bjdata_2_size == 51144830);
CHECK(bjdata_3_size == 51144830);
CHECK(bon8_size == 45942080);
CHECK(bson_size == 56008520);
CHECK(cbor_size == 46187320);
CHECK(msgpack_size == 46158575);
CHECK(ubjson_1_size == 50710965);
CHECK(ubjson_2_size == 51144830);
CHECK(ubjson_3_size == 49861422);
CHECK((100.0 * double(json_size) / double(json_size)) == Approx(100.0));
CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(96.576));
CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(87.494));
CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(106.665));
CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(87.961));
CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(87.906));
CHECK((100.0 * double(ubjson_1_size) / double(json_size)) == Approx(96.576));
CHECK((100.0 * double(ubjson_2_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(94.958));
}
SECTION("sample.json")
{
const auto* filename = TEST_DATA_DIRECTORY "/json_testsuite/sample.json";
@@ -182,47 +224,3 @@ TEST_CASE("Binary Formats")
CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(89.450));
}
}
// jeopardy.json is 52 MB and produces ~500 MB of serialization output, so it
// is kept apart from the cheap corpus files above (#5418)
TEST_CASE("Binary Formats (jeopardy.json)" * doctest::skip())
{
const auto* filename = TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json";
json j = json::parse(std::ifstream(filename));
const auto json_size = j.dump().size();
const auto bjdata_1_size = json::to_bjdata(j).size();
const auto bjdata_2_size = json::to_bjdata(j, true).size();
const auto bjdata_3_size = json::to_bjdata(j, true, true).size();
const auto bon8_size = json::to_bon8(j).size();
const auto bson_size = json::to_bson({{"", j}}).size(); // wrap array in object for BSON
const auto cbor_size = json::to_cbor(j).size();
const auto msgpack_size = json::to_msgpack(j).size();
const auto ubjson_1_size = json::to_ubjson(j).size();
const auto ubjson_2_size = json::to_ubjson(j, true).size();
const auto ubjson_3_size = json::to_ubjson(j, true, true).size();
CHECK(json_size == 52508728);
CHECK(bjdata_1_size == 50710965);
CHECK(bjdata_2_size == 51144830);
CHECK(bjdata_3_size == 51144830);
CHECK(bon8_size == 45942080);
CHECK(bson_size == 56008520);
CHECK(cbor_size == 46187320);
CHECK(msgpack_size == 46158575);
CHECK(ubjson_1_size == 50710965);
CHECK(ubjson_2_size == 51144830);
CHECK(ubjson_3_size == 49861422);
CHECK((100.0 * double(json_size) / double(json_size)) == Approx(100.0));
CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(96.576));
CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(87.494));
CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(106.665));
CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(87.961));
CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(87.906));
CHECK((100.0 * double(ubjson_1_size) / double(json_size)) == Approx(96.576));
CHECK((100.0 * double(ubjson_2_size) / double(json_size)) == Approx(97.402));
CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(94.958));
}
-99
View File
@@ -1,99 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains the C++17-only part of unit-msgpack.cpp (std::byte
// input). It is kept in a separate translation unit so the (much larger)
// unit-msgpack.cpp does not need to be compiled and run a second time just
// for this one test case (#5418).
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#ifdef JSON_HAS_CPP_17
#include <cstddef>
#include <vector>
// Test suite for verifying MessagePack handling with std::byte input
TEST_CASE("MessagePack with std::byte")
{
SECTION("std::byte compatibility")
{
SECTION("vector roundtrip")
{
json original =
{
{"name", "test"},
{"value", 42},
{"array", {1, 2, 3}}
};
std::vector<uint8_t> temp = json::to_msgpack(original);
// Convert the uint8_t vector to std::byte vector
std::vector<std::byte> msgpack_data(temp.size());
for (size_t i = 0; i < temp.size(); ++i)
{
msgpack_data[i] = std::byte(temp[i]);
}
// Deserialize from std::byte vector back to JSON
json from_bytes;
CHECK_NOTHROW(from_bytes = json::from_msgpack(msgpack_data));
CHECK(from_bytes == original);
}
SECTION("empty vector")
{
const std::vector<std::byte> empty_data;
CHECK_THROWS_WITH_AS([&]()
{
[[maybe_unused]] auto result = json::from_msgpack(empty_data);
return true;
}
(),
"[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing MessagePack value: unexpected end of input",
json::parse_error&);
}
SECTION("comparison with workaround")
{
json original =
{
{"string", "hello"},
{"integer", 42},
{"float", 3.14},
{"boolean", true},
{"null", nullptr},
{"array", {1, 2, 3}},
{"object", {{"key", "value"}}}
};
std::vector<uint8_t> temp = json::to_msgpack(original);
std::vector<std::byte> msgpack_data(temp.size());
for (size_t i = 0; i < temp.size(); ++i)
{
msgpack_data[i] = std::byte(temp[i]);
}
// Attempt direct deserialization using std::byte input
const json direct_result = json::from_msgpack(msgpack_data);
// Test the workaround approach: reinterpret as unsigned char* and use iterator range
const auto* const char_start = reinterpret_cast<unsigned char const*>(msgpack_data.data());
const auto* const char_end = char_start + msgpack_data.size();
json workaround_result = json::from_msgpack(char_start, char_end);
// Verify that the final deserialized JSON matches the original JSON
CHECK(direct_result == workaround_result);
CHECK(direct_result == original);
}
}
}
#endif
+79
View File
@@ -2085,6 +2085,85 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip())
}
}
#ifdef JSON_HAS_CPP_17
// Test suite for verifying MessagePack handling with std::byte input
TEST_CASE("MessagePack with std::byte")
{
SECTION("std::byte compatibility")
{
SECTION("vector roundtrip")
{
json original =
{
{"name", "test"},
{"value", 42},
{"array", {1, 2, 3}}
};
std::vector<uint8_t> temp = json::to_msgpack(original);
// Convert the uint8_t vector to std::byte vector
std::vector<std::byte> msgpack_data(temp.size());
for (size_t i = 0; i < temp.size(); ++i)
{
msgpack_data[i] = std::byte(temp[i]);
}
// Deserialize from std::byte vector back to JSON
json from_bytes;
CHECK_NOTHROW(from_bytes = json::from_msgpack(msgpack_data));
CHECK(from_bytes == original);
}
SECTION("empty vector")
{
const std::vector<std::byte> empty_data;
CHECK_THROWS_WITH_AS([&]()
{
[[maybe_unused]] auto result = json::from_msgpack(empty_data);
return true;
}
(),
"[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing MessagePack value: unexpected end of input",
json::parse_error&);
}
SECTION("comparison with workaround")
{
json original =
{
{"string", "hello"},
{"integer", 42},
{"float", 3.14},
{"boolean", true},
{"null", nullptr},
{"array", {1, 2, 3}},
{"object", {{"key", "value"}}}
};
std::vector<uint8_t> temp = json::to_msgpack(original);
std::vector<std::byte> msgpack_data(temp.size());
for (size_t i = 0; i < temp.size(); ++i)
{
msgpack_data[i] = std::byte(temp[i]);
}
// Attempt direct deserialization using std::byte input
const json direct_result = json::from_msgpack(msgpack_data);
// Test the workaround approach: reinterpret as unsigned char* and use iterator range
const auto* const char_start = reinterpret_cast<unsigned char const*>(msgpack_data.data());
const auto* const char_end = char_start + msgpack_data.size();
json workaround_result = json::from_msgpack(char_start, char_end);
// Verify that the final deserialized JSON matches the original JSON
CHECK(direct_result == workaround_result);
CHECK(direct_result == original);
}
}
}
#endif
// the fake sizes below do not fit into a 32-bit std::size_t
// with clang and libstdc++ 10, the std::filesystem::path conversion that
// C++17 builds consider for every string type is ambiguous for a class
+9
View File
@@ -766,6 +766,15 @@ TEST_CASE("regression tests 2")
CHECK(j == k);
}
SECTION("issue #4552 - UTF-8 invalid characters are not always ignored when dumping with error_handler_t::ignore")
{
json node;
node["test"] = "test\334\005";
CHECK(node.dump(-1, ' ', false, json::error_handler_t::ignore) == "{\"test\":\"test\\u0005\"}");
CHECK(node.dump(-1, ' ', false, json::error_handler_t::keep) == "{\"test\":\"test\334\\u0005\"}");
CHECK(node.dump(-1, ' ', true, json::error_handler_t::keep) == "{\"test\":\"test\334\\u0005\"}");
}
}
TEST_CASE("regression test - parser callback must not lose a duplicate key's prior value")
+37
View File
@@ -92,6 +92,8 @@ TEST_CASE("serialization")
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"äü\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"ä\xEF\xBF\xBDü\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"\\u00e4\\ufffd\\u00fc\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"ä\xA9ü\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"\\u00e4\xA9\\u00fc\"");
}
SECTION("invalid character (regression guard for shared UTF-8 decoder, see #5529)")
@@ -114,6 +116,8 @@ TEST_CASE("serialization")
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\xEF\xBF\xBD\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"123\\ufffd\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"123\xC2\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"123\xC2\"");
}
SECTION("unexpected character")
@@ -126,6 +130,39 @@ TEST_CASE("serialization")
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123456\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\xEF\xBF\xBD\x34\x35\x36\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"123\\ufffd456\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"123\xF1\xB0\x34\x35\x36\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"123\xF1\xB0\x34\x35\x36\"");
}
SECTION("keep: valid characters are still escaped")
{
// an invalid byte followed by characters that must be escaped
const json j = "\xC2\"\\\n\xFF\x05";
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"\xC2\\\"\\\\\\n\xFF\\u0005\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"\xC2\\\"\\\\\\n\xFF\\u0005\"");
}
SECTION("keep: truncated multibyte sequences")
{
CHECK(json("\xF0\x9F\x98").dump(-1, ' ', false, json::error_handler_t::keep) == "\"\xF0\x9F\x98\"");
CHECK(json("\xF0\x9F\x98").dump(-1, ' ', true, json::error_handler_t::keep) == "\"\xF0\x9F\x98\"");
CHECK(json("\xF0\x9F\x98" "a").dump(-1, ' ', false, json::error_handler_t::keep) == "\"\xF0\x9F\x98" "a\"");
CHECK(json("\xF0\x9F\x98" "a").dump(-1, ' ', true, json::error_handler_t::keep) == "\"\xF0\x9F\x98" "a\"");
}
SECTION("keep: long string with many invalid bytes")
{
// exceeds the internal string buffer several times
std::string input;
std::string expected = "\"";
for (int i = 0; i < 2000; ++i)
{
input += "\xFF\xE2\x82\n\xC3\xA4";
expected += "\xFF\xE2\x82\\n\xC3\xA4";
}
expected += "\"";
const json j = input;
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == expected);
}
SECTION("U+FFFD Substitution of Maximal Subparts")
File diff suppressed because it is too large Load Diff
+623
View File
@@ -0,0 +1,623 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale>
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <fstream>
#include <sstream>
#include <iomanip>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
TEST_CASE("Unicode (1/5)" * doctest::skip())
{
SECTION("\\uxxxx sequences")
{
// create an escaped string from a code point
const auto codepoint_to_unicode = [](std::size_t cp)
{
// code points are represented as a six-character sequence: a
// reverse solidus, followed by the lowercase letter u, followed
// by four hexadecimal digits that encode the character's code
// point
std::stringstream ss;
ss << "\\u" << std::setw(4) << std::setfill('0') << std::hex << cp;
return ss.str();
};
SECTION("correct sequences")
{
// generate all UTF-8 code points; in total, 1112064 code points are
// generated: 0x1FFFFF code points - 2048 invalid values between
// 0xD800 and 0xDFFF.
for (std::size_t cp = 0; cp <= 0x10FFFFu; ++cp)
{
// string to store the code point as in \uxxxx format
std::string json_text = "\"";
// decide whether to use one or two \uxxxx sequences
if (cp < 0x10000u)
{
// The Unicode standard permanently reserves these code point
// values for UTF-16 encoding of the high and low surrogates, and
// they will never be assigned a character, so there should be no
// reason to encode them. The official Unicode standard says that
// no UTF forms, including UTF-16, can encode these code points.
if (cp >= 0xD800u && cp <= 0xDFFFu)
{
// if we would not skip these code points, we would get a
// "missing low surrogate" exception
continue;
}
// code points in the Basic Multilingual Plane can be
// represented with one \uxxxx sequence
json_text += codepoint_to_unicode(cp);
}
else
{
// To escape an extended character that is not in the Basic
// Multilingual Plane, the character is represented as a
// 12-character sequence, encoding the UTF-16 surrogate pair
const auto codepoint1 = 0xd800u + (((cp - 0x10000u) >> 10) & 0x3ffu);
const auto codepoint2 = 0xdc00u + ((cp - 0x10000u) & 0x3ffu);
json_text += codepoint_to_unicode(codepoint1) + codepoint_to_unicode(codepoint2);
}
json_text += "\"";
CAPTURE(json_text)
json _;
CHECK_NOTHROW(_ = json::parse(json_text));
}
}
SECTION("incorrect sequences")
{
SECTION("incorrect surrogate values")
{
json _;
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uDC00\\uDC00\""), "[json.exception.parse_error.101] parse error at line 1, column 7: syntax error while parsing value - invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF; last read: '\"\\uDC00'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD7FF\\uDC00\""), "[json.exception.parse_error.101] parse error at line 1, column 13: syntax error while parsing value - invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF; last read: '\"\\uD7FF\\uDC00'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD800]\""), "[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF; last read: '\"\\uD800]'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD800\\v\""), "[json.exception.parse_error.101] parse error at line 1, column 9: syntax error while parsing value - invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF; last read: '\"\\uD800\\v'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD800\\u123\""), "[json.exception.parse_error.101] parse error at line 1, column 13: syntax error while parsing value - invalid string: '\\u' must be followed by 4 hex digits; last read: '\"\\uD800\\u123\"'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD800\\uDBFF\""), "[json.exception.parse_error.101] parse error at line 1, column 13: syntax error while parsing value - invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF; last read: '\"\\uD800\\uDBFF'", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::parse("\"\\uD800\\uE000\""), "[json.exception.parse_error.101] parse error at line 1, column 13: syntax error while parsing value - invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF; last read: '\"\\uD800\\uE000'", json::parse_error&);
}
}
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
SECTION("incorrect sequences")
{
SECTION("high surrogate without low surrogate")
{
// D800..DBFF are high surrogates and must be followed by low
// surrogates DC00..DFFF; here, nothing follows
for (std::size_t cp = 0xD800u; cp <= 0xDBFFu; ++cp)
{
std::string json_text = "\"" + codepoint_to_unicode(cp) + "\"";
CAPTURE(json_text)
CHECK_THROWS_AS(json::parse(json_text), json::parse_error&);
}
}
SECTION("high surrogate with wrong low surrogate")
{
// D800..DBFF are high surrogates and must be followed by low
// surrogates DC00..DFFF; here a different sequence follows
for (std::size_t cp1 = 0xD800u; cp1 <= 0xDBFFu; ++cp1)
{
for (std::size_t cp2 = 0x0000u; cp2 <= 0xFFFFu; ++cp2)
{
if (0xDC00u <= cp2 && cp2 <= 0xDFFFu)
{
continue;
}
std::string json_text = "\"" + codepoint_to_unicode(cp1) + codepoint_to_unicode(cp2) + "\"";
CAPTURE(json_text)
CHECK_THROWS_AS(json::parse(json_text), json::parse_error&);
}
}
}
SECTION("low surrogate without high surrogate")
{
// low surrogates DC00..DFFF must follow high surrogates; here,
// they occur alone
for (std::size_t cp = 0xDC00u; cp <= 0xDFFFu; ++cp)
{
std::string json_text = "\"" + codepoint_to_unicode(cp) + "\"";
CAPTURE(json_text)
CHECK_THROWS_AS(json::parse(json_text), json::parse_error&);
}
}
}
#endif
}
SECTION("read all unicode characters")
{
// read a file with all Unicode characters stored as single-character
// strings in a JSON array
std::ifstream f(TEST_DATA_DIRECTORY "/json_nlohmann_tests/all_unicode.json");
json j;
CHECK_NOTHROW(f >> j);
// the array has 1112064 + 1 elements (a terminating "null" value)
// Note: 1112064 = 0x1FFFFF code points - 2048 invalid values between
// 0xD800 and 0xDFFF.
CHECK(j.size() == 1112065);
SECTION("check JSON Pointers")
{
for (const auto& s : j)
{
// skip non-string JSON values
if (!s.is_string())
{
continue;
}
auto ptr = s.get<std::string>();
// tilde must be followed by 0 or 1
if (ptr == "~")
{
ptr += "0";
}
// JSON Pointers must begin with "/"
ptr.insert(0, "/");
CHECK_NOTHROW(json::json_pointer("/" + ptr));
// check escape/unescape roundtrip
auto escaped = nlohmann::detail::escape(ptr);
nlohmann::detail::unescape(escaped);
CHECK(escaped == ptr);
}
}
}
SECTION("ignore byte-order-mark")
{
SECTION("in a stream")
{
// read a file with a UTF-8 BOM
std::ifstream f(TEST_DATA_DIRECTORY "/json_nlohmann_tests/bom.json");
json j;
CHECK_NOTHROW(f >> j);
}
SECTION("with an iterator")
{
std::string i = "\xef\xbb\xbf{\n \"foo\": true\n}";
json _;
CHECK_NOTHROW(_ = json::parse(i.begin(), i.end()));
}
}
SECTION("error for incomplete/wrong BOM")
{
json _;
CHECK_THROWS_AS(_ = json::parse("\xef\xbb"), json::parse_error&);
CHECK_THROWS_AS(_ = json::parse("\xef\xbb\xbb"), json::parse_error&);
}
}
namespace
{
void roundtrip(bool success_expected, const std::string& s);
void roundtrip(bool success_expected, const std::string& s)
{
CAPTURE(s)
json _;
// create JSON string value
const json j = s;
// create JSON text
const std::string ps = std::string("\"") + s + "\"";
if (success_expected)
{
// serialization succeeds
// dump() is nodiscard; this only checks that dumping does not throw
CHECK_NOTHROW(utils::ignore_return_value(j.dump()));
// exclude parse test for U+0000
if (s[0] != '\0')
{
// parsing JSON text succeeds
CHECK_NOTHROW(_ = json::parse(ps));
}
// roundtrip succeeds
CHECK_NOTHROW(_ = json::parse(j.dump()));
// after roundtrip, the same string is stored
const json jr = json::parse(j.dump());
CHECK(jr.get<std::string>() == s);
}
else
{
// serialization fails
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&);
// parsing JSON text fails
CHECK_THROWS_AS(_ = json::parse(ps), json::parse_error&);
}
}
} // namespace
TEST_CASE("Markus Kuhn's UTF-8 decoder capability and stress test")
{
// Markus Kuhn <http://www.cl.cam.ac.uk/~mgk25/> - 2015-08-28 - CC BY 4.0
// http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
SECTION("1 Some correct UTF-8 text")
{
roundtrip(true, "κόσμε");
}
SECTION("2 Boundary condition test cases")
{
SECTION("2.1 First possible sequence of a certain length")
{
// 2.1.1 1 byte (U-00000000)
roundtrip(true, std::string("\0", 1));
// 2.1.2 2 bytes (U-00000080)
roundtrip(true, "\xc2\x80");
// 2.1.3 3 bytes (U-00000800)
roundtrip(true, "\xe0\xa0\x80");
// 2.1.4 4 bytes (U-00010000)
roundtrip(true, "\xf0\x90\x80\x80");
// 2.1.5 5 bytes (U-00200000)
roundtrip(false, "\xF8\x88\x80\x80\x80");
// 2.1.6 6 bytes (U-04000000)
roundtrip(false, "\xFC\x84\x80\x80\x80\x80");
}
SECTION("2.2 Last possible sequence of a certain length")
{
// 2.2.1 1 byte (U-0000007F)
roundtrip(true, "\x7f");
// 2.2.2 2 bytes (U-000007FF)
roundtrip(true, "\xdf\xbf");
// 2.2.3 3 bytes (U-0000FFFF)
roundtrip(true, "\xef\xbf\xbf");
// 2.2.4 4 bytes (U-001FFFFF)
roundtrip(false, "\xF7\xBF\xBF\xBF");
// 2.2.5 5 bytes (U-03FFFFFF)
roundtrip(false, "\xFB\xBF\xBF\xBF\xBF");
// 2.2.6 6 bytes (U-7FFFFFFF)
roundtrip(false, "\xFD\xBF\xBF\xBF\xBF\xBF");
}
SECTION("2.3 Other boundary conditions")
{
// 2.3.1 U-0000D7FF = ed 9f bf
roundtrip(true, "\xed\x9f\xbf");
// 2.3.2 U-0000E000 = ee 80 80
roundtrip(true, "\xee\x80\x80");
// 2.3.3 U-0000FFFD = ef bf bd
roundtrip(true, "\xef\xbf\xbd");
// 2.3.4 U-0010FFFF = f4 8f bf bf
roundtrip(true, "\xf4\x8f\xbf\xbf");
// 2.3.5 U-00110000 = f4 90 80 80
roundtrip(false, "\xf4\x90\x80\x80");
}
}
SECTION("3 Malformed sequences")
{
SECTION("3.1 Unexpected continuation bytes")
{
// Each unexpected continuation byte should be separately signalled as a
// malformed sequence of its own.
// 3.1.1 First continuation byte 0x80
roundtrip(false, "\x80");
// 3.1.2 Last continuation byte 0xbf
roundtrip(false, "\xbf");
// 3.1.3 2 continuation bytes
roundtrip(false, "\x80\xbf");
// 3.1.4 3 continuation bytes
roundtrip(false, "\x80\xbf\x80");
// 3.1.5 4 continuation bytes
roundtrip(false, "\x80\xbf\x80\xbf");
// 3.1.6 5 continuation bytes
roundtrip(false, "\x80\xbf\x80\xbf\x80");
// 3.1.7 6 continuation bytes
roundtrip(false, "\x80\xbf\x80\xbf\x80\xbf");
// 3.1.8 7 continuation bytes
roundtrip(false, "\x80\xbf\x80\xbf\x80\xbf\x80");
// 3.1.9 Sequence of all 64 possible continuation bytes (0x80-0xbf)
roundtrip(false, "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf");
}
SECTION("3.2 Lonely start characters")
{
// 3.2.1 All 32 first bytes of 2-byte sequences (0xc0-0xdf)
roundtrip(false, "\xc0 \xc1 \xc2 \xc3 \xc4 \xc5 \xc6 \xc7 \xc8 \xc9 \xca \xcb \xcc \xcd \xce \xcf \xd0 \xd1 \xd2 \xd3 \xd4 \xd5 \xd6 \xd7 \xd8 \xd9 \xda \xdb \xdc \xdd \xde \xdf");
// 3.2.2 All 16 first bytes of 3-byte sequences (0xe0-0xef)
roundtrip(false, "\xe0 \xe1 \xe2 \xe3 \xe4 \xe5 \xe6 \xe7 \xe8 \xe9 \xea \xeb \xec \xed \xee \xef");
// 3.2.3 All 8 first bytes of 4-byte sequences (0xf0-0xf7)
roundtrip(false, "\xf0 \xf1 \xf2 \xf3 \xf4 \xf5 \xf6 \xf7");
// 3.2.4 All 4 first bytes of 5-byte sequences (0xf8-0xfb)
roundtrip(false, "\xf8 \xf9 \xfa \xfb");
// 3.2.5 All 2 first bytes of 6-byte sequences (0xfc-0xfd)
roundtrip(false, "\xfc \xfd");
}
SECTION("3.3 Sequences with last continuation byte missing")
{
// All bytes of an incomplete sequence should be signalled as a single
// malformed sequence, i.e., you should see only a single replacement
// character in each of the next 10 tests. (Characters as in section 2)
// 3.3.1 2-byte sequence with last byte missing (U+0000)
roundtrip(false, "\xc0");
// 3.3.2 3-byte sequence with last byte missing (U+0000)
roundtrip(false, "\xe0\x80");
// 3.3.3 4-byte sequence with last byte missing (U+0000)
roundtrip(false, "\xf0\x80\x80");
// 3.3.4 5-byte sequence with last byte missing (U+0000)
roundtrip(false, "\xf8\x80\x80\x80");
// 3.3.5 6-byte sequence with last byte missing (U+0000)
roundtrip(false, "\xfc\x80\x80\x80\x80");
// 3.3.6 2-byte sequence with last byte missing (U-000007FF)
roundtrip(false, "\xdf");
// 3.3.7 3-byte sequence with last byte missing (U-0000FFFF)
roundtrip(false, "\xef\xbf");
// 3.3.8 4-byte sequence with last byte missing (U-001FFFFF)
roundtrip(false, "\xf7\xbf\xbf");
// 3.3.9 5-byte sequence with last byte missing (U-03FFFFFF)
roundtrip(false, "\xfb\xbf\xbf\xbf");
// 3.3.10 6-byte sequence with last byte missing (U-7FFFFFFF)
roundtrip(false, "\xfd\xbf\xbf\xbf\xbf");
}
SECTION("3.4 Concatenation of incomplete sequences")
{
// All the 10 sequences of 3.3 concatenated, you should see 10 malformed
// sequences being signalled:
roundtrip(false, "\xc0\xe0\x80\xf0\x80\x80\xf8\x80\x80\x80\xfc\x80\x80\x80\x80\xdf\xef\xbf\xf7\xbf\xbf\xfb\xbf\xbf\xbf\xfd\xbf\xbf\xbf\xbf");
}
SECTION("3.5 Impossible bytes")
{
// The following two bytes cannot appear in a correct UTF-8 string
// 3.5.1 fe
roundtrip(false, "\xfe");
// 3.5.2 ff
roundtrip(false, "\xff");
// 3.5.3 fe fe ff ff
roundtrip(false, "\xfe\xfe\xff\xff");
}
}
SECTION("4 Overlong sequences")
{
// The following sequences are not malformed according to the letter of
// the Unicode 2.0 standard. However, they are longer then necessary and
// a correct UTF-8 encoder is not allowed to produce them. A "safe UTF-8
// decoder" should reject them just like malformed sequences for two
// reasons: (1) It helps to debug applications if overlong sequences are
// not treated as valid representations of characters, because this helps
// to spot problems more quickly. (2) Overlong sequences provide
// alternative representations of characters, that could maliciously be
// used to bypass filters that check only for ASCII characters. For
// instance, a 2-byte encoded line feed (LF) would not be caught by a
// line counter that counts only 0x0a bytes, but it would still be
// processed as a line feed by an unsafe UTF-8 decoder later in the
// pipeline. From a security point of view, ASCII compatibility of UTF-8
// sequences means also, that ASCII characters are *only* allowed to be
// represented by ASCII bytes in the range 0x00-0x7f. To ensure this
// aspect of ASCII compatibility, use only "safe UTF-8 decoders" that
// reject overlong UTF-8 sequences for which a shorter encoding exists.
SECTION("4.1 Examples of an overlong ASCII character")
{
// With a safe UTF-8 decoder, all the following five overlong
// representations of the ASCII character slash ("/") should be rejected
// like a malformed UTF-8 sequence, for instance by substituting it with
// a replacement character. If you see a slash below, you do not have a
// safe UTF-8 decoder!
// 4.1.1 U+002F = c0 af
roundtrip(false, "\xc0\xaf");
// 4.1.2 U+002F = e0 80 af
roundtrip(false, "\xe0\x80\xaf");
// 4.1.3 U+002F = f0 80 80 af
roundtrip(false, "\xf0\x80\x80\xaf");
// 4.1.4 U+002F = f8 80 80 80 af
roundtrip(false, "\xf8\x80\x80\x80\xaf");
// 4.1.5 U+002F = fc 80 80 80 80 af
roundtrip(false, "\xfc\x80\x80\x80\x80\xaf");
}
SECTION("4.2 Maximum overlong sequences")
{
// Below you see the highest Unicode value that is still resulting in an
// overlong sequence if represented with the given number of bytes. This
// is a boundary test for safe UTF-8 decoders. All five characters should
// be rejected like malformed UTF-8 sequences.
// 4.2.1 U-0000007F = c1 bf
roundtrip(false, "\xc1\xbf");
// 4.2.2 U-000007FF = e0 9f bf
roundtrip(false, "\xe0\x9f\xbf");
// 4.2.3 U-0000FFFF = f0 8f bf bf
roundtrip(false, "\xf0\x8f\xbf\xbf");
// 4.2.4 U-001FFFFF = f8 87 bf bf bf
roundtrip(false, "\xf8\x87\xbf\xbf\xbf");
// 4.2.5 U-03FFFFFF = fc 83 bf bf bf bf
roundtrip(false, "\xfc\x83\xbf\xbf\xbf\xbf");
}
SECTION("4.3 Overlong representation of the NUL character")
{
// The following five sequences should also be rejected like malformed
// UTF-8 sequences and should not be treated like the ASCII NUL
// character.
// 4.3.1 U+0000 = c0 80
roundtrip(false, "\xc0\x80");
// 4.3.2 U+0000 = e0 80 80
roundtrip(false, "\xe0\x80\x80");
// 4.3.3 U+0000 = f0 80 80 80
roundtrip(false, "\xf0\x80\x80\x80");
// 4.3.4 U+0000 = f8 80 80 80 80
roundtrip(false, "\xf8\x80\x80\x80\x80");
// 4.3.5 U+0000 = fc 80 80 80 80 80
roundtrip(false, "\xfc\x80\x80\x80\x80\x80");
}
}
SECTION("5 Illegal code positions")
{
// The following UTF-8 sequences should be rejected like malformed
// sequences, because they never represent valid ISO 10646 characters and
// a UTF-8 decoder that accepts them might introduce security problems
// comparable to overlong UTF-8 sequences.
SECTION("5.1 Single UTF-16 surrogates")
{
// 5.1.1 U+D800 = ed a0 80
roundtrip(false, "\xed\xa0\x80");
// 5.1.2 U+DB7F = ed ad bf
roundtrip(false, "\xed\xad\xbf");
// 5.1.3 U+DB80 = ed ae 80
roundtrip(false, "\xed\xae\x80");
// 5.1.4 U+DBFF = ed af bf
roundtrip(false, "\xed\xaf\xbf");
// 5.1.5 U+DC00 = ed b0 80
roundtrip(false, "\xed\xb0\x80");
// 5.1.6 U+DF80 = ed be 80
roundtrip(false, "\xed\xbe\x80");
// 5.1.7 U+DFFF = ed bf bf
roundtrip(false, "\xed\xbf\xbf");
}
SECTION("5.2 Paired UTF-16 surrogates")
{
// 5.2.1 U+D800 U+DC00 = ed a0 80 ed b0 80
roundtrip(false, "\xed\xa0\x80\xed\xb0\x80");
// 5.2.2 U+D800 U+DFFF = ed a0 80 ed bf bf
roundtrip(false, "\xed\xa0\x80\xed\xbf\xbf");
// 5.2.3 U+DB7F U+DC00 = ed ad bf ed b0 80
roundtrip(false, "\xed\xad\xbf\xed\xb0\x80");
// 5.2.4 U+DB7F U+DFFF = ed ad bf ed bf bf
roundtrip(false, "\xed\xad\xbf\xed\xbf\xbf");
// 5.2.5 U+DB80 U+DC00 = ed ae 80 ed b0 80
roundtrip(false, "\xed\xae\x80\xed\xb0\x80");
// 5.2.6 U+DB80 U+DFFF = ed ae 80 ed bf bf
roundtrip(false, "\xed\xae\x80\xed\xbf\xbf");
// 5.2.7 U+DBFF U+DC00 = ed af bf ed b0 80
roundtrip(false, "\xed\xaf\xbf\xed\xb0\x80");
// 5.2.8 U+DBFF U+DFFF = ed af bf ed bf bf
roundtrip(false, "\xed\xaf\xbf\xed\xbf\xbf");
}
SECTION("5.3 Noncharacter code positions")
{
// The following "noncharacters" are "reserved for internal use" by
// applications, and according to older versions of the Unicode Standard
// "should never be interchanged". Unicode Corrigendum #9 dropped the
// latter restriction. Nevertheless, their presence in incoming UTF-8 data
// can remain a potential security risk, depending on what use is made of
// these codes subsequently. Examples of such internal use:
//
// - Some file APIs with 16-bit characters may use the integer value -1
// = U+FFFF to signal an end-of-file (EOF) or error condition.
//
// - In some UTF-16 receivers, code point U+FFFE might trigger a
// byte-swap operation (to convert between UTF-16LE and UTF-16BE).
//
// With such internal use of noncharacters, it may be desirable and safer
// to block those code points in UTF-8 decoders, as they should never
// occur legitimately in incoming UTF-8 data, and could trigger unsafe
// behaviour in subsequent processing.
// Particularly problematic noncharacters in 16-bit applications:
// 5.3.1 U+FFFE = ef bf be
roundtrip(true, "\xef\xbf\xbe");
// 5.3.2 U+FFFF = ef bf bf
roundtrip(true, "\xef\xbf\xbf");
// 5.3.3 U+FDD0 .. U+FDEF
roundtrip(true, "\xEF\xB7\x90");
roundtrip(true, "\xEF\xB7\x91");
roundtrip(true, "\xEF\xB7\x92");
roundtrip(true, "\xEF\xB7\x93");
roundtrip(true, "\xEF\xB7\x94");
roundtrip(true, "\xEF\xB7\x95");
roundtrip(true, "\xEF\xB7\x96");
roundtrip(true, "\xEF\xB7\x97");
roundtrip(true, "\xEF\xB7\x98");
roundtrip(true, "\xEF\xB7\x99");
roundtrip(true, "\xEF\xB7\x9A");
roundtrip(true, "\xEF\xB7\x9B");
roundtrip(true, "\xEF\xB7\x9C");
roundtrip(true, "\xEF\xB7\x9D");
roundtrip(true, "\xEF\xB7\x9E");
roundtrip(true, "\xEF\xB7\x9F");
roundtrip(true, "\xEF\xB7\xA0");
roundtrip(true, "\xEF\xB7\xA1");
roundtrip(true, "\xEF\xB7\xA2");
roundtrip(true, "\xEF\xB7\xA3");
roundtrip(true, "\xEF\xB7\xA4");
roundtrip(true, "\xEF\xB7\xA5");
roundtrip(true, "\xEF\xB7\xA6");
roundtrip(true, "\xEF\xB7\xA7");
roundtrip(true, "\xEF\xB7\xA8");
roundtrip(true, "\xEF\xB7\xA9");
roundtrip(true, "\xEF\xB7\xAA");
roundtrip(true, "\xEF\xB7\xAB");
roundtrip(true, "\xEF\xB7\xAC");
roundtrip(true, "\xEF\xB7\xAD");
roundtrip(true, "\xEF\xB7\xAE");
roundtrip(true, "\xEF\xB7\xAF");
// 5.3.4 U+nFFFE U+nFFFF (for n = 1..10)
roundtrip(true, "\xF0\x9F\xBF\xBF");
roundtrip(true, "\xF0\xAF\xBF\xBF");
roundtrip(true, "\xF0\xBF\xBF\xBF");
roundtrip(true, "\xF1\x8F\xBF\xBF");
roundtrip(true, "\xF1\x9F\xBF\xBF");
roundtrip(true, "\xF1\xAF\xBF\xBF");
roundtrip(true, "\xF1\xBF\xBF\xBF");
roundtrip(true, "\xF2\x8F\xBF\xBF");
roundtrip(true, "\xF2\x9F\xBF\xBF");
roundtrip(true, "\xF2\xAF\xBF\xBF");
}
}
}
+636
View File
@@ -0,0 +1,636 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale>
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
// this test suite uses static variables with non-trivial destructors
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
extern size_t calls;
size_t calls = 0;
void check_utf8dump(bool success_expected, int byte1, int byte2, int byte3, int byte4);
void check_utf8dump(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
static std::string json_string;
json_string.clear();
CAPTURE(byte1)
CAPTURE(byte2)
CAPTURE(byte3)
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
json_string += std::string(1, static_cast<char>(byte4));
}
CAPTURE(json_string)
// store the string in a JSON value
static json j;
static json j2;
j = json_string;
j2 = "abc" + json_string + "xyz";
static std::string s_ignored;
static std::string s_ignored2;
static std::string s_ignored_ascii;
static std::string s_ignored2_ascii;
static std::string s_replaced;
static std::string s_replaced2;
static std::string s_replaced_ascii;
static std::string s_replaced2_ascii;
static std::string s_kept;
static std::string s_kept2;
static std::string s_kept_ascii;
// dumping with ignore/replace/keep must not throw in any case
s_ignored = j.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored2 = j2.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored_ascii = j.dump(-1, ' ', true, json::error_handler_t::ignore);
s_ignored2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::ignore);
s_replaced = j.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced2 = j2.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced_ascii = j.dump(-1, ' ', true, json::error_handler_t::replace);
s_replaced2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::replace);
s_kept = j.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept2 = j2.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept_ascii = j.dump(-1, ' ', true, json::error_handler_t::keep);
if (success_expected)
{
static std::string s_strict;
// strict mode must not throw if success is expected
s_strict = j.dump();
// all dumps should agree on the string
CHECK(s_strict == s_ignored);
CHECK(s_strict == s_replaced);
CHECK(s_strict == s_kept);
}
else
{
// strict mode must throw if success is not expected
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&);
// ignore and replace must create different dumps
CHECK(s_ignored != s_replaced);
// check that replace string contains a replacement character
CHECK(s_replaced.find("\xEF\xBF\xBD") != std::string::npos);
// ignore drops the invalid bytes, keep copies them
CHECK(s_ignored != s_kept);
CHECK(s_ignored_ascii != s_kept_ascii);
// unless a byte needs escaping, keep copies the input unchanged
const bool needs_escaping = std::any_of(json_string.begin(), json_string.end(), [](char c)
{
return static_cast<unsigned char>(c) < 0x20 || c == '"' || c == '\\';
});
if (!needs_escaping)
{
CHECK(s_kept == "\"" + json_string + "\"");
}
}
// check that prefix and suffix are preserved
CHECK(s_ignored2.substr(1, 3) == "abc");
CHECK(s_ignored2.substr(s_ignored2.size() - 4, 3) == "xyz");
CHECK(s_ignored2_ascii.substr(1, 3) == "abc");
CHECK(s_ignored2_ascii.substr(s_ignored2_ascii.size() - 4, 3) == "xyz");
CHECK(s_replaced2.substr(1, 3) == "abc");
CHECK(s_replaced2.substr(s_replaced2.size() - 4, 3) == "xyz");
CHECK(s_replaced2_ascii.substr(1, 3) == "abc");
CHECK(s_replaced2_ascii.substr(s_replaced2_ascii.size() - 4, 3) == "xyz");
CHECK(s_kept2.substr(1, 3) == "abc");
CHECK(s_kept2.substr(s_kept2.size() - 4, 3) == "xyz");
}
void check_utf8string(bool success_expected, int byte1, int byte2, int byte3, int byte4);
// create and check a JSON string with up to four UTF-8 bytes
void check_utf8string(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
if (++calls % 100000 == 0)
{
std::cout << calls << " of 455355 UTF-8 strings checked" << std::endl; // NOLINT(performance-avoid-endl)
}
static std::string json_string;
json_string = "\"";
CAPTURE(byte1)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
CAPTURE(byte2)
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
CAPTURE(byte3)
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte4));
}
json_string += "\"";
CAPTURE(json_string)
json _;
if (success_expected)
{
CHECK_NOTHROW(_ = json::parse(json_string));
}
else
{
CHECK_THROWS_AS(_ = json::parse(json_string), json::parse_error&);
}
}
} // namespace
TEST_CASE("Unicode (2/5)" * doctest::skip())
{
SECTION("RFC 3629")
{
/*
RFC 3629 describes in Sect. 4 the syntax of UTF-8 byte sequences as
follows:
A UTF-8 string is a sequence of octets representing a sequence of UCS
characters. An octet sequence is valid UTF-8 only if it matches the
following syntax, which is derived from the rules for encoding UTF-8
and is expressed in the ABNF of [RFC2234].
UTF8-octets = *( UTF8-char )
UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1 = %x00-7F
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
%xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
%xF4 %x80-8F 2( UTF8-tail )
UTF8-tail = %x80-BF
*/
SECTION("ill-formed first byte")
{
for (int byte1 = 0x80; byte1 <= 0xC1; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
for (int byte1 = 0xF5; byte1 <= 0xFF; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("UTF8-1 (x00-x7F)")
{
SECTION("well-formed")
{
for (int byte1 = 0x00; byte1 <= 0x7F; ++byte1)
{
// unescaped control characters are parse errors in JSON
if (0x00 <= byte1 && byte1 <= 0x1F)
{
check_utf8string(false, byte1);
continue;
}
// a single quote is a parse error in JSON
if (byte1 == 0x22)
{
check_utf8string(false, byte1);
continue;
}
// a single backslash is a parse error in JSON
if (byte1 == 0x5C)
{
check_utf8string(false, byte1);
continue;
}
// all other characters are OK
check_utf8string(true, byte1);
check_utf8dump(true, byte1);
}
}
}
SECTION("UTF8-2 (xC2-xDF UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xC2; byte1 <= 0xDF; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
check_utf8string(true, byte1, byte2);
check_utf8dump(true, byte1, byte2);
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xC2; byte1 <= 0xDF; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xC2; byte1 <= 0xDF; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
}
SECTION("UTF8-3 (xE0 xA0-BF UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xE0; byte1 <= 0xE0; ++byte1)
{
for (int byte2 = 0xA0; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(true, byte1, byte2, byte3);
check_utf8dump(true, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xE0; byte1 <= 0xE0; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xE0; byte1 <= 0xE0; ++byte1)
{
for (int byte2 = 0xA0; byte2 <= 0xBF; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xE0; byte1 <= 0xE0; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0xA0 <= byte2 && byte2 <= 0xBF)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xE0; byte1 <= 0xE0; ++byte1)
{
for (int byte2 = 0xA0; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
}
SECTION("UTF8-3 (xE1-xEC UTF8-tail UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xE1; byte1 <= 0xEC; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(true, byte1, byte2, byte3);
check_utf8dump(true, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xE1; byte1 <= 0xEC; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xE1; byte1 <= 0xEC; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xE1; byte1 <= 0xEC; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0xBF)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xE1; byte1 <= 0xEC; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
}
SECTION("UTF8-3 (xED x80-9F UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xED; byte1 <= 0xED; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x9F; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(true, byte1, byte2, byte3);
check_utf8dump(true, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xED; byte1 <= 0xED; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xED; byte1 <= 0xED; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x9F; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xED; byte1 <= 0xED; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0x9F)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xED; byte1 <= 0xED; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x9F; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
}
SECTION("UTF8-3 (xEE-xEF UTF8-tail UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xEE; byte1 <= 0xEF; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(true, byte1, byte2, byte3);
check_utf8dump(true, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xEE; byte1 <= 0xEF; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xEE; byte1 <= 0xEF; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xEE; byte1 <= 0xEF; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0xBF)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xEE; byte1 <= 0xEF; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
}
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
+350
View File
@@ -0,0 +1,350 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale>
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
// this test suite uses static variables with non-trivial destructors
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
extern size_t calls;
size_t calls = 0;
void check_utf8dump(bool success_expected, int byte1, int byte2, int byte3, int byte4);
void check_utf8dump(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
static std::string json_string;
json_string.clear();
CAPTURE(byte1)
CAPTURE(byte2)
CAPTURE(byte3)
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
json_string += std::string(1, static_cast<char>(byte4));
}
CAPTURE(json_string)
// store the string in a JSON value
static json j;
static json j2;
j = json_string;
j2 = "abc" + json_string + "xyz";
static std::string s_ignored;
static std::string s_ignored2;
static std::string s_ignored_ascii;
static std::string s_ignored2_ascii;
static std::string s_replaced;
static std::string s_replaced2;
static std::string s_replaced_ascii;
static std::string s_replaced2_ascii;
static std::string s_kept;
static std::string s_kept2;
static std::string s_kept_ascii;
// dumping with ignore/replace/keep must not throw in any case
s_ignored = j.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored2 = j2.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored_ascii = j.dump(-1, ' ', true, json::error_handler_t::ignore);
s_ignored2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::ignore);
s_replaced = j.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced2 = j2.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced_ascii = j.dump(-1, ' ', true, json::error_handler_t::replace);
s_replaced2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::replace);
s_kept = j.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept2 = j2.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept_ascii = j.dump(-1, ' ', true, json::error_handler_t::keep);
if (success_expected)
{
static std::string s_strict;
// strict mode must not throw if success is expected
s_strict = j.dump();
// all dumps should agree on the string
CHECK(s_strict == s_ignored);
CHECK(s_strict == s_replaced);
CHECK(s_strict == s_kept);
}
else
{
// strict mode must throw if success is not expected
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&);
// ignore and replace must create different dumps
CHECK(s_ignored != s_replaced);
// check that replace string contains a replacement character
CHECK(s_replaced.find("\xEF\xBF\xBD") != std::string::npos);
// ignore drops the invalid bytes, keep copies them
CHECK(s_ignored != s_kept);
CHECK(s_ignored_ascii != s_kept_ascii);
// unless a byte needs escaping, keep copies the input unchanged
const bool needs_escaping = std::any_of(json_string.begin(), json_string.end(), [](char c)
{
return static_cast<unsigned char>(c) < 0x20 || c == '"' || c == '\\';
});
if (!needs_escaping)
{
CHECK(s_kept == "\"" + json_string + "\"");
}
}
// check that prefix and suffix are preserved
CHECK(s_ignored2.substr(1, 3) == "abc");
CHECK(s_ignored2.substr(s_ignored2.size() - 4, 3) == "xyz");
CHECK(s_ignored2_ascii.substr(1, 3) == "abc");
CHECK(s_ignored2_ascii.substr(s_ignored2_ascii.size() - 4, 3) == "xyz");
CHECK(s_replaced2.substr(1, 3) == "abc");
CHECK(s_replaced2.substr(s_replaced2.size() - 4, 3) == "xyz");
CHECK(s_replaced2_ascii.substr(1, 3) == "abc");
CHECK(s_replaced2_ascii.substr(s_replaced2_ascii.size() - 4, 3) == "xyz");
CHECK(s_kept2.substr(1, 3) == "abc");
CHECK(s_kept2.substr(s_kept2.size() - 4, 3) == "xyz");
}
void check_utf8string(bool success_expected, int byte1, int byte2, int byte3, int byte4);
// create and check a JSON string with up to four UTF-8 bytes
void check_utf8string(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
if (++calls % 100000 == 0)
{
std::cout << calls << " of 1641521 UTF-8 strings checked" << std::endl; // NOLINT(performance-avoid-endl)
}
static std::string json_string;
json_string = "\"";
CAPTURE(byte1)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
CAPTURE(byte2)
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
CAPTURE(byte3)
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte4));
}
json_string += "\"";
CAPTURE(json_string)
json _;
if (success_expected)
{
CHECK_NOTHROW(_ = json::parse(json_string));
}
else
{
CHECK_THROWS_AS(_ = json::parse(json_string), json::parse_error&);
}
}
} // namespace
TEST_CASE("Unicode (3/5)" * doctest::skip())
{
SECTION("RFC 3629")
{
/*
RFC 3629 describes in Sect. 4 the syntax of UTF-8 byte sequences as
follows:
A UTF-8 string is a sequence of octets representing a sequence of UCS
characters. An octet sequence is valid UTF-8 only if it matches the
following syntax, which is derived from the rules for encoding UTF-8
and is expressed in the ABNF of [RFC2234].
UTF8-octets = *( UTF8-char )
UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1 = %x00-7F
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
%xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
%xF4 %x80-8F 2( UTF8-tail )
UTF8-tail = %x80-BF
*/
SECTION("UTF8-4 (xF0 x90-BF UTF8-tail UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x90; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(true, byte1, byte2, byte3, byte4);
check_utf8dump(true, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x90; byte2 <= 0xBF; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: missing fourth byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x90; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x90 <= byte2 && byte2 <= 0xBF)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x90; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong fourth byte")
{
for (int byte1 = 0xF0; byte1 <= 0xF0; ++byte1)
{
for (int byte2 = 0x90; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x00; byte4 <= 0xFF; ++byte4)
{
// skip correct fourth byte
if (0x80 <= byte4 && byte4 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
}
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
+350
View File
@@ -0,0 +1,350 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale>
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
// this test suite uses static variables with non-trivial destructors
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
extern size_t calls;
size_t calls = 0;
void check_utf8dump(bool success_expected, int byte1, int byte2, int byte3, int byte4);
void check_utf8dump(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
static std::string json_string;
json_string.clear();
CAPTURE(byte1)
CAPTURE(byte2)
CAPTURE(byte3)
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
json_string += std::string(1, static_cast<char>(byte4));
}
CAPTURE(json_string)
// store the string in a JSON value
static json j;
static json j2;
j = json_string;
j2 = "abc" + json_string + "xyz";
static std::string s_ignored;
static std::string s_ignored2;
static std::string s_ignored_ascii;
static std::string s_ignored2_ascii;
static std::string s_replaced;
static std::string s_replaced2;
static std::string s_replaced_ascii;
static std::string s_replaced2_ascii;
static std::string s_kept;
static std::string s_kept2;
static std::string s_kept_ascii;
// dumping with ignore/replace/keep must not throw in any case
s_ignored = j.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored2 = j2.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored_ascii = j.dump(-1, ' ', true, json::error_handler_t::ignore);
s_ignored2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::ignore);
s_replaced = j.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced2 = j2.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced_ascii = j.dump(-1, ' ', true, json::error_handler_t::replace);
s_replaced2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::replace);
s_kept = j.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept2 = j2.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept_ascii = j.dump(-1, ' ', true, json::error_handler_t::keep);
if (success_expected)
{
static std::string s_strict;
// strict mode must not throw if success is expected
s_strict = j.dump();
// all dumps should agree on the string
CHECK(s_strict == s_ignored);
CHECK(s_strict == s_replaced);
CHECK(s_strict == s_kept);
}
else
{
// strict mode must throw if success is not expected
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&);
// ignore and replace must create different dumps
CHECK(s_ignored != s_replaced);
// check that replace string contains a replacement character
CHECK(s_replaced.find("\xEF\xBF\xBD") != std::string::npos);
// ignore drops the invalid bytes, keep copies them
CHECK(s_ignored != s_kept);
CHECK(s_ignored_ascii != s_kept_ascii);
// unless a byte needs escaping, keep copies the input unchanged
const bool needs_escaping = std::any_of(json_string.begin(), json_string.end(), [](char c)
{
return static_cast<unsigned char>(c) < 0x20 || c == '"' || c == '\\';
});
if (!needs_escaping)
{
CHECK(s_kept == "\"" + json_string + "\"");
}
}
// check that prefix and suffix are preserved
CHECK(s_ignored2.substr(1, 3) == "abc");
CHECK(s_ignored2.substr(s_ignored2.size() - 4, 3) == "xyz");
CHECK(s_ignored2_ascii.substr(1, 3) == "abc");
CHECK(s_ignored2_ascii.substr(s_ignored2_ascii.size() - 4, 3) == "xyz");
CHECK(s_replaced2.substr(1, 3) == "abc");
CHECK(s_replaced2.substr(s_replaced2.size() - 4, 3) == "xyz");
CHECK(s_replaced2_ascii.substr(1, 3) == "abc");
CHECK(s_replaced2_ascii.substr(s_replaced2_ascii.size() - 4, 3) == "xyz");
CHECK(s_kept2.substr(1, 3) == "abc");
CHECK(s_kept2.substr(s_kept2.size() - 4, 3) == "xyz");
}
void check_utf8string(bool success_expected, int byte1, int byte2, int byte3, int byte4);
// create and check a JSON string with up to four UTF-8 bytes
void check_utf8string(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
if (++calls % 100000 == 0)
{
std::cout << calls << " of 5517507 UTF-8 strings checked" << std::endl; // NOLINT(performance-avoid-endl)
}
static std::string json_string;
json_string = "\"";
CAPTURE(byte1)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
CAPTURE(byte2)
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
CAPTURE(byte3)
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte4));
}
json_string += "\"";
CAPTURE(json_string)
json _;
if (success_expected)
{
CHECK_NOTHROW(_ = json::parse(json_string));
}
else
{
CHECK_THROWS_AS(_ = json::parse(json_string), json::parse_error&);
}
}
} // namespace
TEST_CASE("Unicode (4/5)" * doctest::skip())
{
SECTION("RFC 3629")
{
/*
RFC 3629 describes in Sect. 4 the syntax of UTF-8 byte sequences as
follows:
A UTF-8 string is a sequence of octets representing a sequence of UCS
characters. An octet sequence is valid UTF-8 only if it matches the
following syntax, which is derived from the rules for encoding UTF-8
and is expressed in the ABNF of [RFC2234].
UTF8-octets = *( UTF8-char )
UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1 = %x00-7F
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
%xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
%xF4 %x80-8F 2( UTF8-tail )
UTF8-tail = %x80-BF
*/
SECTION("UTF8-4 (xF1-F3 UTF8-tail UTF8-tail UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(true, byte1, byte2, byte3, byte4);
check_utf8dump(true, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: missing fourth byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0xBF)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong fourth byte")
{
for (int byte1 = 0xF1; byte1 <= 0xF3; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0xBF; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x00; byte4 <= 0xFF; ++byte4)
{
// skip correct fourth byte
if (0x80 <= byte4 && byte4 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
}
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
+350
View File
@@ -0,0 +1,350 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale>
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include "make_test_data_available.hpp"
#include "test_utils.hpp"
// this test suite uses static variables with non-trivial destructors
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
extern size_t calls;
size_t calls = 0;
void check_utf8dump(bool success_expected, int byte1, int byte2, int byte3, int byte4);
void check_utf8dump(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
static std::string json_string;
json_string.clear();
CAPTURE(byte1)
CAPTURE(byte2)
CAPTURE(byte3)
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
json_string += std::string(1, static_cast<char>(byte4));
}
CAPTURE(json_string)
// store the string in a JSON value
static json j;
static json j2;
j = json_string;
j2 = "abc" + json_string + "xyz";
static std::string s_ignored;
static std::string s_ignored2;
static std::string s_ignored_ascii;
static std::string s_ignored2_ascii;
static std::string s_replaced;
static std::string s_replaced2;
static std::string s_replaced_ascii;
static std::string s_replaced2_ascii;
static std::string s_kept;
static std::string s_kept2;
static std::string s_kept_ascii;
// dumping with ignore/replace/keep must not throw in any case
s_ignored = j.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored2 = j2.dump(-1, ' ', false, json::error_handler_t::ignore);
s_ignored_ascii = j.dump(-1, ' ', true, json::error_handler_t::ignore);
s_ignored2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::ignore);
s_replaced = j.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced2 = j2.dump(-1, ' ', false, json::error_handler_t::replace);
s_replaced_ascii = j.dump(-1, ' ', true, json::error_handler_t::replace);
s_replaced2_ascii = j2.dump(-1, ' ', true, json::error_handler_t::replace);
s_kept = j.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept2 = j2.dump(-1, ' ', false, json::error_handler_t::keep);
s_kept_ascii = j.dump(-1, ' ', true, json::error_handler_t::keep);
if (success_expected)
{
static std::string s_strict;
// strict mode must not throw if success is expected
s_strict = j.dump();
// all dumps should agree on the string
CHECK(s_strict == s_ignored);
CHECK(s_strict == s_replaced);
CHECK(s_strict == s_kept);
}
else
{
// strict mode must throw if success is not expected
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&);
// ignore and replace must create different dumps
CHECK(s_ignored != s_replaced);
// check that replace string contains a replacement character
CHECK(s_replaced.find("\xEF\xBF\xBD") != std::string::npos);
// ignore drops the invalid bytes, keep copies them
CHECK(s_ignored != s_kept);
CHECK(s_ignored_ascii != s_kept_ascii);
// unless a byte needs escaping, keep copies the input unchanged
const bool needs_escaping = std::any_of(json_string.begin(), json_string.end(), [](char c)
{
return static_cast<unsigned char>(c) < 0x20 || c == '"' || c == '\\';
});
if (!needs_escaping)
{
CHECK(s_kept == "\"" + json_string + "\"");
}
}
// check that prefix and suffix are preserved
CHECK(s_ignored2.substr(1, 3) == "abc");
CHECK(s_ignored2.substr(s_ignored2.size() - 4, 3) == "xyz");
CHECK(s_ignored2_ascii.substr(1, 3) == "abc");
CHECK(s_ignored2_ascii.substr(s_ignored2_ascii.size() - 4, 3) == "xyz");
CHECK(s_replaced2.substr(1, 3) == "abc");
CHECK(s_replaced2.substr(s_replaced2.size() - 4, 3) == "xyz");
CHECK(s_replaced2_ascii.substr(1, 3) == "abc");
CHECK(s_replaced2_ascii.substr(s_replaced2_ascii.size() - 4, 3) == "xyz");
CHECK(s_kept2.substr(1, 3) == "abc");
CHECK(s_kept2.substr(s_kept2.size() - 4, 3) == "xyz");
}
void check_utf8string(bool success_expected, int byte1, int byte2, int byte3, int byte4);
// create and check a JSON string with up to four UTF-8 bytes
void check_utf8string(bool success_expected, int byte1, int byte2 = -1, int byte3 = -1, int byte4 = -1)
{
if (++calls % 100000 == 0)
{
std::cout << calls << " of 1246225 UTF-8 strings checked" << std::endl; // NOLINT(performance-avoid-endl)
}
static std::string json_string;
json_string = "\"";
CAPTURE(byte1)
json_string += std::string(1, static_cast<char>(byte1));
if (byte2 != -1)
{
CAPTURE(byte2)
json_string += std::string(1, static_cast<char>(byte2));
}
if (byte3 != -1)
{
CAPTURE(byte3)
json_string += std::string(1, static_cast<char>(byte3));
}
if (byte4 != -1)
{
CAPTURE(byte4)
json_string += std::string(1, static_cast<char>(byte4));
}
json_string += "\"";
CAPTURE(json_string)
json _;
if (success_expected)
{
CHECK_NOTHROW(_ = json::parse(json_string));
}
else
{
CHECK_THROWS_AS(_ = json::parse(json_string), json::parse_error&);
}
}
} // namespace
TEST_CASE("Unicode (5/5)" * doctest::skip())
{
SECTION("RFC 3629")
{
/*
RFC 3629 describes in Sect. 4 the syntax of UTF-8 byte sequences as
follows:
A UTF-8 string is a sequence of octets representing a sequence of UCS
characters. An octet sequence is valid UTF-8 only if it matches the
following syntax, which is derived from the rules for encoding UTF-8
and is expressed in the ABNF of [RFC2234].
UTF8-octets = *( UTF8-char )
UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1 = %x00-7F
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
%xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
%xF4 %x80-8F 2( UTF8-tail )
UTF8-tail = %x80-BF
*/
SECTION("UTF8-4 (xF4 x80-8F UTF8-tail UTF8-tail)")
{
SECTION("well-formed")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x8F; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(true, byte1, byte2, byte3, byte4);
check_utf8dump(true, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: missing second byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
check_utf8string(false, byte1);
check_utf8dump(false, byte1);
}
}
SECTION("ill-formed: missing third byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x8F; ++byte2)
{
check_utf8string(false, byte1, byte2);
check_utf8dump(false, byte1, byte2);
}
}
}
SECTION("ill-formed: missing fourth byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x8F; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
check_utf8string(false, byte1, byte2, byte3);
check_utf8dump(false, byte1, byte2, byte3);
}
}
}
}
SECTION("ill-formed: wrong second byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x00; byte2 <= 0xFF; ++byte2)
{
// skip correct second byte
if (0x80 <= byte2 && byte2 <= 0x8F)
{
continue;
}
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong third byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x8F; ++byte2)
{
for (int byte3 = 0x00; byte3 <= 0xFF; ++byte3)
{
// skip correct third byte
if (0x80 <= byte3 && byte3 <= 0xBF)
{
continue;
}
for (int byte4 = 0x80; byte4 <= 0xBF; ++byte4)
{
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
SECTION("ill-formed: wrong fourth byte")
{
for (int byte1 = 0xF4; byte1 <= 0xF4; ++byte1)
{
for (int byte2 = 0x80; byte2 <= 0x8F; ++byte2)
{
for (int byte3 = 0x80; byte3 <= 0xBF; ++byte3)
{
for (int byte4 = 0x00; byte4 <= 0xFF; ++byte4)
{
// skip correct fourth byte
if (0x80 <= byte4 && byte4 <= 0xBF)
{
continue;
}
check_utf8string(false, byte1, byte2, byte3, byte4);
check_utf8dump(false, byte1, byte2, byte3, byte4);
}
}
}
}
}
}
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP