From c96c7a0a10d1b6f96b7d4820c7b16671100e6cb2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 17 Jul 2026 15:27:21 -0700 Subject: [PATCH] validate typed atom kinds and preserve dref entries --- src/atom.rs | 29 ++++++ src/meta/iinf.rs | 2 +- src/meta/mod.rs | 5 +- src/moov/mod.rs | 2 +- src/moov/trak/mdia/minf/dinf/dref/mod.rs | 123 +++++++++++++++++++++-- src/moov/trak/mdia/minf/dinf/dref/urn.rs | 34 +++++++ src/test/av1.rs | 5 +- src/test/bbb.rs | 8 +- src/test/esds.rs | 8 +- src/test/flac.rs | 5 +- src/test/h264.rs | 4 +- src/test/hevc.rs | 5 +- src/test/libavif_anim.rs | 5 +- src/test/uncompressed.rs | 5 +- src/test/vp9.rs | 5 +- src/tokio/atom.rs | 4 + 16 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 src/moov/trak/mdia/minf/dinf/dref/urn.rs diff --git a/src/atom.rs b/src/atom.rs index b493dbe5..b0e94c9c 100644 --- a/src/atom.rs +++ b/src/atom.rs @@ -49,6 +49,10 @@ impl DecodeMaybe for T { None => return Ok(None), }; + if header.kind != T::KIND { + return Err(Error::UnexpectedBox(header.kind)); + } + let size = header.size.unwrap_or(buf.remaining()); if size > buf.remaining() { return Ok(None); @@ -86,6 +90,10 @@ impl ReadFrom for Option { None => return Ok(None), }; + if header.kind != T::KIND { + return Err(Error::UnexpectedBox(header.kind)); + } + let body = &mut header.read_body(r)?; let atom = match T::decode_body(body) { @@ -222,3 +230,24 @@ macro_rules! nested { } pub(crate) use nested; + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + const FTYP_BODY_TAGGED_AS_MOOV: &[u8] = b"\0\0\0\x14mooviso6\0\0\x02\0mp41"; + + #[test] + fn typed_decode_rejects_unexpected_box() { + let err = Ftyp::decode(&mut Cursor::new(FTYP_BODY_TAGGED_AS_MOOV)).unwrap_err(); + assert!(matches!(err, Error::UnexpectedBox(kind) if kind == Moov::KIND)); + } + + #[test] + fn typed_read_from_rejects_unexpected_box() { + let err = + ::read_from(&mut Cursor::new(FTYP_BODY_TAGGED_AS_MOOV)).unwrap_err(); + assert!(matches!(err, Error::UnexpectedBox(kind) if kind == Moov::KIND)); + } +} diff --git a/src/meta/iinf.rs b/src/meta/iinf.rs index f2d65570..1e62e197 100644 --- a/src/meta/iinf.rs +++ b/src/meta/iinf.rs @@ -348,7 +348,7 @@ mod tests { assert!(matches!( fuzz_result, - Err(Error::Unsupported("infe version 1 extensions")) + Err(Error::UnexpectedBox(kind)) if kind == FourCC::new(b"\0\0A\x80") )); } } diff --git a/src/meta/mod.rs b/src/meta/mod.rs index ede274a2..8d970f1c 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -154,9 +154,10 @@ mod tests { expected.push(Pitm { item_id: 3 }); expected.push(Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into(), - }], + } + .into()], }, }); expected.push(Iloc { diff --git a/src/moov/mod.rs b/src/moov/mod.rs index 16142647..7b3f9864 100644 --- a/src/moov/mod.rs +++ b/src/moov/mod.rs @@ -185,7 +185,7 @@ mod test { }), dinf: Dinf { dref: Dref { - urls: vec![Url::default()] + entries: vec![Url::default().into()] } }, stbl: Stbl { diff --git a/src/moov/trak/mdia/minf/dinf/dref/mod.rs b/src/moov/trak/mdia/minf/dinf/dref/mod.rs index 6c9270d0..7a6259de 100644 --- a/src/moov/trak/mdia/minf/dinf/dref/mod.rs +++ b/src/moov/trak/mdia/minf/dinf/dref/mod.rs @@ -1,12 +1,83 @@ mod url; +mod urn; pub use url::*; +pub use urn::*; use crate::*; #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Dref { - pub urls: Vec, + pub entries: Vec, +} + +/// An entry in a data reference box. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum DrefEntry { + Url(Url), + Urn(Urn), + Unknown(FourCC, Vec), +} + +impl DrefEntry { + pub fn kind(&self) -> FourCC { + match self { + Self::Url(_) => Url::KIND, + Self::Urn(_) => Urn::KIND, + Self::Unknown(kind, _) => *kind, + } + } +} + +impl Decode for DrefEntry { + fn decode(buf: &mut B) -> Result { + let header = Header::decode(buf)?; + let size = header.size.unwrap_or(buf.remaining()); + if size > buf.remaining() { + return Err(Error::OutOfBounds); + } + + match header.kind { + kind if kind == Url::KIND => Url::decode_atom(&header, buf).map(Self::Url), + kind if kind == Urn::KIND => Urn::decode_atom(&header, buf).map(Self::Urn), + kind => { + let data = Vec::decode(&mut buf.slice(size))?; + buf.advance(size); + Ok(Self::Unknown(kind, data)) + } + } + } +} + +impl Encode for DrefEntry { + fn encode(&self, buf: &mut B) -> Result<()> { + match self { + Self::Url(url) => url.encode(buf), + Self::Urn(urn) => urn.encode(buf), + Self::Unknown(kind, data) => { + Header { + kind: *kind, + size: Some(data.len()), + } + .encode(buf)?; + data.encode(buf) + } + } + } +} + +impl From for DrefEntry { + fn from(url: Url) -> Self { + Self::Url(url) + } +} + +impl From for DrefEntry { + fn from(urn: Urn) -> Self { + Self::Urn(urn) + } } impl AtomExt for Dref { @@ -16,23 +87,59 @@ impl AtomExt for Dref { fn decode_body_ext(buf: &mut B, _ext: ()) -> Result { let entry_count = u32::decode(buf)?; - let mut urls = Vec::new(); + let mut entries = Vec::new(); for _ in 0..entry_count { - let url = Url::decode(buf)?; - urls.push(url); + entries.push(DrefEntry::decode(buf)?); } - Ok(Dref { urls }) + Ok(Dref { entries }) } fn encode_body_ext(&self, buf: &mut B) -> Result<()> { - (self.urls.len() as u32).encode(buf)?; + let entry_count = + u32::try_from(self.entries.len()).map_err(|_| Error::TooLarge(Self::KIND))?; + entry_count.encode(buf)?; - for url in &self.urls { - url.encode(buf)?; + for entry in &self.entries { + entry.encode(buf)?; } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + const ENCODED_URN_AND_UNKNOWN: &[u8] = &[ + 0, 0, 0, 48, b'd', b'r', b'e', b'f', 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 21, b'u', b'r', b'n', + b' ', 0, 0, 0, 0, b'n', b'a', b'm', b'e', 0, b'l', b'o', b'c', 0, 0, 0, 0, 11, b'a', b'b', + b'c', b'd', 1, 2, 3, + ]; + + #[test] + fn decode_and_preserve_urn_and_unknown_entries() { + let dref = Dref::decode(&mut Cursor::new(ENCODED_URN_AND_UNKNOWN)).unwrap(); + + assert_eq!( + dref, + Dref { + entries: vec![ + Urn { + name: "name".into(), + location: "loc".into(), + } + .into(), + DrefEntry::Unknown(FourCC::new(b"abcd"), vec![1, 2, 3]), + ], + } + ); + + let mut encoded = Vec::new(); + dref.encode(&mut encoded).unwrap(); + assert_eq!(encoded, ENCODED_URN_AND_UNKNOWN); + } +} diff --git a/src/moov/trak/mdia/minf/dinf/dref/urn.rs b/src/moov/trak/mdia/minf/dinf/dref/urn.rs new file mode 100644 index 00000000..3ef9d988 --- /dev/null +++ b/src/moov/trak/mdia/minf/dinf/dref/urn.rs @@ -0,0 +1,34 @@ +use crate::*; + +ext! { + name: Urn, + versions: [0], + flags: {} +} + +/// A name-based data reference and its location. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Urn { + pub name: String, + pub location: String, +} + +impl AtomExt for Urn { + type Ext = UrnExt; + + const KIND_EXT: FourCC = FourCC::new(b"urn "); + + fn decode_body_ext(buf: &mut B, _ext: UrnExt) -> Result { + Ok(Self { + name: String::decode(buf)?, + location: String::decode(buf)?, + }) + } + + fn encode_body_ext(&self, buf: &mut B) -> Result { + self.name.as_str().encode(buf)?; + self.location.as_str().encode(buf)?; + Ok(UrnExt::default()) + } +} diff --git a/src/test/av1.rs b/src/test/av1.rs index 1197118b..247d60d2 100644 --- a/src/test/av1.rs +++ b/src/test/av1.rs @@ -104,9 +104,10 @@ fn av1() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into() - }] + } + .into()] } }, stbl: Stbl { diff --git a/src/test/bbb.rs b/src/test/bbb.rs index b3f760eb..ce82c5c9 100644 --- a/src/test/bbb.rs +++ b/src/test/bbb.rs @@ -69,9 +69,9 @@ fn bbb() { .into(), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into(), - }], + }.into()], }, }, stbl: Stbl { @@ -150,9 +150,9 @@ fn bbb() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into(), - }], + }.into()], }, }, stbl: Stbl { diff --git a/src/test/esds.rs b/src/test/esds.rs index 73daa7e0..a508361c 100644 --- a/src/test/esds.rs +++ b/src/test/esds.rs @@ -69,9 +69,9 @@ fn esds() { .into(), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into(), - }], + }.into()], }, }, stbl: Stbl { @@ -150,9 +150,9 @@ fn esds() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into(), - }], + }.into()], }, }, stbl: Stbl { diff --git a/src/test/flac.rs b/src/test/flac.rs index e433f757..e26f2f76 100644 --- a/src/test/flac.rs +++ b/src/test/flac.rs @@ -93,9 +93,10 @@ fn flac() { smhd: Some(Smhd { balance: 0.into() }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into() - }] + } + .into()] } }, stbl: Stbl { diff --git a/src/test/h264.rs b/src/test/h264.rs index e7f2e27e..125eabbb 100644 --- a/src/test/h264.rs +++ b/src/test/h264.rs @@ -109,7 +109,7 @@ fn avcc_ext() { }), dinf: Dinf { dref: Dref { - urls: vec![Url::default()], + entries: vec![Url::default().into()], }, }, stbl: Stbl { @@ -224,7 +224,7 @@ fn avcc_ext() { smhd: Some(Smhd::default()), dinf: Dinf { dref: Dref { - urls: vec![Url::default()], + entries: vec![Url::default().into()], }, }, stbl: Stbl { diff --git a/src/test/hevc.rs b/src/test/hevc.rs index dfa3be1e..a89bc89d 100644 --- a/src/test/hevc.rs +++ b/src/test/hevc.rs @@ -99,9 +99,10 @@ fn hevc() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".to_string() - }] + } + .into()] } }, stbl: Stbl { diff --git a/src/test/libavif_anim.rs b/src/test/libavif_anim.rs index 46ebd97e..8f53f515 100644 --- a/src/test/libavif_anim.rs +++ b/src/test/libavif_anim.rs @@ -210,9 +210,10 @@ fn av1_anim() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into() - }] + } + .into()] } }, stbl: Stbl { diff --git a/src/test/uncompressed.rs b/src/test/uncompressed.rs index a6f85c67..e6ec8330 100644 --- a/src/test/uncompressed.rs +++ b/src/test/uncompressed.rs @@ -93,9 +93,10 @@ fn uncompressed() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".into() - }], + } + .into()], } }, stbl: Stbl { diff --git a/src/test/vp9.rs b/src/test/vp9.rs index 1033cccb..0eb3fbf3 100644 --- a/src/test/vp9.rs +++ b/src/test/vp9.rs @@ -124,9 +124,10 @@ fn vp9() { }), dinf: Dinf { dref: Dref { - urls: vec![Url { + entries: vec![Url { location: "".to_string() - }] + } + .into()] } }, stbl: Stbl { diff --git a/src/tokio/atom.rs b/src/tokio/atom.rs index 777ed961..26427e51 100644 --- a/src/tokio/atom.rs +++ b/src/tokio/atom.rs @@ -28,6 +28,10 @@ impl AsyncReadFrom for Option { None => return Ok(None), }; + if header.kind != T::KIND { + return Err(Error::UnexpectedBox(header.kind)); + } + let mut buf = header.read_body_tokio(r).await?; let atom = match T::decode_body(&mut buf) {