Convert Expression to Keyframes: refuse layers of more than 2^18 frames

* fix(engine): refuse to convert an expression on a layer of more than 2^18 frames

* address review: refuse a non-positive frame duration; count a partial last frame; check every selected layer before converting

* Convert Expression to Keyframes: cap at 2^20 frames (a 3-hour 60 fps comp is 648,000)
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-12 06:29:38 -05:00
1 parent 2b30f53e7a
commit 4d6036265b
2 files changed
+55 -1

No files matched your search

+30 -1
View File
@@ -1370,6 +1370,13 @@ fn convert_expr_to_keys(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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<Value> {
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 {
+25
View File
@@ -1581,3 +1581,28 @@ fn a_typed_component_moves_its_follow_components_by_each_layers_own_change() {
let keys: Vec<Vec<f64>> = 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());
}