Skip to content

Commit f1ade14

Browse files
committed
fix: json.set recursive processing crash
1 parent f4f4668 commit f1ade14

File tree

4 files changed

+76
-7
lines changed

4 files changed

+76
-7
lines changed

src/core/json/detail/jsoncons_dfs.cc

+24-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
#include "core/json/detail/jsoncons_dfs.h"
1010

11+
#include <set>
12+
1113
namespace dfly::json::detail {
1214

1315
using namespace std;
@@ -82,6 +84,9 @@ Dfs Dfs::Mutate(absl::Span<const PathSegment> path, const MutateCallback& callba
8284
return dfs;
8385
}
8486

87+
// Collect nodes to mutate first, then apply mutations
88+
std::set<JsonType*> nodes_to_mutate;
89+
8590
using Item = detail::JsonconsDfsItem<false>;
8691
vector<Item> stack;
8792
stack.emplace_back(json);
@@ -103,14 +108,32 @@ Dfs Dfs::Mutate(absl::Span<const PathSegment> path, const MutateCallback& callba
103108
if (next_seg_id + 1 < path.size()) {
104109
stack.emplace_back(next, next_seg_id);
105110
} else {
106-
dfs.MutateStep(path[next_seg_id], callback, next);
111+
// Terminal step: collect node for mutation
112+
nodes_to_mutate.insert(next);
107113
}
108114
}
109115
} else {
116+
// If Advance failed (e.g., MISMATCH or OUT_OF_BOUNDS), check if the current node
117+
// itself is a terminal target for mutation due to a previous DESCENT segment.
118+
// This handles cases like $.a..b where 'a' itself might match the 'b' after descent.
119+
if (!res && segment_index > 0 && path[segment_index - 1].type() == SegmentType::DESCENT &&
120+
stack.back().get_segment_step() == 0) {
121+
if (segment_index + 1 == path.size()) {
122+
// Attempt to apply the final mutation step directly to the current node.
123+
// We apply it directly here because this node won't be added to the stack again.
124+
dfs.MutateStep(path_segment, callback, stack.back().obj_ptr());
125+
}
126+
}
110127
stack.pop_back();
111128
}
112129
} while (!stack.empty());
113130

131+
// Apply mutations after DFS traversal is complete
132+
const PathSegment& terminal_segment = path.back();
133+
for (JsonType* node : nodes_to_mutate) {
134+
dfs.MutateStep(terminal_segment, callback, node);
135+
}
136+
114137
return dfs;
115138
}
116139

src/core/json/detail/jsoncons_dfs.h

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

46+
// Added getter for the object pointer
47+
Ptr obj_ptr() const {
48+
return depth_state_.first;
49+
}
50+
51+
// Added getter for segment_step_
52+
unsigned get_segment_step() const {
53+
return segment_step_;
54+
}
55+
4656
private:
4757
static bool ShouldIterateAll(SegmentType type) {
4858
return type == SegmentType::WILDCARD || type == SegmentType::DESCENT;
@@ -128,8 +138,15 @@ class Dfs {
128138
}
129139

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

135152
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)