From 4a4e82daba45992a237306713d2f64d75bd9ce17 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:54:08 +0200 Subject: [PATCH] Reject missing or non-numeric values in mix.pan and mix.trim Found while testing crates/engine/src/cmd/mix.rs:237 (pan); same pattern at mix.rs:76 (mix.trim) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/mix.rs | 6 ++-- crates/engine/tests/mix_required_params.rs | 42 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 crates/engine/tests/mix_required_params.rs diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs index c0569429..faddc6ca 100644 --- a/crates/engine/src/cmd/mix.rs +++ b/crates/engine/src/cmd/mix.rs @@ -73,7 +73,8 @@ pub fn specs() -> Vec { )), cmd!("mix.trim", "Input Trim", [], None, "{tracks?, db}", has_selection, |e, p| { let tracks = tracks_required(e, "mix.trim", p)?; - let db = f32_or(p, "db", 0.0).clamp(-144.0, 24.0); + let db = (p.get("db").and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("mix.trim", "`db` (number) required"))? as f32) + .clamp(-144.0, 24.0); let s = e.session_mut(); for t in tracks { if let Some(tr) = s.track_mut(t) { @@ -234,7 +235,8 @@ fn volume(e: &mut Engine, p: &Value) -> Result { fn pan(e: &mut Engine, p: &Value) -> Result { let tracks = tracks_required(e, "mix.pan", p)?; - let v = f32_or(p, "pan", 0.0).clamp(-1.0, 1.0); + let v = (p.get("pan").and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("mix.pan", "`pan` (number) required"))? as f32) + .clamp(-1.0, 1.0); let idx = i64_or(p, "index", 0).clamp(0, 1) as usize; let s = e.session_mut(); for t in &tracks { diff --git a/crates/engine/tests/mix_required_params.rs b/crates/engine/tests/mix_required_params.rs new file mode 100644 index 00000000..f11e2f31 --- /dev/null +++ b/crates/engine/tests/mix_required_params.rs @@ -0,0 +1,42 @@ +use serde_json::json; +use soundcraft_engine::{Engine, EngineError}; + +fn engine_with_kick() -> Engine { + let mut e = Engine::default(); + e.execute("track.new", &json!({"name": "Kick", "format": "stereo"})).unwrap(); + e +} + +fn kick_pan(e: &Engine) -> Vec { + e.session().tracks.iter().find(|t| t.name == "Kick").unwrap().mixer.pan.to_vec() +} + +fn kick_trim(e: &Engine) -> f32 { + e.session().tracks.iter().find(|t| t.name == "Kick").unwrap().mixer.trim_db +} + +#[test] +fn mix_pan_rejects_a_missing_or_non_numeric_pan_and_keeps_the_pan() { + let mut e = engine_with_kick(); + e.execute("mix.pan", &json!({"tracks": ["Kick"], "pan": -1.0})).unwrap(); + assert_eq!(kick_pan(&e)[0], -1.0); + + for params in [json!({"tracks": ["Kick"]}), json!({"tracks": ["Kick"], "pan": "left"}), json!({"tracks": ["Kick"], "pan": null})] { + let r = e.execute("mix.pan", ¶ms); + assert!(matches!(r, Err(EngineError::BadParams(..))), "{params}: expected BadParams, got {r:?}"); + assert_eq!(kick_pan(&e)[0], -1.0, "{params}: the pan must be untouched"); + } +} + +#[test] +fn mix_trim_rejects_a_missing_or_non_numeric_db_and_keeps_the_trim() { + let mut e = engine_with_kick(); + e.execute("mix.trim", &json!({"tracks": ["Kick"], "db": -6.0})).unwrap(); + assert_eq!(kick_trim(&e), -6.0); + + for params in [json!({"tracks": ["Kick"]}), json!({"tracks": ["Kick"], "db": "loud"}), json!({"tracks": ["Kick"], "db": null})] { + let r = e.execute("mix.trim", ¶ms); + assert!(matches!(r, Err(EngineError::BadParams(..))), "{params}: expected BadParams, got {r:?}"); + assert_eq!(kick_trim(&e), -6.0, "{params}: the trim must be untouched"); + } +}