diff --git a/CMakeLists.txt b/CMakeLists.txt index b93c6e47f..71f8ffe38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(JSON_BuildTests "Build the unit tests when BUILD_TESTING is enab option(JSON_CI "Enable CI build targets." OFF) option(JSON_Diagnostics "Use extended diagnostic messages." OFF) option(JSON_ImplicitConversions "Enable implicit conversions." ON) +option(JSON_StrictContainerSize "Enable strict size checks for container conversions." OFF) option(JSON_Install "Install CMake targets during install step." ${MAIN_PROJECT}) option(JSON_MultipleHeaders "Use non-amalgamated version of the library." OFF) option(JSON_SystemInclude "Include as system headers (skip for clang-tidy)." OFF) @@ -94,6 +95,7 @@ target_compile_definitions( ${NLOHMANN_JSON_TARGET_NAME} INTERFACE JSON_USE_IMPLICIT_CONVERSIONS=$ + JSON_USE_STRICT_CONTAINER_SIZE=$ JSON_DIAGNOSTICS=$ ) diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 207d3e302..e0900b42f 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -186,6 +186,17 @@ template auto from_json(const BasicJsonType& j, T (&arr)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) -> decltype(j.template get(), void()) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != N)) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(N) + ", but is " + std::to_string(j.size()), j)); + } +#endif + for (std::size_t i = 0; i < N; ++i) { arr[i] = j.at(i).template get(); @@ -203,6 +214,14 @@ auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { +#if JSON_USE_STRICT_CONTAINER_SIZE + // array type check done in from_json already, only check size + if (JSON_HEDLEY_UNLIKELY(j.size() != N)) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(N) + ", but is " + std::to_string(j.size()), j)); + } +#endif + for (std::size_t i = 0; i < N; ++i) { arr[i] = j.at(i).template get(); @@ -279,6 +298,14 @@ template < typename BasicJsonType, typename T, std::size_t... Idx > std::array from_json_inplace_array_impl(BasicJsonType&& j, identity_tag> /*unused*/, index_sequence /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + // array type check done in from_json already, only check size + if (JSON_HEDLEY_UNLIKELY(j.size() != sizeof...(Idx))) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(sizeof...(Idx)) + ", but is " + std::to_string(j.size()), j)); + } +#endif + return { { std::forward(j).at(Idx).template get()... } }; } @@ -378,12 +405,34 @@ void from_json(const BasicJsonType& j, ArithmeticType& val) template std::tuple from_json_tuple_impl_base(BasicJsonType&& j, index_sequence /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != sizeof...(Idx))) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(sizeof...(Idx)) + ", but is " + std::to_string(j.size()), j)); + } +#endif + return std::make_tuple(std::forward(j).at(Idx).template get()...); } template < typename BasicJsonType, class A1, class A2 > std::pair from_json_tuple_impl(BasicJsonType&& j, identity_tag> /*unused*/, priority_tag<0> /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != 2)) + { + JSON_THROW(type_error::create(302, "array size must be 2, but is " + std::to_string(j.size()), j)); + } +#endif + return {std::forward(j).at(0).template get(), std::forward(j).at(1).template get()}; } diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 87f964353..ffc5c35c4 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -411,6 +411,10 @@ #define JSON_EXPLICIT explicit #endif +#ifndef JSON_USE_STRICT_CONTAINER_SIZE + #define JSON_USE_STRICT_CONTAINER_SIZE 0 +#endif + #ifndef JSON_DIAGNOSTICS #define JSON_DIAGNOSTICS 0 #endif diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 05000fbba..4a9b1023b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2708,6 +2708,10 @@ using is_detected_convertible = #define JSON_EXPLICIT explicit #endif +#ifndef JSON_USE_STRICT_CONTAINER_SIZE + #define JSON_USE_STRICT_CONTAINER_SIZE 0 +#endif + #ifndef JSON_DIAGNOSTICS #define JSON_DIAGNOSTICS 0 #endif @@ -3988,6 +3992,17 @@ template auto from_json(const BasicJsonType& j, T (&arr)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) -> decltype(j.template get(), void()) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != N)) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(N) + ", but is " + std::to_string(j.size()), j)); + } +#endif + for (std::size_t i = 0; i < N; ++i) { arr[i] = j.at(i).template get(); @@ -4005,6 +4020,14 @@ auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { +#if JSON_USE_STRICT_CONTAINER_SIZE + // array type check done in from_json already, only check size + if (JSON_HEDLEY_UNLIKELY(j.size() != N)) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(N) + ", but is " + std::to_string(j.size()), j)); + } +#endif + for (std::size_t i = 0; i < N; ++i) { arr[i] = j.at(i).template get(); @@ -4081,6 +4104,14 @@ template < typename BasicJsonType, typename T, std::size_t... Idx > std::array from_json_inplace_array_impl(BasicJsonType&& j, identity_tag> /*unused*/, index_sequence /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + // array type check done in from_json already, only check size + if (JSON_HEDLEY_UNLIKELY(j.size() != sizeof...(Idx))) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(sizeof...(Idx)) + ", but is " + std::to_string(j.size()), j)); + } +#endif + return { { std::forward(j).at(Idx).template get()... } }; } @@ -4180,12 +4211,34 @@ void from_json(const BasicJsonType& j, ArithmeticType& val) template std::tuple from_json_tuple_impl_base(BasicJsonType&& j, index_sequence /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != sizeof...(Idx))) + { + JSON_THROW(type_error::create(302, "array size must be " + std::to_string(sizeof...(Idx)) + ", but is " + std::to_string(j.size()), j)); + } +#endif + return std::make_tuple(std::forward(j).at(Idx).template get()...); } template < typename BasicJsonType, class A1, class A2 > std::pair from_json_tuple_impl(BasicJsonType&& j, identity_tag> /*unused*/, priority_tag<0> /*unused*/) { +#if JSON_USE_STRICT_CONTAINER_SIZE + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + if (JSON_HEDLEY_UNLIKELY(j.size() != 2)) + { + JSON_THROW(type_error::create(302, "array size must be 2, but is " + std::to_string(j.size()), j)); + } +#endif + return {std::forward(j).at(0).template get(), std::forward(j).at(1).template get()}; } diff --git a/test/src/unit-constructor1.cpp b/test/src/unit-constructor1.cpp index 03ad689ab..7c1e412a6 100644 --- a/test/src/unit-constructor1.cpp +++ b/test/src/unit-constructor1.cpp @@ -267,6 +267,7 @@ TEST_CASE("constructors") CHECK(j[1] == std::get<1>(p)); } +#if !(JSON_USE_STRICT_CONTAINER_SIZE) SECTION("std::pair with discarded values") { json j{1, 2.0, "string"}; @@ -275,6 +276,7 @@ TEST_CASE("constructors") CHECK(p.first == j[0]); CHECK(p.second == j[1]); } +#endif SECTION("std::tuple") { @@ -291,6 +293,7 @@ TEST_CASE("constructors") CHECK(j[3][1] == 1); } +#if !(JSON_USE_STRICT_CONTAINER_SIZE) SECTION("std::tuple with discarded values") { json j{1, 2.0, "string", 42}; @@ -300,9 +303,27 @@ TEST_CASE("constructors") CHECK(std::get<1>(t) == j[1]); CHECK(std::get<2>(t) == j[2]); } +#endif SECTION("std::pair/tuple/array failures") { +#if JSON_USE_STRICT_CONTAINER_SIZE + json j1{1}; + json j2{1, 2, 3}; + + CHECK_THROWS_AS((j1.get>()), json::type_error&); + CHECK_THROWS_WITH((j1.get>()), "[json.exception.type_error.302] array size must be 2, but is 1"); + CHECK_THROWS_AS((j2.get>()), json::type_error&); + CHECK_THROWS_WITH((j2.get>()), "[json.exception.type_error.302] array size must be 2, but is 3"); + CHECK_THROWS_AS((j1.get>()), json::type_error&); + CHECK_THROWS_WITH((j1.get>()), "[json.exception.type_error.302] array size must be 2, but is 1"); + CHECK_THROWS_AS((j2.get>()), json::type_error&); + CHECK_THROWS_WITH((j2.get>()), "[json.exception.type_error.302] array size must be 2, but is 3"); + CHECK_THROWS_AS((j1.get>()), json::type_error&); + CHECK_THROWS_WITH((j1.get>()), "[json.exception.type_error.302] array size must be 3, but is 1"); + CHECK_THROWS_AS((j2.get>()), json::type_error&); + CHECK_THROWS_WITH((j2.get>()), "[json.exception.type_error.302] array size must be 1, but is 3"); +#else json j{1}; CHECK_THROWS_AS((j.get>()), json::out_of_range&); @@ -311,6 +332,7 @@ TEST_CASE("constructors") CHECK_THROWS_WITH((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range"); CHECK_THROWS_AS((j.get>()), json::out_of_range&); CHECK_THROWS_WITH((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range"); +#endif } SECTION("std::forward_list") diff --git a/test/src/unit-conversions.cpp b/test/src/unit-conversions.cpp index 092d5b8c3..1548146c3 100644 --- a/test/src/unit-conversions.cpp +++ b/test/src/unit-conversions.cpp @@ -40,6 +40,8 @@ using nlohmann::json; #include #include #include +#include +#include #if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 #define JSON_HAS_CPP_17 @@ -397,6 +399,50 @@ TEST_CASE("value conversion") json j2 = nbs; j2.get_to(nbs2); CHECK(std::equal(std::begin(nbs), std::end(nbs), std::begin(nbs2))); + + SECTION("built-in array is larger than JSON") + { + json j1 = {1, 2, 3, 4}; + int arr6[] = {1, 2, 3, 4, 5, 6}; +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j1.get_to(arr6), json::type_error&); + CHECK_THROWS_WITH(j1.get_to(arr6), "[json.exception.type_error.302] " + "array size must be 6, but is 4"); +#else + CHECK_THROWS_AS(j1.get_to(arr6), json::out_of_range&); + CHECK_THROWS_WITH(j1.get_to(arr6), "[json.exception.out_of_range.401] " + "array index 4 is out of range"); +#endif + } + + SECTION("built-in array is smaller than JSON") + { + json j1 = {1, 2, 3, 4}; + int arr2[] = {8, 9}; +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j1.get_to(arr2), json::type_error&); + CHECK_THROWS_WITH(j1.get_to(arr2), "[json.exception.type_error.302] " + "array size must be 2, but is 4"); +#else + j1.get_to(arr2); + CHECK(arr2[0] == 1); + CHECK(arr2[1] == 2); +#endif + } + + SECTION("built-in array from non-array type") + { + json j1; + int arr2[] = {8, 9}; + CHECK_THROWS_AS(j1.get_to(arr2), json::type_error&); +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_WITH(j1.get_to(arr2), "[json.exception.type_error.302] " + "type must be array, but is null"); +#else + CHECK_THROWS_WITH(j1.get_to(arr2), "[json.exception.type_error.304] " + "cannot use at() with null"); +#endif + } } SECTION("std::deque") @@ -1487,7 +1533,7 @@ TEST_CASE("value conversion") SECTION("std::array") { j1.get>(); - j2.get>(); + j2.get>(); j3.get>(); j4.get>(); j5.get>(); @@ -1495,17 +1541,29 @@ TEST_CASE("value conversion") SECTION("std::array is larger than JSON") { std::array arr6 = {{1, 2, 3, 4, 5, 6}}; +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j1.get_to(arr6), json::type_error&); + CHECK_THROWS_WITH(j1.get_to(arr6), "[json.exception.type_error.302] " + "array size must be 6, but is 4"); +#else CHECK_THROWS_AS(j1.get_to(arr6), json::out_of_range&); CHECK_THROWS_WITH(j1.get_to(arr6), "[json.exception.out_of_range.401] " "array index 4 is out of range"); +#endif } SECTION("std::array is smaller than JSON") { std::array arr2 = {{8, 9}}; +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j1.get_to(arr2), json::type_error&); + CHECK_THROWS_WITH(j1.get_to(arr2), "[json.exception.type_error.302] " + "array size must be 2, but is 4"); +#else j1.get_to(arr2); CHECK(arr2[0] == 1); CHECK(arr2[1] == 2); +#endif } } @@ -1612,8 +1670,10 @@ TEST_CASE("value conversion") { CHECK_THROWS_AS((json().get>()), json::type_error&); CHECK_THROWS_AS((json().get>()), json::type_error&); + CHECK_THROWS_AS((json().get>()), json::type_error&); CHECK_THROWS_AS((json().get>()), json::type_error&); CHECK_THROWS_AS((json().get>()), json::type_error&); + CHECK_THROWS_AS((json().get>()), json::type_error&); CHECK_THROWS_AS((json().get>()), json::type_error&); // does type really must be an array? or it rather must not be null? @@ -1624,12 +1684,18 @@ TEST_CASE("value conversion") CHECK_THROWS_WITH( (json().get>()), "[json.exception.type_error.302] type must be array, but is null"); + CHECK_THROWS_WITH( + (json().get>()), + "[json.exception.type_error.302] type must be array, but is null"); CHECK_THROWS_WITH( (json().get>()), "[json.exception.type_error.302] type must be array, but is null"); CHECK_THROWS_WITH( (json().get>()), "[json.exception.type_error.302] type must be array, but is null"); + CHECK_THROWS_WITH( + (json().get>()), + "[json.exception.type_error.302] type must be array, but is null"); CHECK_THROWS_WITH( (json().get>()), "[json.exception.type_error.302] type must be array, but is null"); @@ -1638,6 +1704,115 @@ TEST_CASE("value conversion") "[json.exception.type_error.302] type must be array, but is null"); } } + + SECTION("other STL containers") + { + json j1 = {1, 2}; + json j2 = {1u, 2u}; + json j3 = {1.2, 2.3}; + json j4 = {true, false}; + json j5 = {"one", "two"}; + json j6 = {1, "2"}; + json j7 = {1, 2.3}; + json j8 = {1, false}; + json j9 = {1, 2, 3}; + json j10 = {1}; + + SECTION("std::pair") + { + j1.get>(); + j2.get>(); + j3.get>(); + j4.get>(); + j5.get>(); + j6.get>(); + j7.get>(); + j8.get>(); + + SECTION("std::pair is larger than JSON") + { + std::pair p(8, 9); +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j10.get_to(p), json::type_error&); + CHECK_THROWS_WITH(j10.get_to(p), "[json.exception.type_error.302] " + "array size must be 2, but is 1"); +#else + CHECK_THROWS_AS(j10.get_to(p), json::out_of_range&); + CHECK_THROWS_WITH(j10.get_to(p), "[json.exception.out_of_range.401] " + "array index 1 is out of range"); +#endif + } + + SECTION("std::pair is smaller than JSON") + { + std::pair p(8, 9); +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j9.get_to(p), json::type_error&); + CHECK_THROWS_WITH(j9.get_to(p), "[json.exception.type_error.302] " + "array size must be 2, but is 3"); +#else + j9.get_to(p); + CHECK(p.first == 1); + CHECK(p.second == 2); +#endif + } + } + + SECTION("std::tuple") + { + j1.get>(); + j2.get>(); + j3.get>(); + j4.get>(); + j5.get>(); + j6.get>(); + j7.get>(); + j8.get>(); + j9.get>(); + j10.get>(); + + SECTION("std::tuple is larger than JSON") + { + std::tuple t(8, 9); +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j10.get_to(t), json::type_error&); + CHECK_THROWS_WITH(j10.get_to(t), "[json.exception.type_error.302] " + "array size must be 2, but is 1"); +#else + CHECK_THROWS_AS(j10.get_to(t), json::out_of_range&); + CHECK_THROWS_WITH(j10.get_to(t), "[json.exception.out_of_range.401] " + "array index 1 is out of range"); +#endif + } + + SECTION("std::tuple is smaller than JSON") + { + std::tuple t(8, 9); +#if JSON_USE_STRICT_CONTAINER_SIZE + CHECK_THROWS_AS(j9.get_to(t), json::type_error&); + CHECK_THROWS_WITH(j9.get_to(t), "[json.exception.type_error.302] " + "array size must be 2, but is 3"); +#else + j9.get_to(t); + CHECK(std::get<0>(t) == 1); + CHECK(std::get<1>(t) == 2); +#endif + } + } + + SECTION("exception in case of a non-object type") + { + CHECK_THROWS_AS((json().get>()), json::type_error&); + CHECK_THROWS_AS((json().get>()), json::type_error&); + + CHECK_THROWS_WITH( + (json().get>()), + "[json.exception.type_error.302] type must be array, but is null"); + CHECK_THROWS_WITH( + (json().get>()), + "[json.exception.type_error.302] type must be array, but is null"); + } + } } }