Skip to content

Commit 0244f62

Browse files
authored
feat(deps): upgrade jaq to 3.0, digest crates to 0.11 (#893)
## Summary - Upgrade jaq-core 2.2→3.0, jaq-std 2.1→3.0, jaq-json 1.1→2.0 - Upgrade md-5, sha1, sha2 from 0.10→0.11 (digest ecosystem 0.11) - generic-array removed from dep tree (replaced by hybrid-array) ## Why These were flagged as outdated during maintenance but skipped because `cargo update` only resolves within semver-compatible ranges. Major version bumps require editing `Cargo.toml` constraints. ## Changes **jq.rs** — Rewrote jaq integration for 3.0 API: - Custom `DataT` impl (`InputData`/`InputDataRef`) for `input`/`inputs` filter support - Manual `serde_json::Value` ↔ `jaq_json::Val` conversion (From impls removed in jaq-json 2.0) - Updated Compiler/Ctx/Filter API (`Vars::new`, `filter.id.run`, `Ctx::new(data, vars)`) - Chain `jaq_core::defs()` alongside std/json defs (new requirement in 3.0) **checksum.rs** — No code changes needed (digest 0.11 API compatible) **jq.test.sh** — Fixed `jq_try` spec test: `.foo` on `null` returns `null` (not error) in jaq 3.0, matching real jq behavior ## Test plan - [x] 54 jq unit tests pass - [x] 115 jq spec tests pass (0 failures, 1 skipped) - [x] 4 checksum unit tests pass - [x] Full test suite green - [x] `cargo clippy --all-targets --all-features -- -D warnings` clean - [x] `cargo fmt --check` clean
1 parent 32c65ca commit 0244f62

File tree

4 files changed

+199
-36
lines changed

4 files changed

+199
-36
lines changed

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ serde = { version = "1", features = ["derive"] }
3333
serde_json = "1"
3434

3535
# JSON processing (jq) - verified embeddable
36-
jaq-core = "2.2"
37-
jaq-std = "2.1"
38-
jaq-json = { version = "1.1", features = ["serde_json"] }
36+
jaq-core = "3.0"
37+
jaq-std = "3.0"
38+
jaq-json = "2.0"
3939

4040
# Text search (grep) - verified supports search_slice() for in-memory
4141
grep = "0.3"
@@ -61,9 +61,9 @@ flate2 = "1"
6161
base64 = "0.22"
6262

6363
# Checksums (md5sum, sha256sum, etc.)
64-
md-5 = "0.10"
65-
sha1 = "0.10"
66-
sha2 = "0.10"
64+
md-5 = "0.11"
65+
sha1 = "0.11"
66+
sha2 = "0.11"
6767

6868
# CLI
6969
clap = { version = "4", features = ["derive"] }

crates/bashkit/src/builtins/jq.rs

Lines changed: 144 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,123 @@
77
//! jq '.[] | .id' < data.json
88
99
use async_trait::async_trait;
10-
use jaq_core::{Compiler, Ctx, RcIter, load};
10+
use jaq_core::load::{Arena, File, Loader};
11+
use jaq_core::{Compiler, Ctx, Vars, data};
1112
use jaq_json::Val;
13+
use jaq_std::input::{HasInputs, Inputs, RcIter};
1214

1315
use super::{Builtin, Context, read_text_file, resolve_path};
1416
use crate::error::{Error, Result};
1517
use crate::interpreter::ExecResult;
1618

19+
/// Custom DataT that holds both the LUT and a shared input iterator.
20+
/// Required by jaq 3.0 for `input`/`inputs` filter support.
21+
struct InputData<V>(std::marker::PhantomData<V>);
22+
23+
impl<V: jaq_core::ValT + 'static> data::DataT for InputData<V> {
24+
type V<'a> = V;
25+
type Data<'a> = InputDataRef<'a, V>;
26+
}
27+
28+
#[derive(Clone)]
29+
struct InputDataRef<'a, V: jaq_core::ValT + 'static> {
30+
lut: &'a jaq_core::Lut<InputData<V>>,
31+
inputs: &'a RcIter<dyn Iterator<Item = std::result::Result<V, String>> + 'a>,
32+
}
33+
34+
impl<'a, V: jaq_core::ValT + 'static> data::HasLut<'a, InputData<V>> for InputDataRef<'a, V> {
35+
fn lut(&self) -> &'a jaq_core::Lut<InputData<V>> {
36+
self.lut
37+
}
38+
}
39+
40+
impl<'a, V: jaq_core::ValT + 'static> HasInputs<'a, V> for InputDataRef<'a, V> {
41+
fn inputs(&self) -> Inputs<'a, V> {
42+
self.inputs
43+
}
44+
}
45+
46+
/// Convert serde_json::Value to jaq Val.
47+
fn serde_to_val(v: serde_json::Value) -> Val {
48+
match v {
49+
serde_json::Value::Null => Val::Null,
50+
serde_json::Value::Bool(b) => Val::from(b),
51+
serde_json::Value::Number(n) => {
52+
if let Some(i) = n.as_i64() {
53+
if let Ok(i) = isize::try_from(i) {
54+
Val::from(i)
55+
} else {
56+
Val::from(i as f64)
57+
}
58+
} else if let Some(f) = n.as_f64() {
59+
Val::from(f)
60+
} else {
61+
Val::from(0isize) // unreachable in practice
62+
}
63+
}
64+
serde_json::Value::String(s) => Val::from(s),
65+
serde_json::Value::Array(arr) => arr.into_iter().map(serde_to_val).collect(),
66+
serde_json::Value::Object(map) => Val::obj(
67+
map.into_iter()
68+
.map(|(k, v)| (Val::from(k), serde_to_val(v)))
69+
.collect(),
70+
),
71+
}
72+
}
73+
74+
/// Convert jaq Val to serde_json::Value for output formatting.
75+
fn val_to_serde(v: &Val) -> serde_json::Value {
76+
match v {
77+
Val::Null => serde_json::Value::Null,
78+
Val::Bool(b) => serde_json::Value::Bool(*b),
79+
Val::Num(n) => {
80+
// Use Display to get the number string, then parse
81+
let s = format!("{n}");
82+
if let Ok(i) = s.parse::<i64>() {
83+
serde_json::Value::Number(serde_json::Number::from(i))
84+
} else if let Ok(f) = s.parse::<f64>() {
85+
serde_json::Number::from_f64(f)
86+
.map(serde_json::Value::Number)
87+
.unwrap_or(serde_json::Value::Null)
88+
} else {
89+
serde_json::Value::Null
90+
}
91+
}
92+
Val::BStr(_) | Val::TStr(_) => {
93+
// Extract string bytes and convert to UTF-8
94+
let displayed = format!("{v}");
95+
// Val's Display wraps strings in quotes — strip them
96+
if displayed.starts_with('"') && displayed.ends_with('"') {
97+
// Parse the JSON string to unescape
98+
serde_json::from_str(&displayed).unwrap_or(serde_json::Value::String(displayed))
99+
} else {
100+
serde_json::Value::String(displayed)
101+
}
102+
}
103+
Val::Arr(a) => serde_json::Value::Array(a.iter().map(val_to_serde).collect()),
104+
Val::Obj(o) => {
105+
let map: serde_json::Map<String, serde_json::Value> = o
106+
.iter()
107+
.map(|(k, v)| {
108+
let key = match k {
109+
Val::TStr(_) | Val::BStr(_) => {
110+
let s = format!("{k}");
111+
if s.starts_with('"') && s.ends_with('"') {
112+
serde_json::from_str::<String>(&s).unwrap_or(s)
113+
} else {
114+
s
115+
}
116+
}
117+
_ => format!("{k}"),
118+
};
119+
(key, val_to_serde(v))
120+
})
121+
.collect();
122+
serde_json::Value::Object(map)
123+
}
124+
}
125+
}
126+
17127
/// THREAT[TM-DOS-027]: Maximum nesting depth for JSON input values.
18128
/// Prevents stack overflow when jaq evaluates deeply nested JSON structures
19129
/// like `[[[[...]]]]` or `{"a":{"a":{"a":...}}}`.
@@ -307,8 +417,11 @@ impl Builtin for Jq {
307417
}
308418

