Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 60 additions & 33 deletions crates/bashkit/src/builtins/rg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4147,39 +4147,66 @@ fn write_rg_json_match(
regex: &RgMatcher,
replacement: Option<&str>,
) {
let submatches: Vec<_> = regex
.find_iter(line.match_text)
.into_iter()
.map(|mat| {
let mut value = json!({
"match":{"text":mat.as_str()},
"start":mat.start(),
"end":mat.end()
});
if let Some(replacement) = replacement
&& let Some(obj) = value.as_object_mut()
{
obj.insert(
"replacement".to_string(),
json!({"text":regex.replace_first(mat.as_str(), replacement)}),
);
}
value
})
.collect();
write_rg_json_event(
output,
json!({
"type":"match",
"data":{
"path":{"text":filename},
"lines":{"text":line.raw},
"line_number":line_idx + 1,
"absolute_offset":line.start_offset,
"submatches":submatches,
}
}),
);
// Stream submatches straight into the JSON output buffer instead of first
// collecting them into a Vec<serde_json::Value> and then serializing the
// whole tree. Avoids an O(matches_per_line) intermediate allocation on
// attacker-controlled lines with many matches.
//
// serde_json serializes Object keys in lexicographic order, so the outer
// event looks like `{"data":{...},"type":"match"}` — only one trailing `}`.
// Build the inner `data` object on its own, strip its trailing `}`,
// append `"submatches":[...]` (which sorts last alphabetically after
// `path`), close `data`, then close the outer envelope by hand.
let data = json!({
"path":{"text":filename},
"lines":{"text":line.raw},
"line_number":line_idx + 1,
"absolute_offset":line.start_offset,
});
let data_str = data.to_string();
let Some(data_open) = data_str.strip_suffix('}') else {
// Defensive fallback — preserve the previous semantics if serde_json
// ever produces an unexpected shape.
write_rg_json_event(
output,
json!({
"type":"match",
"data":{
"path":{"text":filename},
"lines":{"text":line.raw},
"line_number":line_idx + 1,
"absolute_offset":line.start_offset,
"submatches":[],
}
}),
);
return;
};
output.push_str("{\"data\":");
output.push_str(data_open);
output.push_str(",\"submatches\":[");
let mut first = true;
for mat in regex.find_iter(line.match_text) {
if !first {
output.push(',');
}
first = false;
let mut value = json!({
"match":{"text":mat.as_str()},
"start":mat.start(),
"end":mat.end()
});
if let Some(replacement) = replacement
&& let Some(obj) = value.as_object_mut()
{
obj.insert(
"replacement".to_string(),
json!({"text":regex.replace_first(mat.as_str(), replacement)}),
);
}
output.push_str(&value.to_string());
}
output.push_str("]},\"type\":\"match\"}\n");
}

fn write_rg_json_context(output: &mut String, filename: &str, line: RgLine<'_>, line_idx: usize) {
Expand Down
Loading