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) <noreply@anthropic.com>
This commit is contained in:
storybored379 2026-10-03 09:31:41 -07:00
parent 4bfb689f19
commit 780419c235
2 changed files with 21 additions and 0 deletions

View File

@ -12,6 +12,14 @@ pub(crate) fn rate_p(p: &Value) -> Option<FrameRate> {
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<Value> {
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<Value> {
}
fn settings(s: &mut Session, p: &Value) -> Result<Value> {
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")

View File

@ -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();