mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 01:47:08 -06:00
security(engine): count GIF frame delays with a map
fps_from_delays counted each frame's delay by scanning a growing vector with iter_mut().find(), O(n^2) for a GIF with many distinct delays. A crafted animation within the frame limit could stall the synchronous import path. Count delays in a HashMap and keep a first-seen order vector so the most-common delay still wins, breaking ties toward the earliest-seen delay exactly as before. Finding: 23d54421-8e26-41b9-9690-ae1125ee2f85 (CWE-407)
This commit is contained in:
@@ -227,16 +227,24 @@ struct Footage {
|
||||
/// do, since they would otherwise play at an unintended 50-100 fps.
|
||||
fn fps_from_delays(delays: &[u32]) -> Option<f32> {
|
||||
let clean = |d: u32| if d < 20 { 100 } else { d };
|
||||
let mut counts: Vec<(u32, usize)> = Vec::new();
|
||||
// Count delays with a map keyed on the cleaned delay, recording first-seen order
|
||||
// separately. A linear scan per frame (iter_mut().find) was O(n^2) for a GIF with many
|
||||
// distinct delays. order[] preserves the tie-break: the earliest-seen delay wins.
|
||||
let mut counts: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
|
||||
let mut order: Vec<u32> = Vec::new();
|
||||
for d in delays.iter().copied().map(clean) {
|
||||
match counts.iter_mut().find(|(k, _)| *k == d) {
|
||||
Some((_, n)) => *n += 1,
|
||||
None => counts.push((d, 1)),
|
||||
}
|
||||
let n = counts.entry(d).or_insert_with(|| {
|
||||
order.push(d);
|
||||
0
|
||||
});
|
||||
*n += 1;
|
||||
}
|
||||
let best = counts.iter().fold(None::<(u32, usize)>, |best, &(d, n)| match best {
|
||||
Some((_, bn)) if bn >= n => best,
|
||||
_ => Some((d, n)),
|
||||
let best = order.iter().fold(None::<(u32, usize)>, |best, &d| {
|
||||
let n = counts[&d];
|
||||
match best {
|
||||
Some((_, bn)) if bn >= n => best,
|
||||
_ => Some((d, n)),
|
||||
}
|
||||
});
|
||||
best.map(|(d, _)| 1000.0 / d as f32)
|
||||
}
|
||||
|
||||
@@ -579,3 +579,19 @@ fn render_video_rejects_gif_frames_too_large_to_stage() {
|
||||
let err = s.execute("file.export.renderVideo", json!({"dir": tmpdir("large-gif"), "format": "gif"})).unwrap_err();
|
||||
assert!(err.to_string().contains("too large"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fps_from_delays_picks_most_common_and_breaks_ties_by_first_seen() {
|
||||
// Most common delay wins: 100 ms appears twice, 40 ms once -> 10 fps.
|
||||
assert_eq!(fps_from_delays(&[100, 40, 100]), Some(10.0));
|
||||
// Tie between 40 ms (25 fps) and 50 ms (20 fps): the first seen (40) wins.
|
||||
assert_eq!(fps_from_delays(&[40, 50]), Some(25.0));
|
||||
// Delays under 20 ms are treated as 100 ms.
|
||||
assert_eq!(fps_from_delays(&[0, 10, 5]), Some(10.0));
|
||||
assert_eq!(fps_from_delays(&[]), None);
|
||||
// Many distinct delays used to be quadratic; this must stay fast and correct. Delay 20
|
||||
// (the one appearing twice) wins at 50 fps.
|
||||
let mut many: Vec<u32> = (20..5_020).collect();
|
||||
many.push(20);
|
||||
assert_eq!(fps_from_delays(&many), Some(50.0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user