Skip to main content

mp4_atom/meta/ilst/
data.rs

1use crate::*;
2
3pub(crate) const DATA_4CC: FourCC = FourCC::new(b"data");
4
5// Well-known `data` atom type indicators.
6// See Apple's [well-known types](https://developer.apple.com/documentation/quicktime-file-format/well-known_types) table.
7const TYPE_INDICATOR_RESERVED: u32 = 0u32;
8const TYPE_INDICATOR_UTF8: u32 = 1u32;
9const TYPE_INDICATOR_UTF16: u32 = 2u32;
10const TYPE_INDICATOR_JPEG: u32 = 13u32;
11const TYPE_INDICATOR_PNG: u32 = 14u32;
12// Per the well-known types table, 21/22 only cover 1-4 byte integers; the
13// fixed-width indicators below (65-67/75-77) are the 1/2/4-byte equivalents,
14// and 74/78 are the *only* valid indicators for an 8-byte integer.
15const TYPE_INDICATOR_BE_SIGNED_INT: u32 = 21u32;
16const TYPE_INDICATOR_BE_UNSIGNED_INT: u32 = 22u32;
17const TYPE_INDICATOR_BE_FLOAT32: u32 = 23u32;
18const TYPE_INDICATOR_BE_FLOAT64: u32 = 24u32;
19const TYPE_INDICATOR_BMP: u32 = 27u32;
20// Fixed-width signed/unsigned integer type indicators, as used by mp4v2 and
21// AtomicParsley alongside the variable-length pair above (21/22). Only used
22// on decode for widths 1/2/4 bytes — `IlstDataValue::to_raw` canonicalizes to
23// 21/22 for those. 74/78 (the 8-byte entries) are also `to_raw`'s encode
24// target for values too large for 21/22. Each entry's declared width (1, 2,
25// 4, 8 bytes, matching array position) is exact: a payload of any other
26// length is malformed.
27const TYPE_INDICATOR_SIGNED_INT_FIXED: [u32; 4] = [65, 66, 67, 74];
28const TYPE_INDICATOR_UNSIGNED_INT_FIXED: [u32; 4] = [75, 76, 77, 78];
29const FIXED_INT_WIDTHS: [usize; 4] = [1, 2, 4, 8];
30const TYPE_INDICATOR_BE_SIGNED_INT64: u32 = TYPE_INDICATOR_SIGNED_INT_FIXED[3];
31const TYPE_INDICATOR_BE_UNSIGNED_INT64: u32 = TYPE_INDICATOR_UNSIGNED_INT_FIXED[3];
32
33/// A [`IlstData`] value, interpreted according to its wire-level type indicator.
34///
35/// [`IlstDataValue::Binary`] is the explicit "no type" reserved indicator (`0`).
36/// [`IlstDataValue::Unknown`] covers everything else this crate can't interpret:
37/// a type indicator it doesn't recognize, or one it does recognize but whose
38/// raw bytes don't match (e.g. a `BeFloat32` value that isn't exactly 4
39/// bytes) — it carries the original type indicator so callers can still
40/// inspect it, and round-trips it unchanged on encode.
41#[derive(Debug, Clone, PartialEq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum IlstDataValue {
44    Reserved(Vec<u8>),
45    Utf8(String),
46    Utf16(String),
47    Jpeg(Vec<u8>),
48    Png(Vec<u8>),
49    Bmp(Vec<u8>),
50    BeSignedInt(i64),
51    BeUnsignedInt(u64),
52    BeFloat32(f32),
53    BeFloat64(f64),
54    Unknown(u32, Vec<u8>),
55}
56
57impl IlstDataValue {
58    fn from_raw(type_indicator: u32, value: &[u8]) -> Self {
59        match type_indicator {
60            TYPE_INDICATOR_RESERVED => IlstDataValue::Reserved(value.to_vec()),
61            TYPE_INDICATOR_UTF8 => match std::str::from_utf8(value) {
62                Ok(text) => IlstDataValue::Utf8(text.to_string()),
63                Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()),
64            },
65            TYPE_INDICATOR_UTF16 => match decode_utf16_be(value) {
66                Some(text) => IlstDataValue::Utf16(text),
67                None => IlstDataValue::Unknown(type_indicator, value.to_vec()),
68            },
69            TYPE_INDICATOR_JPEG => IlstDataValue::Jpeg(value.to_vec()),
70            TYPE_INDICATOR_PNG => IlstDataValue::Png(value.to_vec()),
71            TYPE_INDICATOR_BMP => IlstDataValue::Bmp(value.to_vec()),
72            // 21/22 only cover 1-4 byte widths (an 8-byte value must use the
73            // fixed 74/78 indicators instead), so reject that width here even
74            // though `decode_be_signed_int`/`decode_be_unsigned_int` accept it.
75            TYPE_INDICATOR_BE_SIGNED_INT if value.len() <= 4 => match decode_be_signed_int(value) {
76                Some(v) => IlstDataValue::BeSignedInt(v),
77                None => IlstDataValue::Unknown(type_indicator, value.to_vec()),
78            },
79            TYPE_INDICATOR_BE_UNSIGNED_INT if value.len() <= 4 => {
80                match decode_be_unsigned_int(value) {
81                    Some(v) => IlstDataValue::BeUnsignedInt(v),
82                    None => IlstDataValue::Unknown(type_indicator, value.to_vec()),
83                }
84            }
85            t if TYPE_INDICATOR_SIGNED_INT_FIXED.contains(&t) => {
86                let width = FIXED_INT_WIDTHS[TYPE_INDICATOR_SIGNED_INT_FIXED
87                    .iter()
88                    .position(|&x| x == t)
89                    .unwrap()];
90                match (value.len() == width, decode_be_signed_int(value)) {
91                    (true, Some(v)) => IlstDataValue::BeSignedInt(v),
92                    _ => IlstDataValue::Unknown(type_indicator, value.to_vec()),
93                }
94            }
95            t if TYPE_INDICATOR_UNSIGNED_INT_FIXED.contains(&t) => {
96                let width = FIXED_INT_WIDTHS[TYPE_INDICATOR_UNSIGNED_INT_FIXED
97                    .iter()
98                    .position(|&x| x == t)
99                    .unwrap()];
100                match (value.len() == width, decode_be_unsigned_int(value)) {
101                    (true, Some(v)) => IlstDataValue::BeUnsignedInt(v),
102                    _ => IlstDataValue::Unknown(type_indicator, value.to_vec()),
103                }
104            }
105            TYPE_INDICATOR_BE_FLOAT32 => match <[u8; 4]>::try_from(value) {
106                Ok(bytes) => IlstDataValue::BeFloat32(f32::from_be_bytes(bytes)),
107                Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()),
108            },
109            TYPE_INDICATOR_BE_FLOAT64 => match <[u8; 8]>::try_from(value) {
110                Ok(bytes) => IlstDataValue::BeFloat64(f64::from_be_bytes(bytes)),
111                Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()),
112            },
113            _ => IlstDataValue::Unknown(type_indicator, value.to_vec()),
114        }
115    }
116
117    fn to_raw(&self) -> (u32, Vec<u8>) {
118        match self {
119            IlstDataValue::Utf8(text) => (TYPE_INDICATOR_UTF8, text.clone().into_bytes()),
120            IlstDataValue::Utf16(text) => (TYPE_INDICATOR_UTF16, encode_utf16_be(text)),
121            IlstDataValue::Jpeg(bytes) => (TYPE_INDICATOR_JPEG, bytes.clone()),
122            IlstDataValue::Png(bytes) => (TYPE_INDICATOR_PNG, bytes.clone()),
123            IlstDataValue::Bmp(bytes) => (TYPE_INDICATOR_BMP, bytes.clone()),
124            IlstDataValue::BeSignedInt(v) => {
125                let bytes = encode_be_signed_int(*v);
126                // 21 only covers up to 4 bytes; an 8-byte encoding must use
127                // the fixed 74 (BE 64-bit Signed Integer) indicator instead.
128                let type_indicator = if bytes.len() > 4 {
129                    TYPE_INDICATOR_BE_SIGNED_INT64
130                } else {
131                    TYPE_INDICATOR_BE_SIGNED_INT
132                };
133                (type_indicator, bytes)
134            }
135            IlstDataValue::BeUnsignedInt(v) => {
136                let bytes = encode_be_unsigned_int(*v);
137                let type_indicator = if bytes.len() > 4 {
138                    TYPE_INDICATOR_BE_UNSIGNED_INT64
139                } else {
140                    TYPE_INDICATOR_BE_UNSIGNED_INT
141                };
142                (type_indicator, bytes)
143            }
144            IlstDataValue::BeFloat32(v) => (TYPE_INDICATOR_BE_FLOAT32, v.to_be_bytes().to_vec()),
145            IlstDataValue::BeFloat64(v) => (TYPE_INDICATOR_BE_FLOAT64, v.to_be_bytes().to_vec()),
146            IlstDataValue::Reserved(bytes) => (TYPE_INDICATOR_RESERVED, bytes.clone()),
147            IlstDataValue::Unknown(type_indicator, bytes) => (*type_indicator, bytes.clone()),
148        }
149    }
150}
151
152// Apple's well-known BE integer type indicators use a variable-length
153// encoding (1, 2, 3, 4, or 8 bytes); 3-byte values need sign/zero-extension.
154fn decode_be_signed_int(bytes: &[u8]) -> Option<i64> {
155    Some(match bytes.len() {
156        1 => i64::from(bytes[0] as i8),
157        2 => i64::from(i16::from_be_bytes(bytes.try_into().unwrap())),
158        3 => {
159            let mut widened = [0u8; 4];
160            widened[1..].copy_from_slice(bytes);
161            i64::from((i32::from_be_bytes(widened) << 8) >> 8)
162        }
163        4 => i64::from(i32::from_be_bytes(bytes.try_into().unwrap())),
164        8 => i64::from_be_bytes(bytes.try_into().unwrap()),
165        _ => return None,
166    })
167}
168
169fn decode_be_unsigned_int(bytes: &[u8]) -> Option<u64> {
170    Some(match bytes.len() {
171        1 => u64::from(bytes[0]),
172        2 => u64::from(u16::from_be_bytes(bytes.try_into().unwrap())),
173        3 => {
174            let mut widened = [0u8; 4];
175            widened[1..].copy_from_slice(bytes);
176            u64::from(u32::from_be_bytes(widened))
177        }
178        4 => u64::from(u32::from_be_bytes(bytes.try_into().unwrap())),
179        8 => u64::from_be_bytes(bytes.try_into().unwrap()),
180        _ => return None,
181    })
182}
183
184// Encode using the narrowest of the widths `decode_be_signed_int` accepts
185// (1/2/4/8 bytes; the 3-byte width is decode-only, for compatibility).
186fn encode_be_signed_int(v: i64) -> Vec<u8> {
187    if let Ok(v) = i8::try_from(v) {
188        v.to_be_bytes().to_vec()
189    } else if let Ok(v) = i16::try_from(v) {
190        v.to_be_bytes().to_vec()
191    } else if let Ok(v) = i32::try_from(v) {
192        v.to_be_bytes().to_vec()
193    } else {
194        v.to_be_bytes().to_vec()
195    }
196}
197
198fn encode_be_unsigned_int(v: u64) -> Vec<u8> {
199    if let Ok(v) = u8::try_from(v) {
200        v.to_be_bytes().to_vec()
201    } else if let Ok(v) = u16::try_from(v) {
202        v.to_be_bytes().to_vec()
203    } else if let Ok(v) = u32::try_from(v) {
204        v.to_be_bytes().to_vec()
205    } else {
206        v.to_be_bytes().to_vec()
207    }
208}
209
210fn decode_utf16_be(bytes: &[u8]) -> Option<String> {
211    if !bytes.len().is_multiple_of(2) {
212        return None;
213    }
214    let units: Vec<u16> = bytes
215        .as_chunks::<2>()
216        .0
217        .iter()
218        .map(|c| u16::from_be_bytes([c[0], c[1]]))
219        .collect();
220    String::from_utf16(&units).ok()
221}
222
223fn encode_utf16_be(text: &str) -> Vec<u8> {
224    text.encode_utf16().flat_map(u16::to_be_bytes).collect()
225}
226
227/// The content of an iTunes-style `data` atom.
228///
229/// Unlike the well-known `ilst` tags ([`Copyright`], [`Tool`], [`Desc`], ...),
230/// which are always UTF-8 text, Apple's `mdta`-keyed metadata items (see
231/// [`Keys`]) can hold any of the well-known `data` type indicators — `value`
232/// is a [`IlstDataValue`] that interprets the wire-level type indicator so
233/// callers don't have to.
234///
235/// Two encodings exist in the wild: the FFmpeg "short" style, where the value
236/// follows the item header directly, and the QuickTime/GPAC "long" style,
237/// where the value is wrapped in a nested `data` atom. Both decode into this
238/// same representation; encoding always emits the long style.
239#[derive(Debug, Clone, PartialEq)]
240#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
241pub struct IlstData {
242    pub country_indicator: u16,
243    pub language_indicator: u16,
244    pub value: IlstDataValue,
245}
246
247impl IlstData {
248    pub(crate) fn decode<B: Buf>(buf: &mut B) -> Result<Self> {
249        let type_indicator_or_len = u32::decode(buf)?;
250
251        let type_indicator = match type_indicator_or_len {
252            TYPE_INDICATOR_UTF8 => {
253                // Too short for a valid length, so probably
254                // UTF-8 text, FFmpeg short-style
255                type_indicator_or_len
256            }
257            _ => {
258                // Maybe Atom follows on straight away.
259                // Try parsing as Quicktime data atom: GPAC style or FFmpeg long style
260                let fourcc = FourCC::decode(buf)?;
261                if fourcc != DATA_4CC {
262                    return Err(Error::UnexpectedBox(fourcc));
263                }
264
265                u32::decode(buf)?
266            }
267        };
268
269        let country_indicator = u16::decode(buf)?;
270        let language_indicator = u16::decode(buf)?;
271
272        let size = buf.remaining();
273        let value = IlstDataValue::from_raw(type_indicator, buf.slice(size));
274        buf.advance(size);
275
276        Ok(Self {
277            country_indicator,
278            language_indicator,
279            value,
280        })
281    }
282
283    pub(crate) fn encode<B: BufMut>(&self, buf: &mut B) -> Result<()> {
284        let (type_indicator, raw) = self.value.to_raw();
285
286        // the length of the nested atom is the length field (4 bytes),
287        // the 4CC (4 bytes), the type indicator (4 bytes), the country
288        // indicator (2 bytes), the language indicator (2 bytes) and
289        // then the actual value.
290        let nested_len = (4 + 4 + 4 + 2 + 2 + raw.len()) as u32;
291        nested_len.encode(buf)?;
292        DATA_4CC.encode(buf)?;
293        type_indicator.encode(buf)?;
294        self.country_indicator.encode(buf)?;
295        self.language_indicator.encode(buf)?;
296        raw.as_slice().encode(buf)?;
297        Ok(())
298    }
299}
300
301/// A UTF-8 text payload of an iTunes-style `ilst` metadata item.
302pub(crate) struct DataText {
303    pub country_indicator: u16,
304    pub language_indicator: u16,
305    pub text: String,
306}
307
308pub(crate) fn decode_text<B: Buf>(buf: &mut B) -> Result<DataText> {
309    let data = IlstData::decode(buf)?;
310    match data.value {
311        IlstDataValue::Utf8(text) => Ok(DataText {
312            country_indicator: data.country_indicator,
313            language_indicator: data.language_indicator,
314            text,
315        }),
316        _ => Err(Error::Unsupported(
317            "Only UTF-8 text is supported in ilst data atoms",
318        )),
319    }
320}
321
322pub(crate) fn encode_text<B: BufMut>(
323    country_indicator: u16,
324    language_indicator: u16,
325    text: &str,
326    buf: &mut B,
327) -> Result<()> {
328    IlstData {
329        country_indicator,
330        language_indicator,
331        value: IlstDataValue::Utf8(text.to_string()),
332    }
333    .encode(buf)
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    // Long QuickTime/GPAC style: the value is wrapped in a nested `data` atom.
341    // This is the layout `encode_text` always emits.
342    const LONG_STYLE: &[u8] = &[
343        0x00, 0x00, 0x00, 0x1A, // data atom size = 26
344        b'd', b'a', b't', b'a', //
345        0x00, 0x00, 0x00, 0x01, // type indicator: UTF-8
346        0x00, 0x00, 0x00, 0x00, // country + language
347        b'(', b'c', b')', b' ', b'2', b'0', b'2', b'6', b' ', b'x',
348    ];
349
350    #[test]
351    fn test_decode_text_short_style() {
352        // FFmpeg short style: the leading `1` is the type indicator (too small
353        // to be a valid `data` atom length), so the UTF-8 value follows directly.
354        let buf = [
355            0x00, 0x00, 0x00, 0x01, // type indicator (short style): UTF-8
356            0x00, 0x00, 0x00, 0x00, // country + language
357            b'2', b'0', b'2', b'6',
358        ];
359        let decoded = decode_text(&mut &buf[..]).unwrap();
360        assert_eq!(decoded.country_indicator, 0);
361        assert_eq!(decoded.language_indicator, 0);
362        assert_eq!(decoded.text, "2026");
363    }
364
365    #[test]
366    fn test_decode_text_long_style() {
367        let decoded = decode_text(&mut &LONG_STYLE[..]).unwrap();
368        assert_eq!(decoded.country_indicator, 0);
369        assert_eq!(decoded.language_indicator, 0);
370        assert_eq!(decoded.text, "(c) 2026 x");
371    }
372
373    #[test]
374    fn test_encode_text_emits_long_style() {
375        // `encode_text` always emits the long `data`-wrapped layout, which
376        // decodes back to the same value.
377        let mut buf = Vec::new();
378        encode_text(0, 0, "(c) 2026 x", &mut buf).unwrap();
379        assert_eq!(buf, LONG_STYLE);
380
381        let decoded = decode_text(&mut buf.as_slice()).unwrap();
382        assert_eq!(decoded.text, "(c) 2026 x");
383    }
384
385    fn data_with(value: IlstDataValue) -> IlstData {
386        IlstData {
387            country_indicator: 0,
388            language_indicator: 0,
389            value,
390        }
391    }
392
393    #[test]
394    fn test_data_roundtrip_non_utf8_type() {
395        // `mdta`-keyed items can use type indicators other than UTF-8 text.
396        let data = data_with(IlstDataValue::BeSignedInt(42));
397
398        let mut buf = Vec::new();
399        data.encode(&mut buf).unwrap();
400
401        let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data");
402        assert_eq!(decoded, data);
403    }
404
405    #[test]
406    fn test_data_roundtrip_unknown_preserves_exact_bytes() {
407        // `Unknown` carries the original type indicator, so it round-trips
408        // byte-for-byte even though this crate doesn't otherwise recognize it.
409        let data = data_with(IlstDataValue::Unknown(999, vec![1, 2, 3]));
410
411        let mut buf = Vec::new();
412        data.encode(&mut buf).unwrap();
413
414        let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data");
415        assert_eq!(decoded, data);
416    }
417
418    #[test]
419    fn test_decode_value_utf8() {
420        let mut data_body = Vec::new();
421        data_body.extend_from_slice(&1u32.to_be_bytes());
422        data_body.extend_from_slice(&0u32.to_be_bytes());
423        data_body.extend_from_slice(b"hello");
424
425        let decoded = IlstData::decode(&mut data_body.as_slice()).unwrap();
426        assert_eq!(decoded.value, IlstDataValue::Utf8("hello".into()));
427    }
428
429    #[test]
430    fn test_decode_value_utf16() {
431        let text = "hi \u{263A}"; // includes a non-ASCII code point
432        assert_eq!(
433            decode_raw(2, &encode_utf16_be(text)),
434            IlstDataValue::Utf16(text.into())
435        );
436    }
437
438    #[test]
439    fn test_decode_value_jpeg_png_bmp() {
440        assert_eq!(
441            decode_raw(13, &[0xFF, 0xD8]),
442            IlstDataValue::Jpeg(vec![0xFF, 0xD8])
443        );
444        assert_eq!(
445            decode_raw(14, &[0x89, 0x50]),
446            IlstDataValue::Png(vec![0x89, 0x50])
447        );
448        assert_eq!(
449            decode_raw(27, &[0x42, 0x4D]),
450            IlstDataValue::Bmp(vec![0x42, 0x4D])
451        );
452    }
453
454    // Build the long-style nested `data` atom layout `IlstData::decode` expects
455    // for any type indicator other than the short-style `1`.
456    fn decode_raw(type_indicator: u32, value: &[u8]) -> IlstDataValue {
457        let mut data_body = Vec::new();
458        let nested_len = (4 + 4 + 4 + 2 + 2 + value.len()) as u32;
459        data_body.extend_from_slice(&nested_len.to_be_bytes());
460        data_body.extend_from_slice(b"data");
461        data_body.extend_from_slice(&type_indicator.to_be_bytes());
462        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
463        data_body.extend_from_slice(value);
464        IlstData::decode(&mut data_body.as_slice()).unwrap().value
465    }
466
467    #[test]
468    fn test_decode_value_be_signed_int_variable_length() {
469        assert_eq!(decode_raw(21, &[0xFF]), IlstDataValue::BeSignedInt(-1));
470        assert_eq!(
471            decode_raw(21, &1000i16.to_be_bytes()),
472            IlstDataValue::BeSignedInt(1000)
473        );
474        assert_eq!(
475            decode_raw(21, &(-1i32).to_be_bytes()[1..]),
476            IlstDataValue::BeSignedInt(-1)
477        );
478        assert_eq!(
479            decode_raw(21, &(-100000i32).to_be_bytes()),
480            IlstDataValue::BeSignedInt(-100000)
481        );
482    }
483
484    #[test]
485    fn test_decode_value_be_signed_int_rejects_eight_bytes() {
486        // Per the well-known types table, 21 only covers 1-4 byte integers;
487        // an 8-byte payload must use the fixed 74 indicator instead, so it's
488        // malformed (not silently accepted) under 21.
489        assert_eq!(
490            decode_raw(21, &(-1i64).to_be_bytes()),
491            IlstDataValue::Unknown(21, (-1i64).to_be_bytes().to_vec())
492        );
493    }
494
495    #[test]
496    fn test_decode_value_be_unsigned_int_variable_length() {
497        assert_eq!(decode_raw(22, &[200]), IlstDataValue::BeUnsignedInt(200));
498        assert_eq!(
499            decode_raw(22, &40000u32.to_be_bytes()),
500            IlstDataValue::BeUnsignedInt(40000)
501        );
502    }
503
504    #[test]
505    fn test_decode_value_be_unsigned_int_rejects_eight_bytes() {
506        assert_eq!(
507            decode_raw(22, &1u64.to_be_bytes()),
508            IlstDataValue::Unknown(22, 1u64.to_be_bytes().to_vec())
509        );
510    }
511
512    #[test]
513    fn test_encode_be_int_uses_64_bit_fixed_indicator_beyond_four_bytes() {
514        // Values that fit in i32/u32 canonicalize to the variable-length
515        // 21/22 pair; values that need the full 8 bytes must use the fixed
516        // 74/78 indicators instead (21/22 only cover up to 4 bytes).
517        assert_eq!(
518            data_with(IlstDataValue::BeSignedInt(42)).value.to_raw().0,
519            21
520        );
521        assert_eq!(
522            data_with(IlstDataValue::BeSignedInt(i64::from(i32::MAX) + 1))
523                .value
524                .to_raw()
525                .0,
526            74
527        );
528        assert_eq!(
529            data_with(IlstDataValue::BeUnsignedInt(42)).value.to_raw().0,
530            22
531        );
532        assert_eq!(
533            data_with(IlstDataValue::BeUnsignedInt(u64::from(u32::MAX) + 1))
534                .value
535                .to_raw()
536                .0,
537            78
538        );
539    }
540
541    #[test]
542    fn test_data_roundtrip_eight_byte_int() {
543        for value in [
544            IlstDataValue::BeSignedInt(i64::from(i32::MAX) + 1),
545            IlstDataValue::BeUnsignedInt(u64::from(u32::MAX) + 1),
546        ] {
547            let data = data_with(value.clone());
548
549            let mut buf = Vec::new();
550            data.encode(&mut buf).unwrap();
551
552            let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data");
553            assert_eq!(decoded.value, value);
554        }
555    }
556
557    #[test]
558    fn test_decode_value_fixed_width_ints() {
559        // mp4v2/AtomicParsley-style fixed-width type indicators, distinct
560        // from the variable-length 21/22 pair.
561        assert_eq!(
562            decode_raw(67, &42i32.to_be_bytes()),
563            IlstDataValue::BeSignedInt(42)
564        );
565        assert_eq!(
566            decode_raw(77, &42u32.to_be_bytes()),
567            IlstDataValue::BeUnsignedInt(42)
568        );
569    }
570
571    #[test]
572    fn test_decode_value_fixed_width_ints_rejects_mismatched_length() {
573        // Unlike the variable-length 21/22 pair, each fixed-width indicator's
574        // width is exact: 67 declares 4 bytes, so an 8-byte payload (which
575        // would be perfectly valid under 21) must not be silently accepted.
576        assert_eq!(
577            decode_raw(67, &42i64.to_be_bytes()),
578            IlstDataValue::Unknown(67, 42i64.to_be_bytes().to_vec())
579        );
580        assert_eq!(
581            decode_raw(65, &42i32.to_be_bytes()),
582            IlstDataValue::Unknown(65, 42i32.to_be_bytes().to_vec())
583        );
584        assert_eq!(
585            decode_raw(77, &42u64.to_be_bytes()),
586            IlstDataValue::Unknown(77, 42u64.to_be_bytes().to_vec())
587        );
588    }
589
590    #[test]
591    fn test_decode_value_be_float32_and_float64() {
592        assert_eq!(
593            decode_raw(23, &1.5f32.to_be_bytes()),
594            IlstDataValue::BeFloat32(1.5)
595        );
596        assert_eq!(
597            decode_raw(24, &1.5f64.to_be_bytes()),
598            IlstDataValue::BeFloat64(1.5)
599        );
600    }
601
602    #[test]
603    fn test_decode_value_binary_for_reserved_type() {
604        assert_eq!(
605            decode_raw(0, &[1, 2, 3]),
606            IlstDataValue::Reserved(vec![1, 2, 3])
607        );
608    }
609
610    #[test]
611    fn test_decode_value_unknown_for_unrecognized_or_malformed() {
612        // Unrecognized type indicator.
613        assert_eq!(
614            decode_raw(999, &[1, 2, 3]),
615            IlstDataValue::Unknown(999, vec![1, 2, 3])
616        );
617        // Wrong length for the declared type.
618        assert_eq!(
619            decode_raw(23, &[1, 2, 3]),
620            IlstDataValue::Unknown(23, vec![1, 2, 3])
621        );
622    }
623}