309419
// Set up the loader with standard library definitions
310-
let loader = load::Loader::new(jaq_std::defs().chain(jaq_json::defs()));
311-
let arena = load::Arena::default();
420+
let defs = jaq_core::defs()
421+
.chain(jaq_std::defs())
422+
.chain(jaq_json::defs());
423+
let loader = Loader::new(defs);
424+
let arena = Arena::default();
312425

313426
// Build shell env as a JSON object for the custom `env` filter.
314427
// SECURITY: This avoids calling std::env::set_var() which is
@@ -333,7 +446,7 @@ impl Builtin for Jq {
333446
let filter = compat_filter.as_str();
334447

335448
// Parse the filter
336-
let program = load::File {
449+
let program = File {
337450
code: filter,
338451
path: (),
339452
};
@@ -358,9 +471,16 @@ impl Builtin for Jq {
358471
// a def that reads from our injected global variable.
359472
let mut var_names: Vec<&str> = var_bindings.iter().map(|(n, _)| n.as_str()).collect();
360473
var_names.push(ENV_VAR_NAME);
361-
let native_funs = jaq_std::funs()
362-
.filter(|(name, _, _)| *name != "env")
363-
.chain(jaq_json::funs());
474+
type D = InputData<Val>;
475+
let input_funs: Vec<jaq_core::native::Fun<D>> = jaq_std::input::funs::<D>()
476+
.into_vec()
477+
.into_iter()
478+
.map(|(name, arity, run)| (name, arity, jaq_core::Native::<D>::new(run)))
479+
.collect();
480+
let native_funs = jaq_core::funs::<D>()
481+
.chain(jaq_std::funs::<D>().filter(|(name, _, _)| *name != "env"))
482+
.chain(input_funs)
483+
.chain(jaq_json::funs::<D>());
364484
let compiler = Compiler::default()
365485
.with_funs(native_funs)
366486
.with_global_vars(var_names.iter().copied());
@@ -384,26 +504,26 @@ impl Builtin for Jq {
384504
// Build list of inputs to process
385505
let inputs_to_process: Vec<Val> = if null_input {
386506
// -n flag: use null as input
387-
vec![Val::from(serde_json::Value::Null)]
507+
vec![Val::Null]
388508
} else if raw_input && slurp {
389509
// -Rs flag: read entire input as single string
390-
vec![Val::from(serde_json::Value::String(input.to_string()))]
510+
vec![Val::from(input.to_string())]
391511
} else if raw_input {
392512
// -R flag: each line becomes a JSON string value
393513
input
394514
.lines()
395-
.map(|line| Val::from(serde_json::Value::String(line.to_string())))
515+
.map(|line| Val::from(line.to_string()))
396516
.collect()
397517
} else if slurp {
398518
// -s flag: read all inputs into a single array
399519
match Self::parse_json_values(input) {
400-
Ok(vals) => vec![Val::from(serde_json::Value::Array(vals))],
520+
Ok(vals) => vec![serde_to_val(serde_json::Value::Array(vals))],
401521
Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 5)),
402522
}
403523
} else {
404524
// Parse all JSON values from input (handles multi-line and NDJSON)
405525
match Self::parse_json_values(input) {
406-
Ok(json_vals) => json_vals.into_iter().map(Val::from).collect(),
526+
Ok(json_vals) => json_vals.into_iter().map(serde_to_val).collect(),
407527
Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 5)),
408528
}
409529
};
@@ -414,10 +534,12 @@ impl Builtin for Jq {
414534

415535
// Shared input iterator: main loop pops one value per filter run,
416536
// and jaq's input/inputs functions consume from the same source.
417-
let shared_inputs = RcIter::new(inputs_to_process.into_iter().map(Ok::<Val, String>));
537+
let iter: Box<dyn Iterator<Item = std::result::Result<Val, String>>> =
538+
Box::new(inputs_to_process.into_iter().map(Ok::<Val, String>));
539+
let shared_inputs = RcIter::new(iter);
418540

419541
// Pre-convert env object to jaq Val once (reused for each input)
420-
let env_val = Val::from(env_obj);
542+
let env_val = serde_to_val(env_obj);
421543

422544
for jaq_input in &shared_inputs {
423545
let jaq_input: Val = match jaq_input {
@@ -431,16 +553,20 @@ impl Builtin for Jq {
431553
// plus the env object as the last global variable.
432554
let mut var_vals: Vec<Val> = var_bindings
433555
.iter()
434-
.map(|(_, v)| Val::from(v.clone()))
556+
.map(|(_, v)| serde_to_val(v.clone()))
435557
.collect();
436558
var_vals.push(env_val.clone());
437-
let ctx = Ctx::new(var_vals, &shared_inputs);
438-
for result in filter.run((ctx, jaq_input)) {
559+
let data = InputDataRef {
560+
lut: &filter.lut,
561+
inputs: &shared_inputs,
562+
};
563+
let ctx = Ctx::<InputData<Val>>::new(data, Vars::new(var_vals));
564+
for result in filter.id.run((ctx, jaq_input)) {
439565
match result {
440566
Ok(val) => {
441567
has_output = true;
442568
// Convert back to serde_json::Value and format
443-
let json: serde_json::Value = val.into();
569+
let json = val_to_serde(&val);
444570

445571
// Track for -e exit status
446572
if !matches!(

crates/bashkit/tests/spec_cases/jq/jq.test.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,8 @@ echo 'null' | jq '.foo // "default"'
490490

491491
### jq_try
492492
# Try-catch handles runtime errors gracefully
493-
echo 'null' | jq 'try .foo catch "error"'
493+
# .foo on null returns null (not an error), so use error/0 to trigger catch
494+
echo '1' | jq 'try error catch "error"'
494495
### expect
495496
"error"
496497
### end

0 commit comments

Comments
 (0)