diff --git a/crates/engine/src/cmd/prop.rs b/crates/engine/src/cmd/prop.rs index b23e73575..761a295b2 100644 --- a/crates/engine/src/cmd/prop.rs +++ b/crates/engine/src/cmd/prop.rs @@ -1370,6 +1370,13 @@ fn convert_expr_to_keys(s: &mut Session, p: &Value) -> Result { if sel.is_empty() { return Err(bad(CMD, "no `path` given and no selected property has an expression")); } + // Every layer is checked before the first is converted: a refusal leaves them all as they were. + let step = comp.frame_duration().0; + for (lid, _) in &sel { + if let Some(l) = comp.layer(*lid) { + check_convertible(step, l)?; + } + } let mut n = 0; for (lid, uid) in sel { n += convert_one_expr(s, cid, lid, uid)?.as_u64().unwrap_or(0); @@ -1380,14 +1387,36 @@ fn convert_expr_to_keys(s: &mut Session, p: &Value) -> Result { convert_one_expr(s, cid, lid, uid) } +/// Most keyframes Convert Expression to Keyframes makes: 2^20, 4.8 hours at 60 fps (After Effects +/// compositions run to 3 hours, 648,000 frames at 60 fps). +const MAX_CONVERTED_KEYS: u64 = 1 << 20; +const _: () = assert!(MAX_CONVERTED_KEYS >= 3 * 3600 * 60); + +/// One evaluation and one key per frame of the layer: its In and Out points and the composition's +/// frame duration are saved data, so the step must advance and the count (a partial last frame +/// rounded up, as the loop steps, in 128 bits) is checked before any key is made. +fn check_convertible(step: i64, layer: &Layer) -> Result<()> { + const CMD: &str = "prop.convertExpressionToKeyframes"; + if step <= 0 { + return Err(bad(CMD, "the composition's frame duration is not positive")); + } + let (step, span) = (i128::from(step), i128::from(layer.out_point.0) - i128::from(layer.in_point.0)); + let frames = if span > 0 { (span + step - 1) / step } else { 0 }; + if frames > i128::from(MAX_CONVERTED_KEYS) { + return Err(bad(CMD, format!("the layer spans {frames} frames; at most {MAX_CONVERTED_KEYS} are converted"))); + } + Ok(()) +} + /// Replaces the expression on one property with keyframes sampled from its evaluated values. /// Returns the number of properties converted. fn convert_one_expr(s: &mut Session, cid: ItemId, lid: LayerId, uid: Uid) -> Result { let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone(); let layer = comp.layer(lid).ok_or(EngineError::NoComp)?.clone(); let pr = layer.props.find(uid).ok_or_else(|| bad("prop.convertExpressionToKeyframes", "no property"))?.clone(); - let mut keys = vec![]; let fd = comp.frame_duration(); + check_convertible(fd.0, &layer)?; + let mut keys = vec![]; let mut t = layer.in_point; while t < layer.out_point { let ctx = effectcraft_render::EvalCtx { diff --git a/crates/engine/src/tests/timeline.rs b/crates/engine/src/tests/timeline.rs index b5a44239b..291ad884e 100644 --- a/crates/engine/src/tests/timeline.rs +++ b/crates/engine/src/tests/timeline.rs @@ -1581,3 +1581,28 @@ fn a_typed_component_moves_its_follow_components_by_each_layers_own_change() { let keys: Vec> = prop(&s, b, "audio/levels").keys.iter().map(|k| k.value.components()).collect(); assert_eq!(keys, vec![vec![-20.0, -30.0], vec![-4.0, -34.0]], "B's 1 s key rises 6 dB on both channels"); } + +#[test] +fn convert_expression_to_keyframes_refuses_a_layer_of_millions_of_frames() { + let (mut s, l) = setup(); + s.execute("prop.setExpression", json!({"layer": l, "path": "transform/rotation", "expression": "value"})).unwrap(); + // A saved Out point a million seconds after the In point. + let cid = s.active_comp_id().unwrap(); + std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap().layer_mut(effectcraft_project::LayerId(l)).unwrap().out_point = + effectcraft_time::Tick::from_seconds_f64(1e6); + let e = s.execute("prop.convertExpressionToKeyframes", json!({"layer": l, "path": "transform/rotation"})).unwrap_err().to_string(); + assert!(e.contains("at most"), "{e}"); + assert!(prop(&s, l, "transform/rotation").keys.is_empty()); +} + +#[test] +fn convert_expression_to_keyframes_refuses_a_zero_frame_duration() { + let (mut s, l) = setup(); + s.execute("prop.setExpression", json!({"layer": l, "path": "transform/rotation", "expression": "value"})).unwrap(); + // A saved frame rate of 0/1 s has a frame duration of 0: the sampling loop never advanced. + let cid = s.active_comp_id().unwrap(); + std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap().frame_rate.den = 0; + let e = s.execute("prop.convertExpressionToKeyframes", json!({"layer": l, "path": "transform/rotation"})).unwrap_err().to_string(); + assert!(e.contains("frame duration"), "{e}"); + assert!(prop(&s, l, "transform/rotation").keys.is_empty()); +}