Skip to main content

unity_formats/
clip.rs

1//! Unity AnimationClips: the curves of a Mecanim clip (`m_MuscleClip`) decoded to keys with tangents.
2//!
3//! A clip stores its curves in three blocks, numbered in this order: the **streamed** curves (`m_StreamedClip`), the
4//! **dense** curves (`m_DenseClip`, one sample per frame at a fixed rate) and the **constant** curves
5//! (`m_ConstantClip`, one value each). `m_ClipBindingConstant.genericBindings` lists what the curves drive, in curve
6//! order: a binding takes one curve (a float property), or for a Transform the curves of its attribute, 1 position (3),
7//! 2 rotation, a quaternion (4), 3 scale (3), 4 Euler angles (3). A binding names its target by `path`, the CRC-32 of the
8//! path of GameObject names below the Animator's own (the Animator's GameObject itself is the empty path, hash 0).
9//!
10//! The streamed block is a sequence of frames, each `time: f32`, `count: i32` and `count` keys of `index: i32` and four
11//! `f32` coefficients; a key opens a cubic segment of its curve that lasts until the curve's next key:
12//! `value(dt) = ((c0 dt + c1) dt + c2) dt + c3` for `dt` seconds after the key, so `c3` is the key's value and `c2` its
13//! outgoing slope; the incoming slope of the next key is the derivative at the segment's end. The first frame is at
14//! `-f32::MAX` and gives every curve's initial value; the last is at `+inf` with no keys. A segment whose coefficients are
15//! not finite is a step.
16
17use crate::player::{ObjRef, Player};
18use crate::value::{Array, Value};
19use crate::{Error, Result, invalid};
20
21/// One curve binding of a clip (`genericBindings`).
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct GenericBinding {
24    /// The CRC-32 of the target's path below the Animator's GameObject.
25    pub path: u32,
26    /// The attribute: for a Transform (`type_id` 4) 1 position, 2 rotation, 3 scale, 4 Euler angles; else the
27    /// property's hash.
28    pub attribute: u32,
29    /// The target's class id (4 Transform, 95 Animator, 137 SkinnedMeshRenderer, ...).
30    pub type_id: i64,
31    /// Whether the curve drives an object reference (`isPPtrCurve`); such clips are refused by [`ClipData::read`].
32    pub is_pptr: bool,
33}
34
35impl GenericBinding {
36    /// How many curves this binding takes.
37    pub fn width(&self) -> usize {
38        match (self.type_id, self.attribute) {
39            (4, 1 | 3 | 4) => 3,
40            (4, 2) => 4,
41            _ => 1,
42        }
43    }
44}
45
46/// Where a curve was stored.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Storage {
49    /// The streamed block (`m_StreamedClip`).
50    Streamed,
51    /// The dense block (`m_DenseClip`).
52    Dense,
53    /// The constant block (`m_ConstantClip`).
54    Constant,
55}
56
57/// A key of a curve: its time (seconds), value and the slopes (per second) into and out of it. An infinite outgoing
58/// slope is a step to the next key.
59#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct Key {
61    /// Seconds from the clip's start.
62    pub time: f32,
63    /// The curve's value at `time`.
64    pub value: f32,
65    /// The slope arriving at the key, per second.
66    pub in_slope: f32,
67    /// The slope leaving the key, per second; infinite for a step.
68    pub out_slope: f32,
69}
70
71/// One scalar curve: keys in increasing time, evaluated as a cubic Hermite spline between them (a dense curve's slopes
72/// are its secants, so it is piecewise linear).
73#[derive(Clone, Debug, PartialEq)]
74pub struct Curve {
75    /// Which block the curve was stored in.
76    pub storage: Storage,
77    /// The keys, in increasing time.
78    pub keys: Vec<Key>,
79}
80
81impl Curve {
82    /// The value at `t` and the slopes into and out of it: a key's own at its time, a segment's derivative inside it,
83    /// the end values held with no slope outside. A step segment holds its key's value. A curve without keys is 0; a
84    /// `t` that is NaN has the value NaN.
85    pub fn sample(&self, t: f32) -> (f32, f32, f32) {
86        let k = &self.keys;
87        let (Some(first), Some(last)) = (k.first(), k.last()) else {
88            return (0.0, 0.0, 0.0);
89        };
90        if t.is_nan() {
91            return (f32::NAN, 0.0, 0.0);
92        }
93        if t <= first.time {
94            return (
95                first.value,
96                0.0,
97                if t == first.time { first.out_slope } else { 0.0 },
98            );
99        }
100        if t >= last.time {
101            return (last.value, if t == last.time { last.in_slope } else { 0.0 }, 0.0);
102        }
103        // the first key after `t`, and the one before it; `t` is strictly between the first and last keys
104        let after = k.partition_point(|key| key.time <= t);
105        let (Some(a), Some(b)) = (after.checked_sub(1).and_then(|i| k.get(i)), k.get(after)) else {
106            return (last.value, 0.0, 0.0);
107        };
108        if t == a.time {
109            return (a.value, a.in_slope, a.out_slope);
110        }
111        if !a.out_slope.is_finite() {
112            return (a.value, 0.0, 0.0);
113        }
114        let (dt, s) = (
115            f64::from(b.time - a.time),
116            f64::from(t - a.time) / f64::from(b.time - a.time),
117        );
118        let (p0, p1) = (f64::from(a.value), f64::from(b.value));
119        let (m0, m1) = (f64::from(a.out_slope) * dt, f64::from(b.in_slope) * dt);
120        let (s2, s3) = (s * s, s * s * s);
121        let value = (2.0 * s3 - 3.0 * s2 + 1.0) * p0
122            + (s3 - 2.0 * s2 + s) * m0
123            + (-2.0 * s3 + 3.0 * s2) * p1
124            + (s3 - s2) * m1;
125        let slope = ((6.0 * s2 - 6.0 * s) * p0
126            + (3.0 * s2 - 4.0 * s + 1.0) * m0
127            + (-6.0 * s2 + 6.0 * s) * p1
128            + (3.0 * s2 - 2.0 * s) * m1)
129            / dt;
130        (crate::narrow(value), crate::narrow(slope), crate::narrow(slope))
131    }
132
133    /// Whether any slope is a step.
134    pub fn has_steps(&self) -> bool {
135        self.keys.iter().any(|k| !k.out_slope.is_finite())
136    }
137
138    /// Whether the curve has a single key.
139    pub fn is_constant(&self) -> bool {
140        self.keys.len() == 1
141    }
142}
143
144/// A binding with the curves it takes, in the order of its attribute's components.
145#[derive(Clone, Debug, PartialEq)]
146pub struct Track {
147    /// What the curves drive.
148    pub binding: GenericBinding,
149    /// [`GenericBinding::width`] curves.
150    pub curves: Vec<Curve>,
151}
152
153/// An AnimationClip's curves as stored.
154#[derive(Clone, Debug, PartialEq)]
155pub struct ClipData {
156    /// `m_Name`.
157    pub name: String,
158    /// Seconds, `m_StopTime - m_StartTime`.
159    pub duration: f32,
160    /// `m_LoopTime`.
161    pub looping: bool,
162    /// The bindings with their curves, in binding order.
163    pub tracks: Vec<Track>,
164    /// Animation events (`m_Events`): not read.
165    pub events: usize,
166    /// How far a streamed segment's end was from the next key's value (relative), the worst over the segments of a
167    /// millisecond or more: the decode's own consistency (the segment is the cubic the next key continues).
168    pub worst_continuity_error: f32,
169}
170
171/// Words a key takes in the streamed block: its curve index and four coefficients.
172const KEY_WORDS: usize = 5;
173
174fn int(v: &Value, k: &str) -> Result<i64> {
175    v.get(k)
176        .and_then(Value::as_i64)
177        .ok_or_else(|| invalid(format!("clip: no integer {k}")))
178}
179
180fn float(v: &Value, k: &str) -> Result<f32> {
181    v.get(k)
182        .and_then(Value::as_f32)
183        .ok_or_else(|| invalid(format!("clip: no float {k}")))
184}
185
186fn list<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
187    match v.get(k) {
188        Some(Value::Array(Array::Values(items))) => Ok(items),
189        Some(Value::Array(a)) if a.is_empty() => Ok(&[]),
190        _ => Err(invalid(format!("clip: no list {k}"))),
191    }
192}
193
194fn floats(v: &Value, k: &str) -> Result<Vec<f32>> {
195    match v.get(k) {
196        Some(Value::Array(Array::F32(f))) => Ok(f.clone()),
197        Some(Value::Array(a)) if a.is_empty() => Ok(Vec::new()),
198        _ => Err(invalid(format!("clip: no float list {k}"))),
199    }
200}
201
202/// The streamed block's curves: `curve_count` curves from the frames in `words`. Every curve stores at least its
203/// initial key, five words, so a count the data cannot hold is refused before anything is allocated for it.
204fn read_streamed(name: &str, words: &[u32], curve_count: usize) -> Result<(Vec<Curve>, f32)> {
205    let bad = |why: String| invalid(format!("{name}: streamed clip: {why}"));
206    let most = words.len() / KEY_WORDS;
207    if curve_count > most {
208        return Err(bad(format!(
209            "{curve_count} curves, but {} bytes of frames hold at most {most} keys, and every curve has one",
210            words.len().saturating_mul(4)
211        )));
212    }
213    let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
214    // each curve's entries: the time and the four coefficients of the cubic that starts there
215    let mut entries: Vec<Vec<(f32, [f32; 4])>> = vec![Vec::new(); curve_count];
216    let mut at = 0;
217    let take = |at: &mut usize, n: usize| -> Result<&[u8]> {
218        let end = at.checked_add(n);
219        let part = end.and_then(|end| bytes.get(*at..end)).ok_or_else(|| {
220            bad(format!(
221                "frames past the data at byte {at} of {}",
222                bytes.len(),
223                at = *at
224            ))
225        })?;
226        *at = end.unwrap_or(*at);
227        Ok(part)
228    };
229    let mut last_time = f32::NEG_INFINITY;
230    let mut ended = false;
231    while at < bytes.len() {
232        let frame_at = at;
233        let time = f32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
234        let count = i32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
235        if time.is_nan() || time < last_time {
236            return Err(bad(format!(
237                "frame time {time} after {last_time} (the frame at byte {frame_at}; times must not decrease)"
238            )));
239        }
240        last_time = time;
241        let count = usize::try_from(count).map_err(|_| {
242            bad(format!(
243                "{count} keys in the frame at byte {frame_at} (at least 0)"
244            ))
245        })?;
246        for _ in 0..count {
247            let key_at = at;
248            let index = i32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
249            let mut c = [0.0f32; 4];
250            for x in &mut c {
251                *x = f32::from_le_bytes(take(&mut at, 4)?.try_into().expect("4 bytes"));
252            }
253            let slot = usize::try_from(index).ok().ok_or_else(|| {
254                bad(format!(
255                    "a key of curve {index} of {curve_count} at byte {key_at}"
256                ))
257            })?;
258            entries
259                .get_mut(slot)
260                // frames that end exactly where the data does but name a curve the count does not include (PEAK's
261                // 6000.3.15f1 clips have them, in the first frame and later) are a layout this reader does not know,
262                // not damaged bytes
263                .ok_or_else(|| {
264                    Error::Unsupported(format!(
265                        "{name}: streamed clip: a key of curve {index} of {curve_count} at byte {key_at}, a curve \
266                         past the stated count"
267                    ))
268                })?
269                .push((time, c));
270        }
271        if time == f32::INFINITY {
272            ended = true;
273            break;
274        }
275    }
276    if !ended || at != bytes.len() {
277        return Err(bad(format!(
278            "the frames do not end with the +inf frame at the end of the data (frames end at byte {at} of {})",
279            bytes.len()
280        )));
281    }
282    let mut worst = 0.0f32;
283    let mut out = Vec::with_capacity(curve_count);
284    for (i, list) in entries.iter().enumerate() {
285        // the initial entry (at -f32::MAX) is the value before the first key, which the first key repeats
286        let real: Vec<&(f32, [f32; 4])> = list
287            .iter()
288            .filter(|(t, _)| *t > f32::MIN / 2.0 && t.is_finite())
289            .collect();
290        let mut keys: Vec<Key> = Vec::new();
291        if real.is_empty() {
292            let (_, c) = list.first().ok_or_else(|| bad(format!("curve {i} has no key")))?;
293            keys.push(Key {
294                time: 0.0,
295                value: c[3],
296                in_slope: 0.0,
297                out_slope: 0.0,
298            });
299        }
300        let mut previous: Option<(f32, [f32; 4])> = None;
301        for &&(time, c) in &real {
302            let in_slope = match previous {
303                Some((t0, p)) => {
304                    let dt = time - t0;
305                    // the cubic of the previous key at this key; a segment under a millisecond (a step made of two
306                    // keys) is not checked, its large coefficients cancel past f32 (17 of 135,385 in SkateBIRD's clips)
307                    let end = ((p[0] * dt + p[1]) * dt + p[2]) * dt + p[3];
308                    if dt >= 1e-3 && end.is_finite() && c[3].is_finite() {
309                        worst = worst.max((end - c[3]).abs() / (1.0 + c[3].abs()));
310                    }
311                    (3.0 * p[0] * dt + 2.0 * p[1]) * dt + p[2]
312                }
313                None => 0.0,
314            };
315            keys.push(Key {
316                time,
317                value: c[3],
318                in_slope,
319                out_slope: c[2],
320            });
321            previous = Some((time, c));
322        }
323        // a key whose segment is a step has non-finite coefficients: its slopes are a step
324        for k in &mut keys {
325            if !k.in_slope.is_finite() {
326                k.in_slope = f32::INFINITY;
327            }
328        }
329        out.push(Curve {
330            storage: Storage::Streamed,
331            keys,
332        });
333    }
334    Ok((out, worst))
335}
336
337/// The dense block's curves (`m_DenseClip`): frame `f` of curve `c` is `m_SampleArray[f * curves + c]`, at
338/// `m_BeginTime + f / m_SampleRate`. The sample array must hold exactly `frames * curves` values, and a block that
339/// states curves needs frames: a count nothing stored backs is refused before a curve is made for it.
340fn read_dense(name: &str, dense: &Value) -> Result<Vec<Curve>> {
341    let bad = |why: String| invalid(format!("{name}: dense clip: {why}"));
342    let dense_curves =
343        usize::try_from(int(dense, "m_CurveCount")?).map_err(|_| bad("a negative curve count".into()))?;
344    let frames =
345        usize::try_from(int(dense, "m_FrameCount")?).map_err(|_| bad("a negative frame count".into()))?;
346    let samples = floats(dense, "m_SampleArray")?;
347    if frames.checked_mul(dense_curves) != Some(samples.len()) {
348        return Err(bad(format!(
349            "{} samples for {frames} frames of {dense_curves} curves",
350            samples.len()
351        )));
352    }
353    if frames == 0 && dense_curves > 0 {
354        return Err(bad(format!("{dense_curves} curves of no frames")));
355    }
356    let (rate, begin) = (float(dense, "m_SampleRate")?, float(dense, "m_BeginTime")?);
357    if frames > 1 && !(rate.is_finite() && rate > 0.0) {
358        return Err(bad(format!(
359            "sample rate {rate} for {frames} frames (a positive rate is needed)"
360        )));
361    }
362    let times: Vec<f32> = (0..frames).map(|f| begin + f as f32 / rate).collect();
363    let mut curves = Vec::with_capacity(dense_curves);
364    for c in 0..dense_curves {
365        let values: Vec<f32> = samples.iter().skip(c).step_by(dense_curves).copied().collect();
366        let slope = |a: usize, b: usize| match (values.get(a..=b), times.get(a..=b)) {
367            (Some([v0, .., v1]), Some([t0, .., t1])) => (v1 - v0) / (t1 - t0),
368            _ => 0.0,
369        };
370        curves.push(Curve {
371            storage: Storage::Dense,
372            keys: values
373                .iter()
374                .zip(&times)
375                .enumerate()
376                .map(|(f, (&value, &time))| Key {
377                    time,
378                    value,
379                    in_slope: f.checked_sub(1).map_or(0.0, |p| slope(p, f)),
380                    out_slope: if f < frames.saturating_sub(1) {
381                        slope(f, f.saturating_add(1))
382                    } else {
383                        0.0
384                    },
385                })
386                .collect(),
387        });
388    }
389    Ok(curves)
390}
391
392impl ClipData {
393    /// The clip object `r`. Legacy clips (`m_Legacy`, curves stored per kind) are not read.
394    pub fn read(player: &Player, r: &ObjRef) -> Result<ClipData> {
395        let c = player.decode(r)?;
396        let name = c.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned();
397        if c.get("m_Legacy")
398            .is_some_and(|l| matches!(l, Value::Bool(true)) || l.as_i64().is_some_and(|v| v != 0))
399        {
400            return Err(Error::Unsupported(format!("{name}: legacy animation clip")));
401        }
402        let muscle = c
403            .get("m_MuscleClip")
404            .ok_or_else(|| invalid(format!("{name}: no m_MuscleClip")))?;
405        let data = muscle
406            .at(&["m_Clip", "data"])
407            .ok_or_else(|| invalid(format!("{name}: no m_MuscleClip.m_Clip.data")))?;
408        let streamed = data
409            .get("m_StreamedClip")
410            .ok_or_else(|| invalid(format!("{name}: no m_StreamedClip")))?;
411        let dense = data
412            .get("m_DenseClip")
413            .ok_or_else(|| invalid(format!("{name}: no m_DenseClip")))?;
414        let constant = data
415            .get("m_ConstantClip")
416            .ok_or_else(|| invalid(format!("{name}: no m_ConstantClip")))?;
417        let start = float(muscle, "m_StartTime")?;
418        let stop = float(muscle, "m_StopTime")?;
419        if start != 0.0 {
420            return Err(Error::Unsupported(format!(
421                "{name}: a clip starting at {start} s"
422            )));
423        }
424        let streamed_words = match streamed.get("data") {
425            Some(Value::Array(Array::U32(w))) => w.as_slice(),
426            Some(Value::Array(a)) if a.is_empty() => &[],
427            _ => return Err(invalid(format!("{name}: streamed clip data is not 32-bit words"))),
428        };
429        let streamed_count = usize::try_from(int(streamed, "curveCount")?)
430            .map_err(|_| invalid(format!("{name}: negative curve count")))?;
431        let (mut curves, worst) = if streamed_count == 0 && streamed_words.is_empty() {
432            (Vec::new(), 0.0)
433        } else {
434            read_streamed(&name, streamed_words, streamed_count)?
435        };
436        curves.extend(read_dense(&name, dense)?);
437        for v in floats(constant, "data")? {
438            curves.push(Curve {
439                storage: Storage::Constant,
440                keys: vec![Key {
441                    time: 0.0,
442                    value: v,
443                    in_slope: 0.0,
444                    out_slope: 0.0,
445                }],
446            });
447        }
448        // the bindings take the curves in order
449        let mut bindings = Vec::new();
450        let constant_bindings = c
451            .get("m_ClipBindingConstant")
452            .ok_or_else(|| invalid(format!("{name}: no m_ClipBindingConstant")))?;
453        for b in list(constant_bindings, "genericBindings")? {
454            bindings.push(GenericBinding {
455                path: u32::try_from(int(b, "path")?)
456                    .map_err(|_| invalid(format!("{name}: a path hash beyond 32 bits")))?,
457                attribute: u32::try_from(int(b, "attribute")?)
458                    .map_err(|_| invalid(format!("{name}: an attribute beyond 32 bits")))?,
459                type_id: int(b, "typeID")?,
460                is_pptr: int(b, "isPPtrCurve")? != 0,
461            });
462        }
463        if let Some(b) = bindings.iter().find(|b| b.is_pptr) {
464            return Err(Error::Unsupported(format!(
465                "{name}: a PPtr curve (attribute {})",
466                b.attribute
467            )));
468        }
469        let wanted: usize = bindings.iter().map(GenericBinding::width).sum();
470        if wanted != curves.len() {
471            return Err(invalid(format!(
472                "{name}: the bindings take {wanted} curves, the clip stores {}",
473                curves.len()
474            )));
475        }
476        let mut rest = curves.into_iter();
477        let tracks = bindings
478            .into_iter()
479            .map(|binding| {
480                let curves = rest.by_ref().take(binding.width()).collect();
481                Track { binding, curves }
482            })
483            .collect();
484        let looping = muscle
485            .get("m_LoopTime")
486            .is_some_and(|l| matches!(l, Value::Bool(true)) || l.as_i64().is_some_and(|v| v != 0));
487        Ok(ClipData {
488            name,
489            duration: stop - start,
490            looping,
491            tracks,
492            events: list(&c, "m_Events")?.len(),
493            worst_continuity_error: worst,
494        })
495    }
496}
497
498/// The hash a clip's binding names a path by: the CRC-32 (the zlib one) of the path of GameObject names below the
499/// Animator's GameObject, empty for the GameObject itself.
500pub fn path_hash(path: &str) -> u32 {
501    crate::animator::name_hash(path)
502}
503
504/// The clips an AnimatorController lists (`m_AnimationClips`), in order, null entries left out.
505pub fn controller_clips(player: &Player, controller: &ObjRef) -> Result<Vec<ObjRef>> {
506    let c = player.decode(controller)?;
507    let mut out = Vec::new();
508    for p in list(&c, "m_AnimationClips")? {
509        if let Some(clip) = player.resolve(&controller.file, p)? {
510            out.push(clip);
511        }
512    }
513    Ok(out)
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    fn key(time: f32, value: f32, in_slope: f32, out_slope: f32) -> Key {
521        Key {
522            time,
523            value,
524            in_slope,
525            out_slope,
526        }
527    }
528
529    fn frame(time: f32, keys: &[(i32, [f32; 4])]) -> Vec<u32> {
530        let mut words = vec![time.to_bits(), keys.len() as u32];
531        for (index, c) in keys {
532            words.push(*index as u32);
533            words.extend(c.iter().map(|x| x.to_bits()));
534        }
535        words
536    }
537
538    /// The initial frame, two keys of one curve and the end frame: the keys' values, outgoing slopes, the incoming
539    /// slope of the second as the first cubic's derivative at its end, and the continuity of the decode.
540    #[test]
541    fn a_streamed_curve_decodes_to_keys_with_slopes() {
542        // a cubic from 1 at t=0 over 2 s: v(dt) = 0.5 dt^3 - dt^2 + 3 dt + 1; its end value 1+6-4+4=7... at dt = 2
543        let (c0, c1, c2, c3) = (0.5f32, -1.0, 3.0, 1.0);
544        let end = ((c0 * 2.0 + c1) * 2.0 + c2) * 2.0 + c3;
545        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 1.0])]);
546        words.extend(frame(0.0, &[(0, [c0, c1, c2, c3])]));
547        words.extend(frame(2.0, &[(0, [0.0, 0.0, 0.5, end])]));
548        words.extend(frame(f32::INFINITY, &[]));
549        let (curves, worst) = read_streamed("t", &words, 1).unwrap();
550        assert_eq!(worst, 0.0);
551        let k = &curves[0].keys;
552        assert_eq!(k.len(), 2);
553        assert_eq!((k[0].time, k[0].value, k[0].out_slope), (0.0, 1.0, 3.0));
554        // the derivative at the end of the first segment: 3 c0 dt^2 + 2 c1 dt + c2 = 6 - 4 + 3
555        assert_eq!(
556            (k[1].time, k[1].value, k[1].in_slope, k[1].out_slope),
557            (2.0, end, 5.0, 0.5)
558        );
559        // the Hermite spline through these keys is the stored cubic
560        let (v, s, _) = curves[0].sample(1.0);
561        assert!((v - (0.5 - 1.0 + 3.0 + 1.0)).abs() < 1e-5, "{v}");
562        assert!((s - (1.5 - 2.0 + 3.0)).abs() < 1e-5, "{s}");
563        assert_eq!(curves[0].sample(0.0), (1.0, 0.0, 3.0));
564        assert_eq!(curves[0].sample(2.0), (end, 5.0, 0.0));
565    }
566
567    /// A curve with only its initial entry is constant; frames that do not end at +inf, a key of a curve beyond the
568    /// count and times going back are refused.
569    #[test]
570    fn streamed_clips_refuse_what_is_not_the_format() {
571        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 4.0])]);
572        words.extend(frame(f32::INFINITY, &[]));
573        let (curves, _) = read_streamed("t", &words, 1).unwrap();
574        assert_eq!(curves[0].keys, vec![key(0.0, 4.0, 0.0, 0.0)]);
575        let refused = |words: &[u32], n: usize| read_streamed("t", words, n).unwrap_err().to_string();
576        assert!(refused(&words[..words.len() - 2], 1).contains("do not end"));
577        assert!(refused(&words, 0).contains("curve 0 of 0"));
578        let mut back = frame(1.0, &[]);
579        back.extend(frame(0.5, &[]));
580        back.extend(frame(f32::INFINITY, &[]));
581        assert!(refused(&back, 1).contains("after"));
582    }
583
584    /// A curve count nothing in the data backs is refused before a vector is made for it, with the offset of the bad
585    /// bytes where there is one.
586    #[test]
587    fn a_streamed_curve_count_is_bounded_by_the_data() {
588        let mut words = frame(-f32::MAX, &[(0, [0.0, 0.0, 0.0, 4.0])]);
589        words.extend(frame(f32::INFINITY, &[]));
590        for n in [2, 1 << 40, usize::MAX] {
591            let e = read_streamed("t", &words, n).unwrap_err().to_string();
592            assert!(e.contains("hold at most 1 keys"), "{e}");
593        }
594        // a key naming curve 5 of 1, at the byte the key starts (the frame header takes 8)
595        let mut words = frame(-f32::MAX, &[(5, [0.0; 4])]);
596        words.extend(frame(f32::INFINITY, &[]));
597        let e = read_streamed("t", &words, 1).unwrap_err();
598        // well-formed frames naming a curve past the count are a layout not read; a negative index is damaged bytes
599        assert!(matches!(e, Error::Unsupported(_)), "{e}");
600        assert!(e.to_string().contains("curve 5 of 1 at byte 8"), "{e}");
601        let mut negative = frame(-f32::MAX, &[(-1, [0.0; 4])]);
602        negative.extend(frame(f32::INFINITY, &[]));
603        let e = read_streamed("t", &negative, 1).unwrap_err();
604        assert!(matches!(e, Error::Invalid(_)), "{e}");
605        let mut back = frame(1.0, &[]);
606        back.extend(frame(0.5, &[]));
607        let e = read_streamed("t", &back, 0).unwrap_err().to_string();
608        assert!(e.contains("byte 8"), "{e}");
609    }
610
611    fn dense(curves: i64, frames: i64, samples: Vec<f32>) -> Value {
612        let f = |k: &str, v: Value| (std::sync::Arc::<str>::from(k), v);
613        Value::Struct(vec![
614            f("m_CurveCount", Value::Int(curves)),
615            f("m_FrameCount", Value::Int(frames)),
616            f("m_SampleRate", Value::Float(2.0)),
617            f("m_BeginTime", Value::Float(0.0)),
618            f("m_SampleArray", Value::Array(Array::F32(samples))),
619        ])
620    }
621
622    /// The dense block's frame and curve counts are multiplied without overflow and must be backed by samples: a huge
623    /// count with no samples makes no curves.
624    #[test]
625    fn dense_counts_are_checked_against_the_samples() {
626        for (curves, frames) in [(i64::MAX, 3), (3, i64::MAX), (i64::MAX, i64::MAX), (1 << 40, 0)] {
627            let e = read_dense("t", &dense(curves, frames, vec![]))
628                .unwrap_err()
629                .to_string();
630            assert!(e.contains("dense clip"), "{e}");
631        }
632        let curves = read_dense("t", &dense(2, 2, vec![0.0, 10.0, 1.0, 20.0])).unwrap();
633        assert_eq!(curves.len(), 2);
634        assert_eq!(
635            curves[1].keys,
636            vec![key(0.0, 10.0, 0.0, 20.0), key(0.5, 20.0, 20.0, 0.0)]
637        );
638        assert_eq!(read_dense("t", &dense(0, 0, vec![])).unwrap(), vec![]);
639    }
640
641    /// A time that is not a number samples to NaN instead of indexing before the first key.
642    #[test]
643    fn sampling_at_nan_does_not_panic() {
644        let curve = Curve {
645            storage: Storage::Constant,
646            keys: vec![key(0.0, 1.0, 0.0, 0.0), key(1.0, 2.0, 0.0, 0.0)],
647        };
648        assert!(curve.sample(f32::NAN).0.is_nan());
649    }
650
651    /// A Hermite curve samples to its keys, and a transform track of three curves with different key times becomes
652    /// one Hermite channel at the union of the times whose pieces are the same cubics; constants give one key, dense
653    /// curves a Linear channel.
654    #[test]
655    fn path_hashes_are_the_crc32_of_the_path() {
656        assert_eq!(path_hash(""), 0);
657        assert_eq!(path_hash("scarf"), 2_702_068_705);
658        assert_eq!(path_hash("root/Lchain1/Lchain2/Lchain3"), 3_634_936_137);
659        assert_eq!(path_hash("root/Rchain1/Rchain2"), 356_844_446);
660    }
661}