fix: make array operator[] dense when assigning past the end (#1611) (#1693)
* fix: make array operator[] dense when assigning past the end (#1611)
Value::operator[](ArrayIndex) only inserted the requested index, so
`arr[5] = x` on an empty array stored a single element while size()
reported 6 (highest index + 1) and serialization emitted six elements
(missing indices written as null). Range-for iteration walked the
underlying sparse map and therefore visited only the one populated
element -- inconsistent with both size() and the serialized output.
JSON arrays are dense, so materialize the intervening indices as null
when assigning beyond the current end, exactly as resize() already does
when growing. Iteration, size(), equality, and serialization now agree.
Note: this is a behavior change (arrays are now dense in memory after a
sparse-looking assignment), so it targets the 1.10.0 minor release rather
than a 1.9.x patch. It is ABI-compatible (no signature or layout change),
so SOVERSION is unchanged.
* chore: bump version to 1.10.0
master becomes the 1.10 line. The #1611 array fix changes runtime
behavior (dense arrays) for existing valid code, so the next release is
a minor bump, not a 1.9.x patch. SOVERSION stays at 27 (ABI unchanged).
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b2335b5..d8e6d4c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -57,7 +57,7 @@
# 3. ./CMakeLists.txt
# 4. ./MODULE.bazel
# IMPORTANT: also update the PROJECT_SOVERSION!!
- VERSION 1.9.9 # <major>[.<minor>[.<patch>[.<tweak>]]]
+ VERSION 1.10.0 # <major>[.<minor>[.<patch>[.<tweak>]]]
LANGUAGES CXX)
message(STATUS "JsonCpp Version: ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
diff --git a/MODULE.bazel b/MODULE.bazel
index ad09c20..0960724 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -9,7 +9,7 @@
# 3. /CMakeLists.txt
# 4. /MODULE.bazel
# IMPORTANT: also update the SOVERSION!!
- version = "1.9.9",
+ version = "1.10.0",
compatibility_level = 1,
)
diff --git a/include/json/version.h b/include/json/version.h
index ff435d5..2068ba0 100644
--- a/include/json/version.h
+++ b/include/json/version.h
@@ -10,10 +10,10 @@
// 4. /MODULE.bazel
// IMPORTANT: also update the SOVERSION!!
-#define JSONCPP_VERSION_STRING "1.9.9"
+#define JSONCPP_VERSION_STRING "1.10.0"
#define JSONCPP_VERSION_MAJOR 1
-#define JSONCPP_VERSION_MINOR 9
-#define JSONCPP_VERSION_PATCH 9
+#define JSONCPP_VERSION_MINOR 10
+#define JSONCPP_VERSION_PATCH 0
#define JSONCPP_VERSION_HEXA \
((JSONCPP_VERSION_MAJOR << 24) | (JSONCPP_VERSION_MINOR << 16) | \
(JSONCPP_VERSION_PATCH << 8))
diff --git a/meson.build b/meson.build
index 2cd0b5d..380b7e2 100644
--- a/meson.build
+++ b/meson.build
@@ -10,7 +10,7 @@
# 3. /CMakeLists.txt
# 4. /MODULE.bazel
# IMPORTANT: also update the SOVERSION!!
- version : '1.9.9',
+ version : '1.10.0',
default_options : [
'buildtype=release',
'cpp_std=c++11',
diff --git a/src/lib_json/json_value.cpp b/src/lib_json/json_value.cpp
index ac88109..5823bd1 100644
--- a/src/lib_json/json_value.cpp
+++ b/src/lib_json/json_value.cpp
@@ -985,6 +985,15 @@
if (it != value_.map_->end() && (*it).first == key)
return (*it).second;
+ // JSON arrays are dense: materialize any gap between the current size and
+ // `index` with null so that size(), iteration, and serialization stay
+ // consistent. Without this, `arr[5] = x` on an empty array would store a
+ // single element while size() reported 6 and serialization emitted six
+ // (see issue #1611). resize() already grows arrays this same way.
+ for (ArrayIndex i = size(); i < index; ++i)
+ value_.map_->insert(value_.map_->end(),
+ ObjectValues::value_type(CZString(i), nullSingleton()));
+
ObjectValues::value_type defaultValue(key, nullSingleton());
it = value_.map_->insert(it, defaultValue);
return (*it).second;
diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp
index 9d13fdb..6673867 100644
--- a/src/test_lib_json/main.cpp
+++ b/src/test_lib_json/main.cpp
@@ -524,6 +524,35 @@
JSONTEST_ASSERT_EQUAL(e, Json::Value{});
}
+JSONTEST_FIXTURE_LOCAL(ValueTest, assignBeyondEndPopulatesGapsWithNull) {
+ // Regression test for #1611: assigning past the end of an array via
+ // operator[] must fill the intervening indices with null, so that size(),
+ // iteration, and serialization all agree (JSON arrays are dense). Before the
+ // fix, `arr[5] = x` stored a single element while size() reported 6 and
+ // serialization emitted six, and range-for visited only the one element.
+ Json::Value arr(Json::arrayValue);
+ arr[5] = "Hello, World!";
+
+ JSONTEST_ASSERT_EQUAL(6u, arr.size());
+ JSONTEST_ASSERT_EQUAL(6, std::distance(arr.begin(), arr.end()));
+ for (Json::ArrayIndex i = 0; i < 5; ++i)
+ JSONTEST_ASSERT_EQUAL(Json::Value{}, arr[i]);
+ JSONTEST_ASSERT_EQUAL("Hello, World!", arr[5].asString());
+
+ // Iteration count matches size() and the dense serialization.
+ Json::ArrayIndex iterated = 0;
+ for (const Json::Value& e : arr) {
+ (void)e;
+ ++iterated;
+ }
+ JSONTEST_ASSERT_EQUAL(6u, iterated);
+
+ Json::StreamWriterBuilder b;
+ b.settings_["indentation"] = "";
+ JSONTEST_ASSERT_EQUAL("[null,null,null,null,null,\"Hello, World!\"]",
+ Json::writeString(b, arr));
+}
+
JSONTEST_FIXTURE_LOCAL(ValueTest, getArrayValue) {
Json::Value array;
for (Json::ArrayIndex i = 0; i < 5; i++)