diff --git a/crates/lottie/src/anim.rs b/crates/lottie/src/anim.rs index 3a0f662e..8101b586 100644 --- a/crates/lottie/src/anim.rs +++ b/crates/lottie/src/anim.rs @@ -372,5 +372,5 @@ pub(crate) fn to_bool(j: &Json) -> Option { /// Lottie 1-based enum → 0-based popup index. pub(crate) fn to_enum1(j: &Json) -> Option { - 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)) } diff --git a/crates/lottie/src/lib.rs b/crates/lottie/src/lib.rs index 198ed7cf..1b12cc8b 100644 --- a/crates/lottie/src/lib.rs +++ b/crates/lottie/src/lib.rs @@ -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; diff --git a/crates/lottie/src/shapes.rs b/crates/lottie/src/shapes.rs index 82746cb0..d7e3cc08 100644 --- a/crates/lottie/src/shapes.rs +++ b/crates/lottie/src/shapes.rs @@ -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 { 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 { 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); } } } diff --git a/crates/lottie/src/tests.rs b/crates/lottie/src/tests.rs index 6c342380..9c92852d 100644 --- a/crates/lottie/src/tests.rs +++ b/crates/lottie/src/tests.rs @@ -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(); +} diff --git a/crates/lottie/src/text.rs b/crates/lottie/src/text.rs index 75489688..125835c2 100644 --- a/crates/lottie/src/text.rs +++ b/crates/lottie/src/text.rs @@ -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);