Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ any! {
Name,
Rtng,
Year,
Keys,
Moov,
Mvhd,
Ainf,
Expand Down
601 changes: 548 additions & 53 deletions src/meta/ilst/data.rs

Large diffs are not rendered by default.

108 changes: 105 additions & 3 deletions src/meta/ilst/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ mod year;

pub use covr::*;
pub use cprt::*;
pub use data::*;
pub use desc::*;
pub use name::*;
pub use tool::*;
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<Name>,
Expand All @@ -24,6 +30,10 @@ pub struct Ilst {
pub desc: Option<Desc>,
pub ctoo: Option<Tool>, // 4CC: "©too"
pub cprt: Option<Copyright>, // 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 {
Expand All @@ -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`
Expand All @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Self::decode_unknown(&Any::Unknown(kind, body))?;
}
}
}
Expand All @@ -71,6 +95,7 @@ impl Atom for Ilst {
desc,
ctoo,
cprt,
mdta,
})
}

Expand All @@ -81,6 +106,26 @@ impl Atom for Ilst {
self.desc.encode(buf)?;
self.ctoo.encode(buf)?;
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)?;
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());
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Ok(())
}
}
Expand Down Expand Up @@ -174,4 +219,61 @@ 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);
}

// 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(_)));
}
}
132 changes: 132 additions & 0 deletions src/meta/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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<KeyEntry>,
}

impl AtomExt for Keys {
type Ext = ();

const KIND_EXT: FourCC = FourCC::new(b"keys");

fn decode_body_ext<B: Buf>(buf: &mut B, _ext: ()) -> Result<Self> {
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)?;
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()))?;
buf.advance(size);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

entries.push(KeyEntry {
key_namespace: header.kind,
key_value,
});
}

Ok(Self { entries })
}

fn encode_body_ext<B: BufMut>(&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);
}

// 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));
}
}
3 changes: 3 additions & 0 deletions src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod iloc;
mod ilst;
mod iprp;
mod iref;
mod keys;
mod pitm;
mod properties;

Expand All @@ -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::*;

Expand Down Expand Up @@ -50,6 +52,7 @@ meta_atom! {
Iref,
Idat,
Ilst,
Keys,
}

// Implement helpers to make it easier to get these atoms.
Expand Down
3 changes: 2 additions & 1 deletion src/test/av1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ fn av1() {
language_indicator: 0,
text: "Lavf61.7.100".into()
}),
cprt: None
cprt: None,
mdta: vec![]
}
.into(),],
}),
Expand Down
Binary file added src/test/av1_mdta.mp4
Binary file not shown.
Loading
Loading