// __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ (supporting code) // | | |__ | | | | | | version 3.12.0 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT #include "doctest_compatibility.h" #include using nlohmann::json; #include #include #include #include #include #include #include #include #include // Parsing from a FILE* locks the file once and reads it with the unlocked // stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that // this is not observable: the results are the same, the file is unlocked // again when parsing ends - also with an exception - and the same thread can // keep using the file while it is locked, e.g., from a SAX callback. namespace { // a file name of its own for every run: the test runs for several C++ // standards and configurations at the same time, in the same directory std::string temp_file_name() { std::random_device rd; return "unit-file_input-" + std::to_string(rd()) + ".tmp"; } using file_ptr = std::unique_ptr; // write @a content to the file @a name and open it for reading file_ptr open_with(const char* name, const std::string& content) { { std::ofstream file(name, std::ios::binary); file << content; } return {std::fopen(name, "rb"), &std::fclose}; } // The stdio lock is recursive, so whether parsing released it can only be // seen from another thread: ftrylockfile() fails there while this thread // holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped. #if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) void check_unlocked(std::FILE* f) { int result = -1; std::thread other([f, &result] { result = ftrylockfile(f); if (result == 0) { funlockfile(f); } }); other.join(); CHECK(result == 0); } #else void check_unlocked(std::FILE* /*f*/) {} #endif // SAX consumer that also reads the position of the file it is parsing, which // takes the file's lock again on the same thread class position_reading_sax : public nlohmann::json_sax { public: explicit position_reading_sax(std::FILE* f) : m_file(f) {} position_reading_sax(const position_reading_sax&) = delete; position_reading_sax(position_reading_sax&&) = delete; position_reading_sax& operator=(const position_reading_sax&) = delete; position_reading_sax& operator=(position_reading_sax&&) = delete; ~position_reading_sax() override = default; bool null() override { return record(); } bool boolean(bool /*val*/) override { return record(); } bool number_integer(number_integer_t /*val*/) override { return record(); } bool number_unsigned(number_unsigned_t /*val*/) override { return record(); } bool number_float(number_float_t /*val*/, const string_t& /*s*/) override { return record(); } bool string(string_t& /*val*/) override { return record(); } bool binary(binary_t& /*val*/) override { return record(); } bool start_object(std::size_t /*elements*/) override { return record(); } bool key(string_t& /*val*/) override { return record(); } bool end_object() override { return record(); } bool start_array(std::size_t /*elements*/) override { return record(); } bool end_array() override { return record(); } bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override { return false; } const std::vector& positions() const { return m_positions; } private: bool record() { m_positions.push_back(std::ftell(m_file)); return true; } std::FILE* m_file; std::vector m_positions{}; // NOLINT(readability-redundant-member-init) }; } // namespace TEST_CASE("parsing from a FILE*") { const std::string temp_name = temp_file_name(); const char* const name = temp_name.c_str(); const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}"; SECTION("values are the same as when parsing from a string") { const file_ptr f = open_with(name, input); REQUIRE(f != nullptr); CHECK(json::parse(f.get()) == json::parse(input)); // the whole input was consumed std::array rest{}; CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) == nullptr); check_unlocked(f.get()); } SECTION("the file is unlocked after a parse error") { const file_ptr f = open_with(name, "[1, 2, x]"); REQUIRE(f != nullptr); json _; CHECK_THROWS_WITH_AS(_ = json::parse(f.get()), "[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'", json::parse_error&); check_unlocked(f.get()); // the file is still usable: the rest of the input is there std::array rest{}; CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) != nullptr); CHECK(std::string(rest.data()) == "]"); } SECTION("the file can be parsed again and read with stdio afterwards") { const file_ptr f = open_with(name, "[1] [2] tail"); REQUIRE(f != nullptr); // sax_parse with strict == false stops after the first value; an array // ends with its closing bracket, so the space after it is not read json j; nlohmann::detail::json_sax_dom_parser sax1(j); CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false)); CHECK(j == json({1})); check_unlocked(f.get()); json k; nlohmann::detail::json_sax_dom_parser sax2(k); CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false)); CHECK(k == json({2})); check_unlocked(f.get()); std::array rest{}; CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) != nullptr); CHECK(std::string(rest.data()) == " tail"); } SECTION("the same thread can use the file while it is parsed") { const file_ptr f = open_with(name, "[10, 20, 30]"); REQUIRE(f != nullptr); position_reading_sax sax(f.get()); CHECK(json::sax_parse(f.get(), &sax)); // start_array, three numbers, end_array CHECK(sax.positions().size() == 5); CHECK(sax.positions().front() == 1); check_unlocked(f.get()); } SECTION("binary formats read from a FILE*") { const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}}; const std::vector cbor = json::to_cbor(j); const file_ptr f = open_with(name, std::string(reinterpret_cast(cbor.data()), cbor.size())); REQUIRE(f != nullptr); CHECK(json::from_cbor(f.get()) == j); check_unlocked(f.get()); } static_cast(std::remove(name)); }