From 2f99fb14ed730513ee0de3ed2dccc9354d961329 Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Thu, 20 Aug 2026 16:00:43 +0200 Subject: [PATCH 1/4] meta: add support for keys element --- src/any.rs | 1 + src/meta/keys.rs | 101 +++++++++++++++++++++++++++++++++++++++++++++++ src/meta/mod.rs | 3 ++ 3 files changed, 105 insertions(+) create mode 100644 src/meta/keys.rs diff --git a/src/any.rs b/src/any.rs index 026958f..788b103 100644 --- a/src/any.rs +++ b/src/any.rs @@ -264,6 +264,7 @@ any! { Name, Rtng, Year, + Keys, Moov, Mvhd, Ainf, diff --git a/src/meta/keys.rs b/src/meta/keys.rs new file mode 100644 index 0000000..6c114c5 --- /dev/null +++ b/src/meta/keys.rs @@ -0,0 +1,101 @@ +use crate::*; + +/// A single entry in a Metadata Item Keys Box ('keys'). +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct KeyEntry { + pub key_namespace: FourCC, + pub key_value: String, +} + +/// Metadata Item Keys Box ('keys'). +/// +/// Used alongside a `hdlr` with `handler_type == 'mdta'` (Apple's key-based metadata +/// scheme): each `ilst` item under this scheme is keyed by a 1-based numeric index +/// into this table instead of a well-known FourCC, and this box gives that index a +/// namespaced string key (e.g. `com.apple.quicktime.make`). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Keys { + pub entries: Vec, +} + +impl AtomExt for Keys { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"keys"); + + fn decode_body_ext(buf: &mut B, _ext: ()) -> Result { + let entry_count = u32::decode(buf)?; + let mut entries = Vec::with_capacity((entry_count as usize).min(4096)); + + for _ in 0..entry_count { + let header = Header::decode(buf)?; + let size = header.size.ok_or(Error::InvalidSize)?; + + let key_value = String::from_utf8(buf.slice(size).to_vec()) + .map_err(|err| Error::InvalidString(err.to_string()))?; + buf.advance(size); + + entries.push(KeyEntry { + key_namespace: header.kind, + key_value, + }); + } + + Ok(Self { entries }) + } + + fn encode_body_ext(&self, buf: &mut B) -> Result<()> { + (self.entries.len() as u32).encode(buf)?; + + for entry in &self.entries { + let header = Header { + kind: entry.key_namespace, + size: Some(entry.key_value.len()), + }; + header.encode(buf)?; + entry.key_value.as_bytes().encode(buf)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keys_roundtrip() { + let keys = Keys { + entries: vec![ + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "com.apple.quicktime.make".into(), + }, + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "com.apple.quicktime.model".into(), + }, + ], + }; + + let mut buf = Vec::new(); + keys.encode(&mut buf).unwrap(); + + let decoded = Keys::decode(&mut buf.as_slice()).expect("failed to decode keys"); + assert_eq!(decoded, keys); + } + + #[test] + fn test_keys_empty() { + let keys = Keys::default(); + + let mut buf = Vec::new(); + keys.encode(&mut buf).unwrap(); + + let decoded = Keys::decode(&mut buf.as_slice()).expect("failed to decode keys"); + assert_eq!(decoded, keys); + } +} diff --git a/src/meta/mod.rs b/src/meta/mod.rs index 5d5d945..8d594c7 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -4,6 +4,7 @@ mod iloc; mod ilst; mod iprp; mod iref; +mod keys; mod pitm; mod properties; @@ -13,6 +14,7 @@ pub use iloc::*; pub use ilst::*; pub use iprp::*; pub use iref::*; +pub use keys::*; pub use pitm::*; pub use properties::*; @@ -50,6 +52,7 @@ meta_atom! { Iref, Idat, Ilst, + Keys, } // Implement helpers to make it easier to get these atoms. From 736190d3b6920d41a67ad59accef3186d41cedda Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Thu, 20 Aug 2026 16:00:43 +0200 Subject: [PATCH 2/4] data: add support for additional type indicators --- src/meta/ilst/data.rs | 485 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 432 insertions(+), 53 deletions(-) diff --git a/src/meta/ilst/data.rs b/src/meta/ilst/data.rs index de2c7ca..337796b 100644 --- a/src/meta/ilst/data.rs +++ b/src/meta/ilst/data.rs @@ -1,13 +1,267 @@ use crate::*; pub(crate) const DATA_4CC: FourCC = FourCC::new(b"data"); + +// Well-known `data` atom type indicators. +// See Apple's [well-known types](https://developer.apple.com/documentation/quicktime-file-format/well-known_types) table. +const TYPE_INDICATOR_RESERVED: u32 = 0u32; const TYPE_INDICATOR_UTF8: u32 = 1u32; +const TYPE_INDICATOR_UTF16: u32 = 2u32; +const TYPE_INDICATOR_JPEG: u32 = 13u32; +const TYPE_INDICATOR_PNG: u32 = 14u32; +const TYPE_INDICATOR_BE_SIGNED_INT: u32 = 21u32; +const TYPE_INDICATOR_BE_UNSIGNED_INT: u32 = 22u32; +const TYPE_INDICATOR_BE_FLOAT32: u32 = 23u32; +const TYPE_INDICATOR_BE_FLOAT64: u32 = 24u32; +const TYPE_INDICATOR_BMP: u32 = 27u32; +// Fixed-width signed/unsigned integer type indicators, as used by mp4v2 and +// AtomicParsley alongside the variable-length pair above (21/22). Only used +// on decode — `IlstDataValue::to_raw` always canonicalizes to 21/22 on encode. +const TYPE_INDICATOR_SIGNED_INT_FIXED: [u32; 4] = [65, 66, 67, 74]; +const TYPE_INDICATOR_UNSIGNED_INT_FIXED: [u32; 4] = [75, 76, 77, 78]; -/// A UTF-8 text payload of an iTunes-style `ilst` metadata item. +/// A [`IlstData`] value, interpreted according to its wire-level type indicator. +/// +/// [`IlstDataValue::Binary`] is the explicit "no type" reserved indicator (`0`). +/// [`IlstDataValue::Unknown`] covers everything else this crate can't interpret: +/// a type indicator it doesn't recognize, or one it does recognize but whose +/// raw bytes don't match (e.g. a `BeFloat32` value that isn't exactly 4 +/// bytes) — it carries the original type indicator so callers can still +/// inspect it, and round-trips it unchanged on encode. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum IlstDataValue { + Reserved(Vec), + Utf8(String), + Utf16(String), + Jpeg(Vec), + Png(Vec), + Bmp(Vec), + BeSignedInt(i64), + BeUnsignedInt(u64), + BeFloat32(f32), + BeFloat64(f64), + Unknown(u32, Vec), +} + +impl IlstDataValue { + fn from_raw(type_indicator: u32, value: &[u8]) -> Self { + match type_indicator { + TYPE_INDICATOR_RESERVED => IlstDataValue::Reserved(value.to_vec()), + TYPE_INDICATOR_UTF8 => match std::str::from_utf8(value) { + Ok(text) => IlstDataValue::Utf8(text.to_string()), + Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + TYPE_INDICATOR_UTF16 => match decode_utf16_be(value) { + Some(text) => IlstDataValue::Utf16(text), + None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + TYPE_INDICATOR_JPEG => IlstDataValue::Jpeg(value.to_vec()), + TYPE_INDICATOR_PNG => IlstDataValue::Png(value.to_vec()), + TYPE_INDICATOR_BMP => IlstDataValue::Bmp(value.to_vec()), + TYPE_INDICATOR_BE_SIGNED_INT => match decode_be_signed_int(value) { + Some(v) => IlstDataValue::BeSignedInt(v), + None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + TYPE_INDICATOR_BE_UNSIGNED_INT => match decode_be_unsigned_int(value) { + Some(v) => IlstDataValue::BeUnsignedInt(v), + None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + t if TYPE_INDICATOR_SIGNED_INT_FIXED.contains(&t) => { + match decode_be_signed_int(value) { + Some(v) => IlstDataValue::BeSignedInt(v), + None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + } + } + t if TYPE_INDICATOR_UNSIGNED_INT_FIXED.contains(&t) => { + match decode_be_unsigned_int(value) { + Some(v) => IlstDataValue::BeUnsignedInt(v), + None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + } + } + TYPE_INDICATOR_BE_FLOAT32 => match <[u8; 4]>::try_from(value) { + Ok(bytes) => IlstDataValue::BeFloat32(f32::from_be_bytes(bytes)), + Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + TYPE_INDICATOR_BE_FLOAT64 => match <[u8; 8]>::try_from(value) { + Ok(bytes) => IlstDataValue::BeFloat64(f64::from_be_bytes(bytes)), + Err(_) => IlstDataValue::Unknown(type_indicator, value.to_vec()), + }, + _ => IlstDataValue::Unknown(type_indicator, value.to_vec()), + } + } + + fn to_raw(&self) -> (u32, Vec) { + match self { + IlstDataValue::Utf8(text) => (TYPE_INDICATOR_UTF8, text.clone().into_bytes()), + IlstDataValue::Utf16(text) => (TYPE_INDICATOR_UTF16, encode_utf16_be(text)), + IlstDataValue::Jpeg(bytes) => (TYPE_INDICATOR_JPEG, bytes.clone()), + IlstDataValue::Png(bytes) => (TYPE_INDICATOR_PNG, bytes.clone()), + IlstDataValue::Bmp(bytes) => (TYPE_INDICATOR_BMP, bytes.clone()), + IlstDataValue::BeSignedInt(v) => { + (TYPE_INDICATOR_BE_SIGNED_INT, encode_be_signed_int(*v)) + } + IlstDataValue::BeUnsignedInt(v) => { + (TYPE_INDICATOR_BE_UNSIGNED_INT, encode_be_unsigned_int(*v)) + } + IlstDataValue::BeFloat32(v) => (TYPE_INDICATOR_BE_FLOAT32, v.to_be_bytes().to_vec()), + IlstDataValue::BeFloat64(v) => (TYPE_INDICATOR_BE_FLOAT64, v.to_be_bytes().to_vec()), + IlstDataValue::Reserved(bytes) => (TYPE_INDICATOR_RESERVED, bytes.clone()), + IlstDataValue::Unknown(type_indicator, bytes) => (*type_indicator, bytes.clone()), + } + } +} + +// Apple's well-known BE integer type indicators use a variable-length +// encoding (1, 2, 3, 4, or 8 bytes); 3-byte values need sign/zero-extension. +fn decode_be_signed_int(bytes: &[u8]) -> Option { + Some(match bytes.len() { + 1 => i64::from(bytes[0] as i8), + 2 => i64::from(i16::from_be_bytes(bytes.try_into().unwrap())), + 3 => { + let mut widened = [0u8; 4]; + widened[1..].copy_from_slice(bytes); + i64::from((i32::from_be_bytes(widened) << 8) >> 8) + } + 4 => i64::from(i32::from_be_bytes(bytes.try_into().unwrap())), + 8 => i64::from_be_bytes(bytes.try_into().unwrap()), + _ => return None, + }) +} + +fn decode_be_unsigned_int(bytes: &[u8]) -> Option { + Some(match bytes.len() { + 1 => u64::from(bytes[0]), + 2 => u64::from(u16::from_be_bytes(bytes.try_into().unwrap())), + 3 => { + let mut widened = [0u8; 4]; + widened[1..].copy_from_slice(bytes); + u64::from(u32::from_be_bytes(widened)) + } + 4 => u64::from(u32::from_be_bytes(bytes.try_into().unwrap())), + 8 => u64::from_be_bytes(bytes.try_into().unwrap()), + _ => return None, + }) +} + +// Encode using the narrowest of the widths `decode_be_signed_int` accepts +// (1/2/4/8 bytes; the 3-byte width is decode-only, for compatibility). +fn encode_be_signed_int(v: i64) -> Vec { + if let Ok(v) = i8::try_from(v) { + v.to_be_bytes().to_vec() + } else if let Ok(v) = i16::try_from(v) { + v.to_be_bytes().to_vec() + } else if let Ok(v) = i32::try_from(v) { + v.to_be_bytes().to_vec() + } else { + v.to_be_bytes().to_vec() + } +} + +fn encode_be_unsigned_int(v: u64) -> Vec { + if let Ok(v) = u8::try_from(v) { + v.to_be_bytes().to_vec() + } else if let Ok(v) = u16::try_from(v) { + v.to_be_bytes().to_vec() + } else if let Ok(v) = u32::try_from(v) { + v.to_be_bytes().to_vec() + } else { + v.to_be_bytes().to_vec() + } +} + +fn decode_utf16_be(bytes: &[u8]) -> Option { + if !bytes.len().is_multiple_of(2) { + return None; + } + let units: Vec = bytes + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_be_bytes([c[0], c[1]])) + .collect(); + String::from_utf16(&units).ok() +} + +fn encode_utf16_be(text: &str) -> Vec { + text.encode_utf16().flat_map(u16::to_be_bytes).collect() +} + +/// The content of an iTunes-style `data` atom. +/// +/// Unlike the well-known `ilst` tags ([`Copyright`], [`Tool`], [`Desc`], ...), +/// which are always UTF-8 text, Apple's `mdta`-keyed metadata items (see +/// [`Keys`]) can hold any of the well-known `data` type indicators — `value` +/// is a [`IlstDataValue`] that interprets the wire-level type indicator so +/// callers don't have to. /// /// Two encodings exist in the wild: the FFmpeg "short" style, where the value /// follows the item header directly, and the QuickTime/GPAC "long" style, -/// where the value is wrapped in a nested `data` atom. +/// where the value is wrapped in a nested `data` atom. Both decode into this +/// same representation; encoding always emits the long style. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct IlstData { + pub country_indicator: u16, + pub language_indicator: u16, + pub value: IlstDataValue, +} + +impl IlstData { + pub(crate) fn decode(buf: &mut B) -> Result { + let type_indicator_or_len = u32::decode(buf)?; + + let type_indicator = match type_indicator_or_len { + TYPE_INDICATOR_UTF8 => { + // Too short for a valid length, so probably + // UTF-8 text, FFmpeg short-style + type_indicator_or_len + } + _ => { + // Maybe Atom follows on straight away. + // Try parsing as Quicktime data atom: GPAC style or FFmpeg long style + let fourcc = FourCC::decode(buf)?; + if fourcc != DATA_4CC { + return Err(Error::UnexpectedBox(fourcc)); + } + + u32::decode(buf)? + } + }; + + let country_indicator = u16::decode(buf)?; + let language_indicator = u16::decode(buf)?; + + let size = buf.remaining(); + let value = IlstDataValue::from_raw(type_indicator, buf.slice(size)); + buf.advance(size); + + Ok(Self { + country_indicator, + language_indicator, + value, + }) + } + + pub(crate) fn encode(&self, buf: &mut B) -> Result<()> { + let (type_indicator, raw) = self.value.to_raw(); + + // the length of the nested atom is the length field (4 bytes), + // the 4CC (4 bytes), the type indicator (4 bytes), the country + // indicator (2 bytes), the language indicator (2 bytes) and + // then the actual value. + let nested_len = (4 + 4 + 4 + 2 + 2 + raw.len()) as u32; + nested_len.encode(buf)?; + DATA_4CC.encode(buf)?; + type_indicator.encode(buf)?; + self.country_indicator.encode(buf)?; + self.language_indicator.encode(buf)?; + raw.as_slice().encode(buf)?; + Ok(()) + } +} + +/// A UTF-8 text payload of an iTunes-style `ilst` metadata item. pub(crate) struct DataText { pub country_indicator: u16, pub language_indicator: u16, @@ -15,44 +269,16 @@ pub(crate) struct DataText { } pub(crate) fn decode_text(buf: &mut B) -> Result { - let type_indicator_or_len = u32::decode(buf)?; - match type_indicator_or_len { - 1 => { - // Too short for a valid length, so probably - // UTF-8 text, FFmpeg short-style - let country_indicator = u16::decode(buf)?; - let language_indicator = u16::decode(buf)?; - Ok(DataText { - country_indicator, - language_indicator, - text: String::decode(buf)?, - }) - } - _ => { - // Maybe Atom follows on straight away. - // Try parsing as Quicktime data atom: GPAC style or FFmpeg long style - let fourcc = FourCC::decode(buf)?; - if fourcc != DATA_4CC { - return Err(Error::UnexpectedBox(fourcc)); - } - let type_indicator = u32::decode(buf)?; - if type_indicator != TYPE_INDICATOR_UTF8 { - return Err(Error::Unsupported( - "Only UTF-8 text is supported in ilst data atoms", - )); - } - let country_indicator = u16::decode(buf)?; - let language_indicator = u16::decode(buf)?; - let remaining_bytes = buf.remaining(); - let body = &mut buf.slice(remaining_bytes); - let text = String::from_utf8(body.to_vec()).map_err(|_| Error::InvalidSize)?; - buf.advance(remaining_bytes); - Ok(DataText { - country_indicator, - language_indicator, - text, - }) - } + let data = IlstData::decode(buf)?; + match data.value { + IlstDataValue::Utf8(text) => Ok(DataText { + country_indicator: data.country_indicator, + language_indicator: data.language_indicator, + text, + }), + _ => Err(Error::Unsupported( + "Only UTF-8 text is supported in ilst data atoms", + )), } } @@ -62,19 +288,12 @@ pub(crate) fn encode_text( text: &str, buf: &mut B, ) -> Result<()> { - let text_bytes = text.as_bytes(); - // the length of the nested atom is the length field (4 bytes), - // the 4CC (4 bytes), the type indicator (4 bytes), the country - // indicator (2 bytes), the language indicator (2 bytes) and - // then the actual text. - let nested_len = (4 + 4 + 4 + 2 + 2 + text_bytes.len()) as u32; - nested_len.encode(buf)?; - DATA_4CC.encode(buf)?; - TYPE_INDICATOR_UTF8.encode(buf)?; - country_indicator.encode(buf)?; - language_indicator.encode(buf)?; - text_bytes.encode(buf)?; - Ok(()) + IlstData { + country_indicator, + language_indicator, + value: IlstDataValue::Utf8(text.to_string()), + } + .encode(buf) } #[cfg(test)] @@ -125,4 +344,164 @@ mod tests { let decoded = decode_text(&mut buf.as_slice()).unwrap(); assert_eq!(decoded.text, "(c) 2026 x"); } + + fn data_with(value: IlstDataValue) -> IlstData { + IlstData { + country_indicator: 0, + language_indicator: 0, + value, + } + } + + #[test] + fn test_data_roundtrip_non_utf8_type() { + // `mdta`-keyed items can use type indicators other than UTF-8 text. + let data = data_with(IlstDataValue::BeSignedInt(42)); + + let mut buf = Vec::new(); + data.encode(&mut buf).unwrap(); + + let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data"); + assert_eq!(decoded, data); + } + + #[test] + fn test_data_roundtrip_unknown_preserves_exact_bytes() { + // `Unknown` carries the original type indicator, so it round-trips + // byte-for-byte even though this crate doesn't otherwise recognize it. + let data = data_with(IlstDataValue::Unknown(999, vec![1, 2, 3])); + + let mut buf = Vec::new(); + data.encode(&mut buf).unwrap(); + + let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data"); + assert_eq!(decoded, data); + } + + #[test] + fn test_decode_value_utf8() { + let mut data_body = Vec::new(); + data_body.extend_from_slice(&1u32.to_be_bytes()); + data_body.extend_from_slice(&0u32.to_be_bytes()); + data_body.extend_from_slice(b"hello"); + + let decoded = IlstData::decode(&mut data_body.as_slice()).unwrap(); + assert_eq!(decoded.value, IlstDataValue::Utf8("hello".into())); + } + + #[test] + fn test_decode_value_utf16() { + let text = "hi \u{263A}"; // includes a non-ASCII code point + assert_eq!( + decode_raw(2, &encode_utf16_be(text)), + IlstDataValue::Utf16(text.into()) + ); + } + + #[test] + fn test_decode_value_jpeg_png_bmp() { + assert_eq!( + decode_raw(13, &[0xFF, 0xD8]), + IlstDataValue::Jpeg(vec![0xFF, 0xD8]) + ); + assert_eq!( + decode_raw(14, &[0x89, 0x50]), + IlstDataValue::Png(vec![0x89, 0x50]) + ); + assert_eq!( + decode_raw(27, &[0x42, 0x4D]), + IlstDataValue::Bmp(vec![0x42, 0x4D]) + ); + } + + // Build the long-style nested `data` atom layout `IlstData::decode` expects + // for any type indicator other than the short-style `1`. + fn decode_raw(type_indicator: u32, value: &[u8]) -> IlstDataValue { + let mut data_body = Vec::new(); + let nested_len = (4 + 4 + 4 + 2 + 2 + value.len()) as u32; + data_body.extend_from_slice(&nested_len.to_be_bytes()); + data_body.extend_from_slice(b"data"); + data_body.extend_from_slice(&type_indicator.to_be_bytes()); + data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language + data_body.extend_from_slice(value); + IlstData::decode(&mut data_body.as_slice()).unwrap().value + } + + #[test] + fn test_decode_value_be_signed_int_variable_length() { + assert_eq!(decode_raw(21, &[0xFF]), IlstDataValue::BeSignedInt(-1)); + assert_eq!( + decode_raw(21, &1000i16.to_be_bytes()), + IlstDataValue::BeSignedInt(1000) + ); + assert_eq!( + decode_raw(21, &(-1i32).to_be_bytes()[1..]), + IlstDataValue::BeSignedInt(-1) + ); + assert_eq!( + decode_raw(21, &(-100000i32).to_be_bytes()), + IlstDataValue::BeSignedInt(-100000) + ); + assert_eq!( + decode_raw(21, &(-1i64).to_be_bytes()), + IlstDataValue::BeSignedInt(-1) + ); + } + + #[test] + fn test_decode_value_be_unsigned_int_variable_length() { + assert_eq!(decode_raw(22, &[200]), IlstDataValue::BeUnsignedInt(200)); + assert_eq!( + decode_raw(22, &40000u32.to_be_bytes()), + IlstDataValue::BeUnsignedInt(40000) + ); + } + + #[test] + fn test_decode_value_fixed_width_ints() { + // mp4v2/AtomicParsley-style fixed-width type indicators, distinct + // from the variable-length 21/22 pair. + assert_eq!( + decode_raw(67, &42i32.to_be_bytes()), + IlstDataValue::BeSignedInt(42) + ); + assert_eq!( + decode_raw(77, &42u32.to_be_bytes()), + IlstDataValue::BeUnsignedInt(42) + ); + } + + #[test] + fn test_decode_value_be_float32_and_float64() { + assert_eq!( + decode_raw(23, &1.5f32.to_be_bytes()), + IlstDataValue::BeFloat32(1.5) + ); + assert_eq!( + decode_raw(24, &1.5f64.to_be_bytes()), + IlstDataValue::BeFloat64(1.5) + ); + } + + #[test] + fn test_decode_value_binary_for_reserved_type() { + assert_eq!( + decode_raw(0, &[1, 2, 3]), + IlstDataValue::Reserved(vec![1, 2, 3]) + ); + } + + #[test] + fn test_decode_value_unknown_for_unrecognized_or_malformed() { + // Unrecognized type indicator. + assert_eq!( + decode_raw(999, &[1, 2, 3]), + IlstDataValue::Unknown(999, vec![1, 2, 3]) + ); + // Wrong length for the declared type. + assert_eq!( + decode_raw(23, &[1, 2, 3]), + IlstDataValue::Unknown(23, vec![1, 2, 3]) + ); + } } From 2e676b891c3797c74d869d85c25e9670e9ec428c Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Thu, 20 Aug 2026 16:00:43 +0200 Subject: [PATCH 3/4] ilst: add support for mdat-keyed values --- src/meta/ilst/mod.rs | 79 ++++++++++++++++++++++++++++++++++++++-- src/test/av1.rs | 3 +- src/test/bbb.rs | 2 +- src/test/hevc.rs | 3 +- src/test/uncompressed.rs | 3 +- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/meta/ilst/mod.rs b/src/meta/ilst/mod.rs index cfe4908..74b4d7f 100644 --- a/src/meta/ilst/mod.rs +++ b/src/meta/ilst/mod.rs @@ -8,6 +8,7 @@ mod year; pub use covr::*; pub use cprt::*; +pub use data::*; pub use desc::*; pub use name::*; pub use tool::*; @@ -15,7 +16,12 @@ pub use year::*; use crate::*; -#[derive(Debug, Clone, PartialEq, Eq, Default)] +/// Below this value, a child's raw fourcc looks like a big-endian 1-based +/// index (top byte zero) rather than 4 ASCII/Latin1 tag characters — the +/// scheme Apple's `mdta`-keyed metadata (see [`Keys`]) uses for `ilst` items. +const MDTA_INDEX_LIMIT: u32 = 0x0100_0000; + +#[derive(Debug, Clone, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Ilst { pub name: Option, @@ -24,6 +30,10 @@ pub struct Ilst { pub desc: Option, pub ctoo: Option, // 4CC: "©too" pub cprt: Option, // iTunes item, NOT the ISO CopyrightBox + + /// Apple `mdta`-keyed items, keyed by their 1-based index into the + /// sibling [`Keys`] box's entries. + pub mdta: Vec<(u32, IlstData)>, } impl Atom for Ilst { @@ -36,6 +46,7 @@ impl Atom for Ilst { let mut desc = None; let mut ctoo = None; let mut cprt = None; + let mut mdta = vec![]; // `ilst` children live in the iTunes metadata namespace, which reuses // fourccs of unrelated ISO atoms — an ilst `cprt` item wraps a `data` @@ -57,9 +68,22 @@ impl Atom for Ilst { Tool::KIND => ctoo = Some(Tool::decode_atom(&header, buf)?), Copyright::KIND => cprt = Some(Copyright::decode_atom(&header, buf)?), kind => { - let body = Vec::decode(&mut buf.slice(size))?; + let index = u32::from(kind); + + let mdta_item = if index < MDTA_INDEX_LIMIT { + IlstData::decode(&mut buf.slice(size)).ok() + } else { + None + }; + + if let Some(data) = mdta_item { + mdta.push((index, data)) + } else { + let body = Vec::decode(&mut buf.slice(size))?; + Self::decode_unknown(&Any::Unknown(kind, body))?; + } + buf.advance(size); - Self::decode_unknown(&Any::Unknown(kind, body))?; } } } @@ -71,6 +95,7 @@ impl Atom for Ilst { desc, ctoo, cprt, + mdta, }) } @@ -81,6 +106,19 @@ impl Atom for Ilst { self.desc.encode(buf)?; self.ctoo.encode(buf)?; self.cprt.encode(buf)?; + + for (index, data) in &self.mdta { + let start = buf.len(); + 0u32.encode(buf)?; // size placeholder + FourCC::from(*index).encode(buf)?; + data.encode(buf)?; + + let size: u32 = (buf.len() - start) + .try_into() + .map_err(|_| Error::TooLarge(FourCC::from(*index)))?; + buf.set_slice(start, &size.to_be_bytes()); + } + Ok(()) } } @@ -174,4 +212,39 @@ mod tests { other => panic!("expected UnexpectedBox, got {other:?}"), } } + + // Apple's `mdta`-keyed metadata scheme (used alongside a sibling `keys` + // box) keys each `ilst` item by a raw 1-based index rather than a + // well-known fourcc — the index is stored where a fourcc would be, with + // its top byte zero (so it can't collide with a real ASCII/Latin1 tag). + #[test] + fn test_ilst_mdta_item() { + let mut data_body = Vec::new(); + data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8 + data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language + data_body.extend_from_slice(b"Apple"); + let item = atom_box(&1u32.to_be_bytes(), &atom_box(b"data", &data_body)); + let encoded = atom_box(b"ilst", &item); + + let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap(); + assert_eq!( + decoded, + Ilst { + mdta: vec![( + 1, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("Apple".into()), + } + )], + ..Default::default() + } + ); + + // The encoder writes the same long-style `data` layout back. + let mut reencoded = Vec::new(); + decoded.encode(&mut reencoded).unwrap(); + assert_eq!(reencoded, encoded); + } } diff --git a/src/test/av1.rs b/src/test/av1.rs index 1197118..599e5c1 100644 --- a/src/test/av1.rs +++ b/src/test/av1.rs @@ -181,7 +181,8 @@ fn av1() { language_indicator: 0, text: "Lavf61.7.100".into() }), - cprt: None + cprt: None, + mdta: vec![] } .into(),], }), diff --git a/src/test/bbb.rs b/src/test/bbb.rs index 23574a9..2b5d4e5 100644 --- a/src/test/bbb.rs +++ b/src/test/bbb.rs @@ -208,7 +208,7 @@ fn bbb() { udta: Some(Udta { meta: Some(Meta { hdlr: Hdlr{ handler: FourCC::new(b"mdir"), name: "".into() }, - items: vec![Ilst { name: None, year: None, covr: None, desc: None, ctoo: Some(Tool { country_indicator: 0, language_indicator: 0, text: "Lavf61.1.100".into()}), cprt: None }.into(),], + items: vec![Ilst { name: None, year: None, covr: None, desc: None, ctoo: Some(Tool { country_indicator: 0, language_indicator: 0, text: "Lavf61.1.100".into()}), cprt: None, mdta: vec![] }.into(),], }), ..Default::default() }), diff --git a/src/test/hevc.rs b/src/test/hevc.rs index dfa3be1..feb3f40 100644 --- a/src/test/hevc.rs +++ b/src/test/hevc.rs @@ -399,7 +399,8 @@ fn hevc() { language_indicator: 0, text: "Lavf61.7.100".into() }), - cprt: None + cprt: None, + mdta: vec![] } .into(),], }), diff --git a/src/test/uncompressed.rs b/src/test/uncompressed.rs index a6f85c6..e87d1da 100644 --- a/src/test/uncompressed.rs +++ b/src/test/uncompressed.rs @@ -233,7 +233,8 @@ fn uncompressed() { language_indicator: 0, text: "GPAC-2.5-DEV-rev2076-gd245ba575-rawff_amd2_2024-07-06".into() }), - cprt: None + cprt: None, + mdta: vec![] } .into(),], }), From 72f0a9f62c1a76981ea71c1481f6bd1754eea059 Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Thu, 20 Aug 2026 16:52:44 +0200 Subject: [PATCH 4/4] mdta: add full parser test for a file generated by ffmpeg --- src/meta/ilst/data.rs | 146 +++++++++++++++++--- src/meta/ilst/mod.rs | 29 ++++ src/meta/keys.rs | 31 +++++ src/test/av1_mdta.mp4 | Bin 0 -> 1345 bytes src/test/av1_mdta.rs | 307 ++++++++++++++++++++++++++++++++++++++++++ src/test/mod.rs | 1 + 6 files changed, 499 insertions(+), 15 deletions(-) create mode 100644 src/test/av1_mdta.mp4 create mode 100644 src/test/av1_mdta.rs diff --git a/src/meta/ilst/data.rs b/src/meta/ilst/data.rs index 337796b..0d1874c 100644 --- a/src/meta/ilst/data.rs +++ b/src/meta/ilst/data.rs @@ -9,6 +9,9 @@ const TYPE_INDICATOR_UTF8: u32 = 1u32; const TYPE_INDICATOR_UTF16: u32 = 2u32; const TYPE_INDICATOR_JPEG: u32 = 13u32; const TYPE_INDICATOR_PNG: u32 = 14u32; +// Per the well-known types table, 21/22 only cover 1-4 byte integers; the +// fixed-width indicators below (65-67/75-77) are the 1/2/4-byte equivalents, +// and 74/78 are the *only* valid indicators for an 8-byte integer. const TYPE_INDICATOR_BE_SIGNED_INT: u32 = 21u32; const TYPE_INDICATOR_BE_UNSIGNED_INT: u32 = 22u32; const TYPE_INDICATOR_BE_FLOAT32: u32 = 23u32; @@ -16,9 +19,16 @@ const TYPE_INDICATOR_BE_FLOAT64: u32 = 24u32; const TYPE_INDICATOR_BMP: u32 = 27u32; // Fixed-width signed/unsigned integer type indicators, as used by mp4v2 and // AtomicParsley alongside the variable-length pair above (21/22). Only used -// on decode — `IlstDataValue::to_raw` always canonicalizes to 21/22 on encode. +// on decode for widths 1/2/4 bytes — `IlstDataValue::to_raw` canonicalizes to +// 21/22 for those. 74/78 (the 8-byte entries) are also `to_raw`'s encode +// target for values too large for 21/22. Each entry's declared width (1, 2, +// 4, 8 bytes, matching array position) is exact: a payload of any other +// length is malformed. const TYPE_INDICATOR_SIGNED_INT_FIXED: [u32; 4] = [65, 66, 67, 74]; const TYPE_INDICATOR_UNSIGNED_INT_FIXED: [u32; 4] = [75, 76, 77, 78]; +const FIXED_INT_WIDTHS: [usize; 4] = [1, 2, 4, 8]; +const TYPE_INDICATOR_BE_SIGNED_INT64: u32 = TYPE_INDICATOR_SIGNED_INT_FIXED[3]; +const TYPE_INDICATOR_BE_UNSIGNED_INT64: u32 = TYPE_INDICATOR_UNSIGNED_INT_FIXED[3]; /// A [`IlstData`] value, interpreted according to its wire-level type indicator. /// @@ -59,24 +69,37 @@ impl IlstDataValue { TYPE_INDICATOR_JPEG => IlstDataValue::Jpeg(value.to_vec()), TYPE_INDICATOR_PNG => IlstDataValue::Png(value.to_vec()), TYPE_INDICATOR_BMP => IlstDataValue::Bmp(value.to_vec()), - TYPE_INDICATOR_BE_SIGNED_INT => match decode_be_signed_int(value) { + // 21/22 only cover 1-4 byte widths (an 8-byte value must use the + // fixed 74/78 indicators instead), so reject that width here even + // though `decode_be_signed_int`/`decode_be_unsigned_int` accept it. + TYPE_INDICATOR_BE_SIGNED_INT if value.len() <= 4 => match decode_be_signed_int(value) { Some(v) => IlstDataValue::BeSignedInt(v), None => IlstDataValue::Unknown(type_indicator, value.to_vec()), }, - TYPE_INDICATOR_BE_UNSIGNED_INT => match decode_be_unsigned_int(value) { - Some(v) => IlstDataValue::BeUnsignedInt(v), - None => IlstDataValue::Unknown(type_indicator, value.to_vec()), - }, - t if TYPE_INDICATOR_SIGNED_INT_FIXED.contains(&t) => { - match decode_be_signed_int(value) { - Some(v) => IlstDataValue::BeSignedInt(v), + TYPE_INDICATOR_BE_UNSIGNED_INT if value.len() <= 4 => { + match decode_be_unsigned_int(value) { + Some(v) => IlstDataValue::BeUnsignedInt(v), None => IlstDataValue::Unknown(type_indicator, value.to_vec()), } } + t if TYPE_INDICATOR_SIGNED_INT_FIXED.contains(&t) => { + let width = FIXED_INT_WIDTHS[TYPE_INDICATOR_SIGNED_INT_FIXED + .iter() + .position(|&x| x == t) + .unwrap()]; + match (value.len() == width, decode_be_signed_int(value)) { + (true, Some(v)) => IlstDataValue::BeSignedInt(v), + _ => IlstDataValue::Unknown(type_indicator, value.to_vec()), + } + } t if TYPE_INDICATOR_UNSIGNED_INT_FIXED.contains(&t) => { - match decode_be_unsigned_int(value) { - Some(v) => IlstDataValue::BeUnsignedInt(v), - None => IlstDataValue::Unknown(type_indicator, value.to_vec()), + let width = FIXED_INT_WIDTHS[TYPE_INDICATOR_UNSIGNED_INT_FIXED + .iter() + .position(|&x| x == t) + .unwrap()]; + match (value.len() == width, decode_be_unsigned_int(value)) { + (true, Some(v)) => IlstDataValue::BeUnsignedInt(v), + _ => IlstDataValue::Unknown(type_indicator, value.to_vec()), } } TYPE_INDICATOR_BE_FLOAT32 => match <[u8; 4]>::try_from(value) { @@ -99,10 +122,24 @@ impl IlstDataValue { IlstDataValue::Png(bytes) => (TYPE_INDICATOR_PNG, bytes.clone()), IlstDataValue::Bmp(bytes) => (TYPE_INDICATOR_BMP, bytes.clone()), IlstDataValue::BeSignedInt(v) => { - (TYPE_INDICATOR_BE_SIGNED_INT, encode_be_signed_int(*v)) + let bytes = encode_be_signed_int(*v); + // 21 only covers up to 4 bytes; an 8-byte encoding must use + // the fixed 74 (BE 64-bit Signed Integer) indicator instead. + let type_indicator = if bytes.len() > 4 { + TYPE_INDICATOR_BE_SIGNED_INT64 + } else { + TYPE_INDICATOR_BE_SIGNED_INT + }; + (type_indicator, bytes) } IlstDataValue::BeUnsignedInt(v) => { - (TYPE_INDICATOR_BE_UNSIGNED_INT, encode_be_unsigned_int(*v)) + let bytes = encode_be_unsigned_int(*v); + let type_indicator = if bytes.len() > 4 { + TYPE_INDICATOR_BE_UNSIGNED_INT64 + } else { + TYPE_INDICATOR_BE_UNSIGNED_INT + }; + (type_indicator, bytes) } IlstDataValue::BeFloat32(v) => (TYPE_INDICATOR_BE_FLOAT32, v.to_be_bytes().to_vec()), IlstDataValue::BeFloat64(v) => (TYPE_INDICATOR_BE_FLOAT64, v.to_be_bytes().to_vec()), @@ -442,9 +479,16 @@ mod tests { decode_raw(21, &(-100000i32).to_be_bytes()), IlstDataValue::BeSignedInt(-100000) ); + } + + #[test] + fn test_decode_value_be_signed_int_rejects_eight_bytes() { + // Per the well-known types table, 21 only covers 1-4 byte integers; + // an 8-byte payload must use the fixed 74 indicator instead, so it's + // malformed (not silently accepted) under 21. assert_eq!( decode_raw(21, &(-1i64).to_be_bytes()), - IlstDataValue::BeSignedInt(-1) + IlstDataValue::Unknown(21, (-1i64).to_be_bytes().to_vec()) ); } @@ -457,6 +501,59 @@ mod tests { ); } + #[test] + fn test_decode_value_be_unsigned_int_rejects_eight_bytes() { + assert_eq!( + decode_raw(22, &1u64.to_be_bytes()), + IlstDataValue::Unknown(22, 1u64.to_be_bytes().to_vec()) + ); + } + + #[test] + fn test_encode_be_int_uses_64_bit_fixed_indicator_beyond_four_bytes() { + // Values that fit in i32/u32 canonicalize to the variable-length + // 21/22 pair; values that need the full 8 bytes must use the fixed + // 74/78 indicators instead (21/22 only cover up to 4 bytes). + assert_eq!( + data_with(IlstDataValue::BeSignedInt(42)).value.to_raw().0, + 21 + ); + assert_eq!( + data_with(IlstDataValue::BeSignedInt(i64::from(i32::MAX) + 1)) + .value + .to_raw() + .0, + 74 + ); + assert_eq!( + data_with(IlstDataValue::BeUnsignedInt(42)).value.to_raw().0, + 22 + ); + assert_eq!( + data_with(IlstDataValue::BeUnsignedInt(u64::from(u32::MAX) + 1)) + .value + .to_raw() + .0, + 78 + ); + } + + #[test] + fn test_data_roundtrip_eight_byte_int() { + for value in [ + IlstDataValue::BeSignedInt(i64::from(i32::MAX) + 1), + IlstDataValue::BeUnsignedInt(u64::from(u32::MAX) + 1), + ] { + let data = data_with(value.clone()); + + let mut buf = Vec::new(); + data.encode(&mut buf).unwrap(); + + let decoded = IlstData::decode(&mut buf.as_slice()).expect("failed to decode data"); + assert_eq!(decoded.value, value); + } + } + #[test] fn test_decode_value_fixed_width_ints() { // mp4v2/AtomicParsley-style fixed-width type indicators, distinct @@ -471,6 +568,25 @@ mod tests { ); } + #[test] + fn test_decode_value_fixed_width_ints_rejects_mismatched_length() { + // Unlike the variable-length 21/22 pair, each fixed-width indicator's + // width is exact: 67 declares 4 bytes, so an 8-byte payload (which + // would be perfectly valid under 21) must not be silently accepted. + assert_eq!( + decode_raw(67, &42i64.to_be_bytes()), + IlstDataValue::Unknown(67, 42i64.to_be_bytes().to_vec()) + ); + assert_eq!( + decode_raw(65, &42i32.to_be_bytes()), + IlstDataValue::Unknown(65, 42i32.to_be_bytes().to_vec()) + ); + assert_eq!( + decode_raw(77, &42u64.to_be_bytes()), + IlstDataValue::Unknown(77, 42u64.to_be_bytes().to_vec()) + ); + } + #[test] fn test_decode_value_be_float32_and_float64() { assert_eq!( diff --git a/src/meta/ilst/mod.rs b/src/meta/ilst/mod.rs index 74b4d7f..fe4c32d 100644 --- a/src/meta/ilst/mod.rs +++ b/src/meta/ilst/mod.rs @@ -108,6 +108,13 @@ impl Atom for Ilst { self.cprt.encode(buf)?; for (index, data) in &self.mdta { + if *index == 0 || *index >= MDTA_INDEX_LIMIT { + // A top-byte-nonzero index would encode as a FourCC that + // looks like (or collides with) a real ASCII/Latin1 tag, + // silently changing meaning on the next decode. + return Err(Error::Unsupported("mdta index out of range")); + } + let start = buf.len(); 0u32.encode(buf)?; // size placeholder FourCC::from(*index).encode(buf)?; @@ -247,4 +254,26 @@ mod tests { decoded.encode(&mut reencoded).unwrap(); assert_eq!(reencoded, encoded); } + + // An out-of-range index would encode as a FourCC whose top byte is + // nonzero, which could look like (or collide with) a real ASCII/Latin1 + // tag on the next decode -- reject it instead of silently corrupting it. + #[test] + fn test_ilst_mdta_index_out_of_range_rejected() { + let ilst = Ilst { + mdta: vec![( + MDTA_INDEX_LIMIT, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("Apple".into()), + }, + )], + ..Default::default() + }; + + let mut buf = Vec::new(); + let err = ilst.encode(&mut buf).unwrap_err(); + assert!(matches!(err, Error::Unsupported(_))); + } } diff --git a/src/meta/keys.rs b/src/meta/keys.rs index 6c114c5..e9cbb70 100644 --- a/src/meta/keys.rs +++ b/src/meta/keys.rs @@ -32,6 +32,9 @@ impl AtomExt for Keys { for _ in 0..entry_count { let header = Header::decode(buf)?; let size = header.size.ok_or(Error::InvalidSize)?; + if size > buf.remaining() { + return Err(Error::OutOfBounds); + } let key_value = String::from_utf8(buf.slice(size).to_vec()) .map_err(|err| Error::InvalidString(err.to_string()))?; @@ -98,4 +101,32 @@ mod tests { let decoded = Keys::decode(&mut buf.as_slice()).expect("failed to decode keys"); assert_eq!(decoded, keys); } + + // A declared per-entry size larger than what's actually left in the box + // must be a decode error, not a panic from an out-of-bounds slice. + #[test] + fn test_keys_truncated_entry() { + let mut buf = Vec::new(); + 0u32.encode(&mut buf).unwrap(); // version + flags + 1u32.encode(&mut buf).unwrap(); // entry_count + 100u32.encode(&mut buf).unwrap(); // header size (way beyond what follows) + FourCC::new(b"mdta").encode(&mut buf).unwrap(); + buf.extend_from_slice(b"short"); + + let err = Keys::decode_body(&mut buf.as_slice()).unwrap_err(); + assert!(matches!(err, Error::OutOfBounds)); + } + + // A bogus/huge entry_count with no matching entries must fail cleanly + // once the buffer runs out, rather than allocate or loop unboundedly. + #[test] + fn test_keys_malformed_count() { + let mut buf = Vec::new(); + 0u32.encode(&mut buf).unwrap(); // version + flags + u32::MAX.encode(&mut buf).unwrap(); // entry_count + FourCC::new(b"mdta").encode(&mut buf).unwrap(); // truncated first header + + let err = Keys::decode_body(&mut buf.as_slice()).unwrap_err(); + assert!(matches!(err, Error::OutOfBounds)); + } } diff --git a/src/test/av1_mdta.mp4 b/src/test/av1_mdta.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e316c0c74d1f67240046e069305152dba4048b71 GIT binary patch literal 1345 zcmcgs&r1|>6n~=|x@olKN@^QzA>?J3o!wgWlwqcXh(rZRVBgvKu1?N(XPNo#sC6;y zSVV;O2Z$j=5M6>U26^gGhwfz%*g+zqLq@c{H#1A4S;ro{d2c?S_ulu%`+mOwkSxlX zvf)?)pbp3~H>0A>it=$402+%n=KxAX=uGZzLgehjl^0XhbtSu@#9%E3Fbf&zg;?e} zuX|qX;x-2KXG3!GaK{XtAyKEb0r*Ru*aYfx; zVkg(7gI7k!oTzAHf;o~?8D&5ir6M|US9s%wSJtWfTtc@w z#gGu+T(Qlh?<)*u;r#85i2vNjFt+K#x-gCTdlOvTG1s#drok&o*W6cN_c$01ZH}Y&(OIb*@fCdL4sYi>@m$|CECtb z4>C#!x!iUPtAt=XfdxyHnKU$$`@vMGEg;f-nu)T>(~^4!ZZF}yRIqff>5mNm940V@ z77)j$hbB!vy-|5I2T}2UKuefAp=J^4LZxex3{#X=5XAzuJ$TxVVoU_JBOHe>AWyo? YtkA*jM5To%!VyEvE#`A+HLYgUUwB#;m;e9( literal 0 HcmV?d00001 diff --git a/src/test/av1_mdta.rs b/src/test/av1_mdta.rs new file mode 100644 index 0000000..b24c1fa --- /dev/null +++ b/src/test/av1_mdta.rs @@ -0,0 +1,307 @@ +use crate::*; + +#[test] +fn av1_mdta() { + // Created from av1.mp4 with the following command: + // ffmpeg -i av1.mp4 -c copy -movflags use_metadata_tags \ + // -metadata com.example.test="some value" av1_mdta.mp4 + const ENCODED: &[u8] = include_bytes!("av1_mdta.mp4"); + + let buf = &mut std::io::Cursor::new(ENCODED); + let ftyp = Ftyp::decode(buf).expect("failed to decode ftyp"); + + assert_eq!( + ftyp, + Ftyp { + major_brand: b"isom".into(), + minor_version: 512, + compatible_brands: vec![ + b"isom".into(), + b"av01".into(), + b"iso2".into(), + b"mp41".into() + ], + } + ); + + let free = Free::decode(buf).expect("failed to decode free"); + + let mdat = Mdat::decode(buf).expect("failed to decode mdat"); + + let moov = Moov::decode(buf).expect("failed to decode moov"); + assert_eq!( + moov, + Moov { + mvhd: Mvhd { + creation_time: 0, + modification_time: 0, + timescale: 25000, + duration: 1000, + rate: 1.into(), + volume: 1.into(), + matrix: Matrix { + a: 65536, + b: 0, + u: 0, + c: 0, + d: 65536, + v: 0, + x: 0, + y: 0, + w: 1073741824 + }, + next_track_id: 2 + }, + mvex: None, + trak: vec![Trak { + tkhd: Tkhd { + creation_time: 0, + modification_time: 0, + track_id: 1, + duration: 1000, + layer: 0, + alternate_group: 0, + enabled: true, + in_movie: true, + size_is_aspect_ratio: false, + volume: 0.into(), + matrix: Matrix { + a: 65536, + b: 0, + u: 0, + c: 0, + d: 65536, + v: 0, + x: 0, + y: 0, + w: 1073741824 + }, + width: 1920.into(), + height: 1080.into() + }, + edts: Some(Edts { + elst: Some(Elst { + entries: vec![ElstEntry { + segment_duration: 1000, + media_time: Some(0), + media_rate: 1.into() + }] + }) + }), + mdia: Mdia { + mdhd: Mdhd { + creation_time: 0, + modification_time: 0, + timescale: 25000, + duration: 1000, + language: "und".into() + }, + hdlr: Hdlr { + handler: b"vide".into(), + name: "obu@GPAC2.1-DEV-rev199-g8e29f6e8b-github_master".into() + }, + minf: Minf { + vmhd: Some(Vmhd { + graphics_mode: 0, + op_color: RgbColor { + red: 0, + green: 0, + blue: 0 + } + }), + dinf: Dinf { + dref: Dref { + urls: vec![Url { + location: "".into() + }] + } + }, + stbl: Stbl { + stsd: Stsd { + codecs: vec![Av01 { + visual: Visual { + data_reference_index: 1, + width: 1920, + height: 1080, + horizresolution: 72.into(), + vertresolution: 72.into(), + frame_count: 1, + compressor: "".into(), + depth: 24 + }, + av1c: Av1c { + seq_profile: 0, + seq_level_idx_0: 9, + seq_tier_0: false, + high_bitdepth: true, + twelve_bit: false, + monochrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: true, + chroma_sample_position: 0, + initial_presentation_delay: None, + config_obus: vec![ + 10, 11, 0, 0, 0, 74, 171, 191, 195, 119, 255, 231, 1 + ] + }, + btrt: Some(Btrt { + buffer_size_db: 0, + max_bitrate: 70500, + avg_bitrate: 50400 + }), + pasp: Some(Pasp { + h_spacing: 1, + v_spacing: 1 + }), + ..Default::default() + } + .into()], + }, + stts: Stts { + entries: vec![SttsEntry { + sample_count: 1, + sample_delta: 1000 + }] + }, + ctts: None, + stss: None, + stsc: Stsc { + entries: vec![StscEntry { + first_chunk: 1, + samples_per_chunk: 1, + sample_description_index: 1 + }] + }, + stsz: Stsz { + samples: StszSamples::Identical { + count: 1, + size: 252 + } + }, + stco: Some(Stco { entries: vec![48] }), + co64: None, + sbgp: vec![], + sgpd: vec![], + subs: vec![], + saio: vec![], + saiz: vec![], + cslg: None, + }, + ..Default::default() + } + }, + ..Default::default() + }], + udta: Some(Udta { + meta: Some(Meta { + hdlr: Hdlr { + handler: FourCC::new(b"mdta"), + name: "".into() + }, + items: vec![ + Keys { + entries: vec![ + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "major_brand".into() + }, + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "minor_version".into() + }, + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "compatible_brands".into() + }, + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "com.example.test".into() + }, + KeyEntry { + key_namespace: FourCC::new(b"mdta"), + key_value: "encoder".into() + }, + ] + } + .into(), + Ilst { + name: None, + year: None, + covr: None, + desc: None, + ctoo: None, + cprt: None, + mdta: vec![ + ( + 1, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("iso6".into()) + } + ), + ( + 2, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("512".into()) + } + ), + ( + 3, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("iso6cmfcav01mp41".into()) + } + ), + ( + 4, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("some value".into()) + } + ), + ( + 5, + IlstData { + country_indicator: 0, + language_indicator: 0, + value: IlstDataValue::Utf8("Lavf63.1.101".into()) + } + ) + ] + } + .into(), + ], + }), + ..Default::default() + }), + ..Default::default() + } + ); + + // Make sure the av01 atom encodes/decodes to the exact same content. + let av01 = &moov.trak[0].mdia.minf.stbl.stsd.codecs[0]; + av01.assert_encode_decode(); + + let mut reencoded = Vec::new(); + ftyp.encode(&mut reencoded).expect("failed to encode ftyp"); + free.encode(&mut reencoded).expect("failed to encode free"); + mdat.encode(&mut reencoded).expect("failed to encode mdat"); + moov.encode(&mut reencoded).expect("failed to encode moov"); + + let reencoded_buf = &mut reencoded.as_slice(); + let reencoded_ftyp = Ftyp::decode(reencoded_buf).expect("failed to re-decode ftyp"); + let reencoded_free = Free::decode(reencoded_buf).expect("failed to re-decode free"); + let reencoded_mdat = Mdat::decode(reencoded_buf).expect("failed to re-decode mdat"); + let reencoded_moov = Moov::decode(reencoded_buf).expect("failed to re-decode moov"); + assert_eq!(reencoded_ftyp, ftyp); + assert_eq!(reencoded_free, free); + assert_eq!(reencoded_mdat, mdat); + assert_eq!(reencoded_moov, moov); + + // assert_eq!(buf, ENCODED); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index dcae856..789d319 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,4 +1,5 @@ mod av1; +mod av1_mdta; mod bbb; mod esds; mod flac;