Skip to content

Commit f7a40f6

Browse files
authored
fix: json.set recursive processing crash (#5040)
fixed: #5038
1 parent f09df99 commit f7a40f6

File tree

4 files changed

+72
-7
lines changed

4 files changed

+72
-7
lines changed

src/core/json/detail/jsoncons_dfs.cc

+22-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ Dfs Dfs::Mutate(absl::Span<const PathSegment> path, const MutateCallback& callba
8282
return dfs;
8383
}
8484

85+
// Use vector to maintain order
86+
std::vector<JsonType*> nodes_to_mutate;
87+
8588
using Item = detail::JsonconsDfsItem<false>;
8689
vector<Item> stack;
8790
stack.emplace_back(json);
@@ -103,14 +106,32 @@ Dfs Dfs::Mutate(absl::Span<const PathSegment> path, const MutateCallback& callba
103106
if (next_seg_id + 1 < path.size()) {
104107
stack.emplace_back(next, next_seg_id);
105108
} else {
106-
dfs.MutateStep(path[next_seg_id], callback, next);
109+
// Terminal step: collect node for mutation if not seen before
110+
nodes_to_mutate.push_back(next);
107111
}
108112
}
109113
} else {
114+
// If Advance failed (e.g., MISMATCH or OUT_OF_BOUNDS), check if the current node
115+
// itself is a terminal target for mutation due to a previous DESCENT segment.
116+
// This handles cases like $.a..b where 'a' itself might match the 'b' after descent.
117+
if (!res && segment_index > 0 && path[segment_index - 1].type() == SegmentType::DESCENT &&
118+
stack.back().get_segment_step() == 0) {
119+
if (segment_index + 1 == path.size()) {
120+
// Attempt to apply the final mutation step directly to the current node.
121+
// We apply it directly here because this node won't be added to the stack again.
122+
dfs.MutateStep(path_segment, callback, stack.back().obj_ptr());
123+
}
124+
}
110125
stack.pop_back();
111126
}
112127
} while (!stack.empty());
113128

129+
// Apply mutations after DFS traversal is complete
130+
const PathSegment& terminal_segment = path.back();
131+
for (JsonType* node : nodes_to_mutate) {
132+
dfs.MutateStep(terminal_segment, callback, node);
133+
}
134+
114135
return dfs;
115136
}
116137

src/core/json/detail/jsoncons_dfs.h

+17-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ template <bool IsConst> class JsonconsDfsItem {
4343
return depth_state_.second;
4444
}
4545

46+
Ptr obj_ptr() const {
47+
return depth_state_.first;
48+
}
49+
50+
unsigned get_segment_step() const {
51+
return segment_step_;
52+
}
53+
4654
private:
4755
static bool ShouldIterateAll(SegmentType type) {
4856
return type == SegmentType::WILDCARD || type == SegmentType::DESCENT;
@@ -128,8 +136,15 @@ class Dfs {
128136
}
129137

130138
bool Mutate(const MutateCallback& callback, std::optional<std::string_view> key, JsonType* node) {
131-
++matches_;
132-
return callback(key, node);
139+
// matches_ is incremented in MutateStep after successful deletion/mutation by callback.
140+
bool deleted = callback(key, node);
141+
// We only increment matches_ here if the callback indicates deletion,
142+
// as MutateStep handles incrementing based on its own logic.
143+
// This might need adjustment if non-deleting mutations should also count universally.
144+
if (deleted) {
145+
matches_++;
146+
}
147+
return deleted; // Return true if node should be deleted
133148
}
134149

135150
unsigned matches_ = 0;

src/core/json/jsonpath_test.cc

+24
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,30 @@ TYPED_TEST(JsonPathTest, Mutate) {
521521
}
522522
}
523523

524+
TYPED_TEST(JsonPathTest, MutateRecursiveDescentKey) {
525+
ASSERT_EQ(0, this->Parse("$..value"));
526+
Path path = this->driver_.TakePath();
527+
528+
JsonType json = ValidJson<JsonType>(R"({"data":{"value":10,"subdata":{"value":20}}})");
529+
JsonType replacement = ValidJson<JsonType>(R"({"value": 30})");
530+
531+
auto cb = [&](optional<string_view> key, JsonType* val) {
532+
if (key && key.value() == "value" && (val->is_int64() || val->is_double())) {
533+
*val = replacement;
534+
return false;
535+
}
536+
return false;
537+
};
538+
539+
unsigned reported_matches = MutatePath(path, cb, &json);
540+
541+
JsonType expected =
542+
ValidJson<JsonType>(R"({"data":{"subdata":{"value":{"value":30}},"value":{"value":30}}})");
543+
544+
EXPECT_EQ(expected, json);
545+
EXPECT_EQ(0, reported_matches);
546+
}
547+
524548
TYPED_TEST(JsonPathTest, SubRange) {
525549
TypeParam json = ValidJson<TypeParam>(R"({"arr": [1, 2, 3, 4, 5]})");
526550
ASSERT_EQ(0, this->Parse("$.arr[1:2]"));

src/core/json/path.cc

+9-4
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,16 @@ unsigned MutatePath(const Path& path, MutateCallback callback, FlatJson json,
288288
flexbuffers::Builder* fbb) {
289289
JsonType mut_json = FromFlat(json);
290290
unsigned res = MutatePath(path, std::move(callback), &mut_json);
291-
if (res) {
292-
FromJsonType(mut_json, fbb);
293-
fbb->Finish();
294-
}
295291

292+
// Populate the output builder 'fbb' with the resulting JSON state
293+
// (mutated or original if res == 0) and finalize it.
294+
// The builder MUST be finished before returning so that the caller
295+
// can safely access the resulting flatbuffer data (e.g., via GetBuffer()).
296+
// Skipping Finish() would leave the builder in an invalid, unusable state.
297+
FromJsonType(mut_json, fbb); // Always convert (changed or not) JSON
298+
fbb->Finish(); // Always finish the builder
299+
300+
// Return the number of actual mutations that occurred.
296301
return res;
297302
}
298303

0 commit comments

Comments
 (0)