Skip to content

Commit 780ef1f

Browse files
chaliyclaude
andauthored
fix(builtins): use streaming JSON deserializer in jq for multi-line input (#194)
## Summary - Replace line-by-line JSON parsing with `serde_json::StreamDeserializer` - Properly handles multi-line pretty-printed JSON, NDJSON, and concatenated values - Works for both regular and slurp (`-s`) modes ## Test plan - [x] All 19 jq unit tests pass - [x] jq spec tests pass - [x] `cargo check` clean Co-authored-by: Claude <noreply@anthropic.com>
1 parent 300ad8c commit 780ef1f

1 file changed

Lines changed: 28 additions & 26 deletions

File tree

  • crates/bashkit/src/builtins

crates/bashkit/src/builtins/jq.rs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ const MAX_JQ_JSON_DEPTH: usize = 100;
2525
/// jq command - JSON processor
2626
pub struct Jq;
2727

28+
impl Jq {
29+
/// Parse multiple JSON values from input using streaming deserializer.
30+
/// Handles multi-line JSON, NDJSON, and concatenated JSON values.
31+
fn parse_json_values(input: &str) -> Result<Vec<serde_json::Value>> {
32+
use serde_json::Deserializer;
33+
34+
let trimmed = input.trim();
35+
if trimmed.is_empty() {
36+
return Ok(Vec::new());
37+
}
38+
39+
let mut vals = Vec::new();
40+
let stream = Deserializer::from_str(trimmed).into_iter::<serde_json::Value>();
41+
for result in stream {
42+
let json_input =
43+
result.map_err(|e| Error::Execution(format!("jq: invalid JSON: {}", e)))?;
44+
// THREAT[TM-DOS-027]: Check nesting depth before evaluation
45+
check_json_depth(&json_input, MAX_JQ_JSON_DEPTH).map_err(Error::Execution)?;
46+
vals.push(json_input);
47+
}
48+
Ok(vals)
49+
}
50+
}
51+
2852
/// THREAT[TM-DOS-027]: Check JSON nesting depth to prevent stack overflow
2953
/// during jaq filter evaluation on deeply nested input.
3054
fn check_json_depth(
@@ -220,34 +244,12 @@ impl Builtin for Jq {
220244
vec![Val::from(serde_json::Value::Null)]
221245
} else if slurp {
222246
// -s flag: read all inputs into a single array
223-
let mut vals = Vec::new();
224-
for line in input.lines() {
225-
let line = line.trim();
226-
if line.is_empty() {
227-
continue;
228-
}
229-
let json_input: serde_json::Value = serde_json::from_str(line)
230-
.map_err(|e| Error::Execution(format!("jq: invalid JSON: {}", e)))?;
231-
// THREAT[TM-DOS-027]: Check nesting depth before evaluation
232-
check_json_depth(&json_input, MAX_JQ_JSON_DEPTH).map_err(Error::Execution)?;
233-
vals.push(json_input);
234-
}
247+
let vals = Self::parse_json_values(input)?;
235248
vec![Val::from(serde_json::Value::Array(vals))]
236249
} else {
237-
// Process each line of input as JSON
238-
let mut vals = Vec::new();
239-
for line in input.lines() {
240-
let line = line.trim();
241-
if line.is_empty() {
242-
continue;
243-
}
244-
let json_input: serde_json::Value = serde_json::from_str(line)
245-
.map_err(|e| Error::Execution(format!("jq: invalid JSON: {}", e)))?;
246-
// THREAT[TM-DOS-027]: Check nesting depth before evaluation
247-
check_json_depth(&json_input, MAX_JQ_JSON_DEPTH).map_err(Error::Execution)?;
248-
vals.push(Val::from(json_input));
249-
}
250-
vals
250+
// Parse all JSON values from input (handles multi-line and NDJSON)
251+
let json_vals = Self::parse_json_values(input)?;
252+
json_vals.into_iter().map(Val::from).collect()
251253
};
252254

253255
// Track for -e exit status

0 commit comments

Comments
 (0)