Merge pull request #25 from storytold/pr/never-crash-lottie

Never crash: Lottie enum values can't overflow
This commit is contained in:
Brandon Thomas 2026-10-04 15:29:22 -04:00 committed by GitHub
commit d350f4e7ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 36 additions and 6 deletions

View File

@ -372,5 +372,5 @@ pub(crate) fn to_bool(j: &Json) -> Option<Value> {
/// Lottie 1-based enum → 0-based popup index.
pub(crate) fn to_enum1(j: &Json) -> Option<Value> {
to_scalar(j).map(|v| Value::Enum((v.as_f64().round() as i64 - 1).max(0) as u32))
to_scalar(j).map(|v| Value::Enum((v.as_f64().round() as i64).saturating_sub(1).max(0) as u32))
}

View File

@ -18,6 +18,8 @@
//! Keyframe times in Lottie are frames of the layer's local time, which is EffectCraft's layer
//! time; layer `ip`/`op`/`st` are composition frames and `sr` is the stretch factor.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod anim;
mod base64;
mod export;

View File

@ -359,10 +359,10 @@ fn import_blend(j: &Json, g: &mut PropGroup) {
fn import_stroke(im: &Im, j: &Json, g: &mut PropGroup) {
set(im, g, "width", j.get("w"), &anim::to_scalar);
if let Some(lc) = int(j, "lc") {
set_enum(g, "cap", (lc - 1).clamp(0, 2) as u32);
set_enum(g, "cap", lc.saturating_sub(1).clamp(0, 2) as u32);
}
if let Some(lj) = int(j, "lj") {
set_enum(g, "join", (lj - 1).clamp(0, 2) as u32);
set_enum(g, "join", lj.saturating_sub(1).clamp(0, 2) as u32);
}
if let Some(p) = g.get_mut("miter") {
if let Some(ml) = j.get("ml").and_then(Json::as_f64) {
@ -541,7 +541,7 @@ pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec<Pr
"op" => {
set(im, &mut g, "amount", j.get("a"), &anim::to_scalar);
if let Some(lj) = int(j, "lj") {
set_enum(&mut g, "join", (lj - 1).clamp(0, 2) as u32);
set_enum(&mut g, "join", lj.saturating_sub(1).clamp(0, 2) as u32);
}
if let (Some(ml), Some(p)) = (j.get("ml").and_then(Json::as_f64), g.get_mut("miter")) {
p.value = Value::Scalar(ml);
@ -561,7 +561,7 @@ pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec<Pr
}
_ => {
if let Some(m) = int(j, "mm") {
set_enum(&mut g, "mode", (m - 1).clamp(0, 4) as u32);
set_enum(&mut g, "mode", m.saturating_sub(1).clamp(0, 4) as u32);
}
}
}

View File

@ -725,3 +725,31 @@ fn text_style_runs_export_as_range_animators() {
// Played back as an animator, it looks the same.
compare_renders(&p, cid, &q, nc, 2.0 / 255.0);
}
#[test]
fn extreme_enum_numbers_do_not_overflow() {
// Line cap / join / merge mode / text-selector enums are 1-based; a huge negative number
// (-1e308 rounds to i64::MIN) overflowed the `- 1`.
let path = format!("{}/tests/fixtures/shapes-gradient.json", env!("CARGO_MANIFEST_DIR"));
let mut doc: Json = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
let mut hits = 0;
fn patch(j: &mut Json, hits: &mut usize) {
match j {
Json::Object(o) => {
for k in ["lc", "lj", "mm"] {
if o.contains_key(k) {
o.insert(k.into(), json!(-1e308));
*hits += 1;
}
}
o.values_mut().for_each(|v| patch(v, hits));
}
Json::Array(a) => a.iter_mut().for_each(|v| patch(v, hits)),
_ => {}
}
}
patch(&mut doc, &mut hits);
assert!(hits > 0);
let mut q = Project::default();
import(&mut q, doc.to_string().as_bytes(), "x", "", &mut |_| None).unwrap();
}

View File

@ -363,7 +363,7 @@ pub(crate) fn import_text(im: &mut Im, ids: &mut Ids, t: &Json, tg: &mut PropGro
let mut en = |m: &str, key: &str, max: u32| {
let v = s.get(key).and_then(|v| if v.is_object() { v.get("k") } else { Some(v) }).and_then(anim::nums).and_then(|n| n.first().copied());
if let (Some(v), Some(p)) = (v, adv.get_mut(m)) {
p.value = Value::Enum(((v.round() as i64 - 1).max(0) as u32).min(max));
p.value = Value::Enum((((v.round() as i64).saturating_sub(1)).max(0) as u32).min(max));
}
};
en("units", "r", 1);