From 780419c235bf17b3c5e5fc6eeefbe7d75ceb41a3 Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sat, 3 Oct 2026 09:31:41 -0700 Subject: [PATCH] Compositions: reject zero and negative frame rates instead of panicking comp.new / comp.settings passed `frameRate` straight to FrameRate::from_f64, so 0 (or anything below 0.0005) became a 0/1 rate and the next frame-duration computation divided by zero, killing the process (and an MCP session with its unsaved project); a negative rate was accepted as-is. Scripts hit it via addComp(..., 0) or comp.frameRate = 0. Return a parameter error like renderQueue.add's frameRate check does. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/engine/src/commands/comp.rs | 10 ++++++++++ crates/engine/src/tests.rs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/engine/src/commands/comp.rs b/crates/engine/src/commands/comp.rs index dec74406..ce92599e 100644 --- a/crates/engine/src/commands/comp.rs +++ b/crates/engine/src/commands/comp.rs @@ -12,6 +12,14 @@ pub(crate) fn rate_p(p: &Value) -> Option { f_p(p, "frameRate").or(f_p(p, "fps")).map(FrameRate::from_f64) } +/// A zero, negative or tiny rate would make a zero-length frame (and divide by zero later). +fn check_rate(p: &Value, cmd: &str) -> Result<()> { + match f_p(p, "frameRate").or(f_p(p, "fps")) { + Some(f) if !(0.001..=1000.0).contains(&f) => Err(bad(cmd, "frameRate: fps from 0.001 to 1000")), + _ => Ok(()), + } +} + fn apply_settings(c: &mut Comp, p: &Value) { let (ow, oh) = (c.width, c.height); if let Some(w) = p.get("width").and_then(Value::as_u64) { @@ -93,6 +101,7 @@ fn apply_settings(c: &mut Comp, p: &Value) { } fn new_comp(s: &mut Session, p: &Value) -> Result { + check_rate(p, "comp.new")?; let name = str_p(p, "name").unwrap_or("Comp 1").to_string(); let rate = rate_p(p).unwrap_or(FrameRate::FPS_29_97); let mut c = Comp::new(1920, 1080, rate, rate.snap_nearest(Tick::from_seconds_f64(10.0))); @@ -119,6 +128,7 @@ fn new_comp(s: &mut Session, p: &Value) -> Result { } fn settings(s: &mut Session, p: &Value) -> Result { + check_rate(p, "comp.settings")?; let cid = comp_id(s, p)?; s.edit("Composition Settings", None, |proj, _| { if let Some(n) = str_p(p, "name") diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c18d957e..a0acbd1a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -301,6 +301,17 @@ fn comp_settings_anchor_start_timecode_renderer() { assert_eq!(c.renderer, crate::project::Renderer::Advanced3D); } +#[test] +fn comp_rejects_zero_and_negative_frame_rates() { + let mut s = Session::default(); + for fps in [0.0, -30.0, 0.0001] { + assert!(s.execute("comp.new", json!({"frameRate": fps})).is_err(), "{fps}"); + } + s.execute("comp.new", json!({"frameRate": 30, "duration": 1})).unwrap(); + assert!(s.execute("comp.settings", json!({"frameRate": 0})).is_err()); + assert_eq!(s.active_comp().unwrap().frame_rate, crate::time::FrameRate::FPS_30); +} + #[test] fn render_queue_add_accepts_documented_settings_when_checked() { let mut s = demo();