diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..42f9651d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mp4parse/av1-avif"] + path = mp4parse/av1-avif + url = https://github.com/AOMediaCodec/av1-avif.git diff --git a/.travis.yml b/.travis.yml index 7b4268c2..bbc2bd04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,11 @@ env: script: - cargo test --all --verbose $RELEASE + # We cannot run `cargo test` with the mp4parse_infallible feature enabled + # (see comment where the feature is defined in mp4parse_capi/Cargo.toml), + # but we can at least check for changes behind features that would break the + # build. Also note that we can't run `cargo check` before `cargo test` or + # else it breaks `build_ffi_test` for complicated reasons. + # See https://github.com/mozilla/mp4parse-rust/issues/197 + - cargo check --all --verbose $RELEASE --tests --all-features - cargo doc --package mp4parse_capi diff --git a/Cargo.toml b/Cargo.toml index db4aa844..58a8e014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,7 @@ overflow-checks = true [profile.bench] overflow-checks = true + +# Uncomment below to test local changes to mp4parse_fallible crate +# [patch.crates-io] +# mp4parse_fallible = { path = "../mp4parse_fallible" } diff --git a/mp4parse/Cargo.toml b/mp4parse/Cargo.toml index 99bd8b4d..645c9b15 100644 --- a/mp4parse/Cargo.toml +++ b/mp4parse/Cargo.toml @@ -5,6 +5,7 @@ authors = [ "Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", + "Jon Bauman ", ] description = "Parser for ISO base media file format (mp4)" @@ -24,16 +25,13 @@ travis-ci = { repository = "https://github.com/mozilla/mp4parse-rust" } [dependencies] byteorder = "1.2.1" -afl = { version = "0.3", optional = true } abort_on_panic = { version = "1.0.0", optional = true } -bitreader = { version = "0.3.0" } +bitreader = { version = "0.3.2" } num-traits = "0.2.0" -mp4parse_fallible = { version = "0.0.1", optional = true } +mp4parse_fallible = { version = "0.0.3", optional = true } log = "0.4" static_assertions = "1.1.0" [dev-dependencies] test-assembler = "0.1.2" - -[features] -fuzz = ["afl", "abort_on_panic"] +env_logger = "0.7.1" diff --git a/mp4parse/av1-avif b/mp4parse/av1-avif new file mode 160000 index 00000000..560a9730 --- /dev/null +++ b/mp4parse/av1-avif @@ -0,0 +1 @@ +Subproject commit 560a9730391791b199859e5c7db2a85d186c13aa diff --git a/mp4parse/src/boxes.rs b/mp4parse/src/boxes.rs index 92e906df..7e8e528b 100644 --- a/mp4parse/src/boxes.rs +++ b/mp4parse/src/boxes.rs @@ -94,6 +94,11 @@ impl fmt::Display for FourCC { box_database!( FileTypeBox 0x6674_7970, // "ftyp" + MediaDataBox 0x6d64_6174, // "mdat" + PrimaryItemBox 0x7069_746d, // "pitm" + ItemInfoBox 0x6969_6e66, // "iinf" + ItemInfoEntry 0x696e_6665, // "infe" + ItemLocationBox 0x696c_6f63, // "iloc" MovieBox 0x6d6f_6f76, // "moov" MovieHeaderBox 0x6d76_6864, // "mvhd" TrackBox 0x7472_616b, // "trak" @@ -135,7 +140,7 @@ box_database!( ProtectionSystemSpecificHeaderBox 0x7073_7368, // "pssh" SchemeInformationBox 0x7363_6869, // "schi" TrackEncryptionBox 0x7465_6e63, // "tenc" - ProtectionSchemeInformationBox 0x7369_6e66, // "sinf" + ProtectionSchemeInfoBox 0x7369_6e66, // "sinf" OriginalFormatBox 0x6672_6d61, // "frma" SchemeTypeBox 0x7363_686d, // "schm" MP3AudioSampleEntry 0x2e6d_7033, // ".mp3" - from F4V. @@ -149,7 +154,6 @@ box_database!( MetadataItemListEntry 0x696c_7374, // "ilst" MetadataItemDataEntry 0x6461_7461, // "data" MetadataItemNameBox 0x6e61_6d65, // "name" - MetadataItemInformationBox 0x6974_6966, // "itif" UserdataBox 0x7564_7461, // "udta" AlbumEntry 0xa961_6c62, // "©alb" ArtistEntry 0xa941_5254, // "©ART" diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 4c4d2f17..195bbfa5 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -3,8 +3,6 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -#[cfg(feature = "fuzz")] -extern crate afl; #[macro_use] extern crate log; @@ -14,9 +12,10 @@ extern crate bitreader; extern crate num_traits; use byteorder::{ReadBytesExt, WriteBytesExt}; use bitreader::{BitReader, ReadInto}; -use std::convert::TryInto as _; +use std::convert::{TryFrom, TryInto as _}; use std::io::{Read, Take}; use std::io::Cursor; +use std::ops::{Range, RangeFrom}; use num_traits::Num; #[cfg(feature = "mp4parse_fallible")] @@ -36,9 +35,9 @@ use boxes::{BoxType, FourCC}; mod tests; // Arbitrary buffer size limit used for raw read_bufs on a box. -const BUF_SIZE_LIMIT: usize = 10 * 1024 * 1024; +const BUF_SIZE_LIMIT: u64 = 10 * 1024 * 1024; -// Max table length. Calculating in worth case for one week long video, one +// Max table length. Calculating in worst case for one week long video, one // frame per table entry in 30 fps. const TABLE_SIZE_LIMIT: u32 = 30 * 60 * 60 * 24 * 7; @@ -82,6 +81,37 @@ impl_to_usize_from!(u8); impl_to_usize_from!(u16); impl_to_usize_from!(u32); +/// Indicate the current offset (i.e., bytes already read) in a reader +trait Offset { + fn offset(&self) -> u64; +} + +/// Wraps a reader to track the current offset +struct OffsetReader<'a, T: 'a> { + reader: &'a mut T, + offset: u64, +} + +impl<'a, T> OffsetReader<'a, T> { + fn new(reader: &'a mut T) -> Self { + Self { reader, offset: 0 } + } +} + +impl<'a, T> Offset for OffsetReader<'a, T> { + fn offset(&self) -> u64 { + self.offset + } +} + +impl<'a, T: Read> Read for OffsetReader<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.read(buf)?; + self.offset = self.offset.checked_add(bytes_read.to_u64()).expect("total bytes read too large for offset type"); + Ok(bytes_read) + } +} + // TODO: vec_push() needs to be replaced when Rust supports fallible memory // allocation in raw_vec. #[allow(unreachable_code)] @@ -95,17 +125,58 @@ pub fn vec_push(vec: &mut Vec, val: T) -> std::result::Result<(), ()> { Ok(()) } -#[allow(unreachable_code)] -fn allocate_read_buf(size: usize) -> std::result::Result, ()> { +fn vec_with_capacity(capacity: usize) -> std::result::Result, ()> { #[cfg(feature = "mp4parse_fallible")] { - let mut buf: Vec = Vec::new(); - FallibleVec::try_reserve(&mut buf, size)?; - buf.extend(std::iter::repeat(0).take(size)); - return Ok(buf); + let mut v = Vec::new(); + FallibleVec::try_reserve(&mut v, capacity)?; + Ok(v) } + #[cfg(not(feature = "mp4parse_fallible"))] + { + Ok(Vec::with_capacity(capacity)) + } +} + +pub fn extend_from_slice(vec: &mut Vec, other: &[T]) -> std::result::Result<(), ()> { + #[cfg(feature = "mp4parse_fallible")] + { + FallibleVec::try_extend_from_slice(vec, other) + } + #[cfg(not(feature = "mp4parse_fallible"))] + { + vec.extend_from_slice(other); + Ok(()) + } +} - Ok(vec![0; size]) +/// With the `mp4parse_fallible` feature enabled, this function reserves the +/// upper limit of what `src` can generate before reading all bytes until EOF +/// in this source, placing them into buf. If the allocation is unsuccessful, +/// or reading from the source generates an error before reaching EOF, this +/// will return an error. Otherwise, it will return the number of bytes read. +/// +/// Since `src.limit()` may return a value greater than the number of bytes +/// which can be read from the source, it's possible this function may fail +/// in the allocation phase even though allocating the number of bytes available +/// to read would have succeeded. In general, it is assumed that the callers +/// have accurate knowledge of the number of bytes of interest and have created +/// `src` accordingly. +/// +/// With the `mp4parse_fallible` feature disabled, this is wrapper around +/// `std::io::Read::read_to_end()`. +fn read_to_end(src: &mut Take, buf: &mut Vec) -> std::result::Result { + #[cfg(feature = "mp4parse_fallible")] + { + let limit: usize = src.limit().try_into().map_err(|_| ())?; + FallibleVec::try_reserve(buf, limit)?; + let bytes_read = src.read_to_end(buf).map_err(|_| ())?; + Ok(bytes_read) + } + #[cfg(not(feature = "mp4parse_fallible"))] + { + src.read_to_end(buf).map_err(|_| ()) + } } /// Describes parser failures. @@ -689,6 +760,180 @@ impl MediaContext { } } +#[derive(Debug, Default)] +pub struct AvifContext { + /// The collected data indicated by the `pitm` box, See ISO 14496-12:2015 § 8.11.4 + pub primary_item: Vec, +} + +impl AvifContext { + pub fn new() -> Self { + Default::default() + } +} + +/// A Media Data Box +/// See ISO 14496-12:2015 § 8.1.1 +struct MediaDataBox { + /// Offset of `data` from the beginning of the file. See ConstructionMethod::FileOffset + offset: u64, + data: Vec, +} + +impl MediaDataBox { + /// Check whether the beginning of `extent` is within the bounds of the `MediaDataBox`. + /// We assume extents to not cross box boundaries. If so, this will cause an error + /// in `read_extent`. + fn contains_extent(&self, extent: &ExtentRange) -> bool { + if self.offset <= extent.start() { + let start_offset = extent.start() - self.offset; + start_offset < self.data.len().to_u64() + } else { + false + } + } + + /// Check whether `extent` covers the `MediaDataBox` exactly. + fn matches_extent(&self, extent: &ExtentRange) -> bool { + if self.offset == extent.start() { + match extent { + ExtentRange::WithLength(range) => { + if let Some(end) = self.offset.checked_add(self.data.len().to_u64()) { + end == range.end + } else { + false + } + } + ExtentRange::ToEnd(_) => true, + } + } else { + false + } + } + + /// Copy the range specified by `extent` to the end of `buf` or return an error if the range + /// is not fully contained within `MediaDataBox`. + fn read_extent(&mut self, extent: &ExtentRange, buf: &mut Vec) -> Result<()> { + let start_offset = extent.start().checked_sub(self.offset).expect("mdat does not contain extent"); + let slice = match extent { + ExtentRange::WithLength(range) => { + let range_len = range.end.checked_sub(range.start).expect("range start > end"); + let end = start_offset.checked_add(range_len).expect("extent end overflow"); + self.data.get(start_offset.try_into()?..end.try_into()?) + } + ExtentRange::ToEnd(_) => { + self.data.get(start_offset.try_into()?..) + } + }; + let slice = slice.ok_or(Error::InvalidData("extent crosses box boundary"))?; + extend_from_slice(buf, slice)?; + Ok(()) + } + +} + +/// Used for 'infe' boxes within 'iinf' boxes +/// See ISO 14496-12:2015 § 8.11.6 +/// Only versions {2, 3} are supported +#[derive(Debug)] +struct ItemInfoEntry { + item_id: u32, + item_type: u32, +} + +/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box +/// See ISO 14496-12:2015 § 8.11.3 +enum IlocFieldSize { + Zero, + Four, + Eight, +} + +impl IlocFieldSize { + fn to_bits(&self) -> u8 { + match self { + IlocFieldSize::Zero => 0, + IlocFieldSize::Four => 32, + IlocFieldSize::Eight => 64, + } + } +} + +impl TryFrom for IlocFieldSize { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 4 => Ok(Self::Four), + 8 => Ok(Self::Eight), + _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")), + } + } +} + +#[derive(PartialEq)] +enum IlocVersion { + Zero, + One, + Two, +} + +impl TryFrom for IlocVersion { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 1 => Ok(Self::One), + 2 => Ok(Self::Two), + _ => Err(Error::Unsupported("unsupported version in 'iloc' box")), + } + } +} + +/// Used for 'iloc' boxes +/// See ISO 14496-12:2015 § 8.11.3 +/// `base_offset` is omitted since it is integrated into the ranges in `extents` +/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported +#[derive(Clone, Debug)] +struct ItemLocationBoxItem { + item_id: u32, + construction_method: ConstructionMethod, + /// Unused for ConstructionMethod::IdatOffset + extents: Vec, +} + +#[derive(Clone, Copy, Debug)] +enum ConstructionMethod { + FileOffset, + IdatOffset, + #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196 + ItemOffset, +} + +/// `extent_index` is omitted since it's only used for ConstructionMethod::ItemOffset which +/// is currently not implemented. +#[derive(Clone, Debug)] +struct ItemLocationBoxExtent { + extent_range: ExtentRange, +} + +#[derive(Clone, Debug)] +enum ExtentRange { + WithLength(Range), + ToEnd(RangeFrom), +} + +impl ExtentRange { + fn start(&self) -> u64 { + match self { + Self::WithLength(r) => r.start, + Self::ToEnd(r) => r.start + } + } +} + #[derive(Debug, PartialEq)] pub enum TrackType { Audio, @@ -807,6 +1052,12 @@ impl<'a, T: Read> Read for BMFFBox<'a, T> { } } +impl<'a, T: Offset> Offset for BMFFBox<'a, T> { + fn offset(&self) -> u64 { + self.content.get_ref().offset() + } +} + impl<'a, T: Read> BMFFBox<'a, T> { fn bytes_left(&self) -> u64 { self.content.limit() @@ -821,6 +1072,40 @@ impl<'a, T: Read> BMFFBox<'a, T> { } } +impl<'a, T: Read + Offset> BMFFBox<'a, T> { + /// Check whether the beginning of `extent` is within the bounds of the `BMFFBox`. + /// We assume extents to not cross box boundaries. If so, this will cause an error + /// in `read_extent`. + fn contains_extent(&self, extent: &ExtentRange) -> bool { + if self.offset() <= extent.start() { + let start_offset = extent.start() - self.offset(); + start_offset < self.bytes_left() + } else { + false + } + } + + /// Read the range specified by `extent` into `buf` or return an error if the range is not + /// fully contained within the `BMFFBox`. + fn read_extent(&mut self, extent: &ExtentRange, buf: &mut Vec) -> Result<()> { + let start_offset = extent.start().checked_sub(self.offset()).expect("box does not contain extent"); + skip(self, start_offset)?; + match extent { + ExtentRange::WithLength(range) => { + let len = range.end.checked_sub(range.start).expect("range start > end"); + if len > self.bytes_left() { + return Err(Error::InvalidData("extent crosses box boundary")); + } + read_to_end(&mut self.take(len), buf)?; + } + ExtentRange::ToEnd(_) => { + read_to_end(&mut self.take(self.bytes_left()), buf)?; + } + } + Ok(()) + } +} + impl<'a, T> Drop for BMFFBox<'a, T> { fn drop(&mut self) { if self.content.limit() > 0 { @@ -893,6 +1178,17 @@ fn read_fullbox_extra(src: &mut T) -> Result<(u8, u32)> { u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c))) } +// Parse the extra fields for a full box whose flag fields must be zero. +fn read_fullbox_version_no_flags(src: &mut T) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + + if flags != 0 { + return Err(Error::Unsupported("expected flags to be 0")); + } + + Ok(version) +} + /// Skip over the entire contents of a box. fn skip_box_content(src: &mut BMFFBox) -> Result<()> { // Skip the contents of unknown chunks. @@ -916,6 +1212,340 @@ fn skip_box_remain(src: &mut BMFFBox) -> Result<()> { skip(src, remain) } +/// Read the contents of an AVIF file +/// +/// Metadata is accumulated in the passed-through `AvifContext` struct, +/// which can be examined later. +pub fn read_avif(f: &mut T, context: &mut AvifContext) -> Result<()> { + let mut f = OffsetReader::new(f); + + let mut iter = BoxIter::new(&mut f); + + // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1 + if let Some(mut b) = iter.next_box()? { + if b.head.name == BoxType::FileTypeBox { + let ftyp = read_ftyp(&mut b)?; + if !ftyp.compatible_brands.contains(&FourCC::from("mif1")) { + return Err(Error::InvalidData("compatible_brands must contain 'mif1'")); + } + } else { + return Err(Error::InvalidData("'ftyp' box must occur first")) + } + } + + let mut read_meta = false; + let mut mdats = vec![]; + let mut primary_item_extents = None; + let mut primary_item_extents_data: Vec> = vec![]; + + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MetadataBox => { + if read_meta { + return Err(Error::InvalidData("There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1")); + } + read_meta = true; + let primary_item_loc = read_avif_meta(&mut b)?; + match primary_item_loc.construction_method { + ConstructionMethod::FileOffset => { + primary_item_extents = Some(primary_item_loc.extents); + primary_item_extents_data = primary_item_extents.iter().map(|_| vec![]).collect(); + } + _ => return Err(Error::Unsupported("unsupported construction_method")), + } + } + BoxType::MediaDataBox => { // See ISO 14496-12:2015 § 8.1.1 + // If we know our primary item location by this point, try to read it out of this + // mdat directly and avoid a copy + if let Some(extents) = &primary_item_extents { + for (extent, data) in extents.iter().zip(primary_item_extents_data.iter_mut()) { + if b.contains_extent(&extent.extent_range) { + b.read_extent(&extent.extent_range, data)?; + } + } + } + + // Store any remaining data for potential later extraction + if b.bytes_left() > 0 { + let offset = b.offset(); + let mut data = vec_with_capacity(b.bytes_left().try_into()?)?; + b.read_to_end(&mut data)?; + vec_push(&mut mdats, MediaDataBox { offset, data })?; + } + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + // If the `mdat` box came before the `meta` box, we need to fill in our primary item data + let primary_item_extents = primary_item_extents.ok_or(Error::InvalidData("primary item extents missing"))?; + for (extent, data) in primary_item_extents.iter().zip(primary_item_extents_data.iter_mut()) { + if data.is_empty() { + // try to find an overlapping mdat + for mdat in &mut mdats { + if mdat.matches_extent(&extent.extent_range) { + data.append(&mut mdat.data) + } else if mdat.contains_extent(&extent.extent_range) { + mdat.read_extent(&extent.extent_range, data)?; + } + } + } + } + + context.primary_item = primary_item_extents_data.into_iter().flatten().collect(); + + Ok(()) +} + +/// Parse a metadata box in the context of an AVIF +/// Currently requires the primary item to be an av01 item type and generates +/// an error otherwise. +/// See ISO 14496-12:2015 § 8.11.1 +fn read_avif_meta(src: &mut BMFFBox) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + if version != 0 { + return Err(Error::Unsupported("unsupported meta version")) + } + + let mut primary_item_id = None; + let mut item_infos = None; + let mut iloc_items = None; + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::ItemInfoBox => { + if item_infos.is_some() { + return Err(Error::InvalidData("There should be zero or one iinf boxes per ISO 14496-12:2015 § 8.11.6.1")); + } + item_infos = Some(read_iinf(&mut b)?); + } + BoxType::ItemLocationBox => { + if iloc_items.is_some() { + return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.3.1")); + } + iloc_items = Some(read_iloc(&mut b)?); + } + BoxType::PrimaryItemBox => { + if primary_item_id.is_some() { + return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.4.1")); + } + primary_item_id = Some(read_pitm(&mut b)?); + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + let primary_item_id = primary_item_id.ok_or(Error::InvalidData("Required pitm box not present in meta box"))?; + + if let Some(item_info) = item_infos.iter().flatten().find(|x| x.item_id == primary_item_id) { + if &item_info.item_type.to_be_bytes() != b"av01" { + warn!("primary_item_id type: {}", be_u32_to_string(item_info.item_type)); + return Err(Error::InvalidData("primary_item_id type is not av01")); + } + } else { + return Err(Error::InvalidData("primary_item_id not present in iinf box")); + } + + if let Some(loc) = iloc_items.iter().flatten().find(|loc| loc.item_id == primary_item_id) { + Ok(loc.clone()) + } else { + Err(Error::InvalidData("primary_item_id not present in iloc box")) + } +} + +/// Parse a Primary Item Box +/// See ISO 14496-12:2015 § 8.11.4 +fn read_pitm(src: &mut BMFFBox) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + let item_id = match version { + 0 => be_u16(src)?.into(), + 1 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported pitm version")), + }; + + Ok(item_id) +} + +/// Parse an Item Information Box +/// See ISO 14496-12:2015 § 8.11.6 +fn read_iinf(src: &mut BMFFBox) -> Result> { + let version = read_fullbox_version_no_flags(src)?; + + match version { + 0 | 1 => (), + _ => return Err(Error::Unsupported("unsupported iinf version")), + } + + let entry_count = if version == 0 { + be_u16(src)?.to_usize() + } else { + be_u32(src)?.to_usize() + }; + let mut item_infos = vec_with_capacity(entry_count)?; + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + if b.head.name != BoxType::ItemInfoEntry { + return Err(Error::InvalidData("iinf box should contain only infe boxes")); + } + + vec_push(&mut item_infos, read_infe(&mut b)?)?; + + check_parser_state!(b.content); + } + + Ok(item_infos) +} + +fn be_u32_to_string(src: u32) -> String { + String::from_utf8(src.to_be_bytes().to_vec()).unwrap_or(format!("{:x?}", src)) +} + +/// Parse an Item Info Entry +/// See ISO 14496-12:2015 § 8.11.6.2 +fn read_infe(src: &mut BMFFBox) -> Result { + // According to the standard, it seems the flags field should be 0, but + // at least one sample AVIF image has a nonzero value. + let (version, _) = read_fullbox_extra(src)?; + + // mif1 brand (see ISO 23008-12:2017 § 10.2.1) only requires v2 and 3 + let item_id = match version { + 2 => be_u16(src)?.into(), + 3 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported version in 'infe' box")) + }; + + let item_protection_index = be_u16(src)?; + + if item_protection_index != 0 { + return Err(Error::Unsupported("protected items (infe.item_protection_index != 0) are not supported")); + } + + let item_type = be_u32(src)?; + debug!("infe item_id {} item_type: {}", item_id, be_u32_to_string(item_type)); + + // There are some additional fields here, but they're not of interest to us + skip_box_remain(src)?; + + Ok(ItemInfoEntry { item_id, item_type }) +} + +/// Parse an item location box inside a meta box +/// See ISO 14496-12:2015 § 8.11.3 +fn read_iloc(src: &mut BMFFBox) -> Result> { + let version: IlocVersion = read_fullbox_version_no_flags(src)?.try_into()?; + + let mut iloc = vec_with_capacity(src.bytes_left().try_into()?)?; + src.read_to_end(&mut iloc)?; + let mut iloc = BitReader::new(iloc.as_slice()); + + let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + + let index_size: Option = match version { + IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?), + IlocVersion::Zero => { + let _reserved = iloc.read_u8(4)?; + None + } + }; + + let item_count = match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }; + + let mut items = vec_with_capacity(item_count.to_usize())?; + + for _ in 0..item_count { + let item_id = match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }; + + // The spec isn't entirely clear how an `iloc` should be interpreted for version 0, + // which has no `construction_method` field. It does say: + // "For maximum compatibility, version 0 of this box should be used in preference to + // version 1 with `construction_method==0`, or version 2 when possible." + // We take this to imply version 0 can be interpreted as using file offsets. + let construction_method = match version { + IlocVersion::Zero => ConstructionMethod::FileOffset, + IlocVersion::One | IlocVersion::Two => { + let _reserved = iloc.read_u16(12)?; + match iloc.read_u16(4)? { + 0 => ConstructionMethod::FileOffset, + 1 => ConstructionMethod::IdatOffset, + 2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")), + _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISO 14496-12:2015 § 8.11.3.3")) + } + } + }; + + let data_reference_index = iloc.read_u16(16)?; + + if data_reference_index != 0 { + return Err(Error::Unsupported("external file references (iloc.data_reference_index != 0) are not supported")); + } + + let base_offset = iloc.read_u64(base_offset_size.to_bits())?; + let extent_count = iloc.read_u16(16)?; + + if extent_count < 1 { + return Err(Error::InvalidData("extent_count must have a value 1 or greater per ISO 14496-12:2015 § 8.11.3.3")); + } + + let mut extents = vec_with_capacity(extent_count.to_usize())?; + + for _ in 0..extent_count { + // Parsed but currently ignored, see `ItemLocationBoxExtent` + let _extent_index = match &index_size { + None | Some(IlocFieldSize::Zero) => None, + Some(index_size) => { + debug_assert!(version == IlocVersion::One || version == IlocVersion::Two); + Some(iloc.read_u64(index_size.to_bits())?) + } + }; + + // Per ISO 14496-12:2015 § 8.11.3.1: + // "If the offset is not identified (the field has a length of zero), then the + // beginning of the source (offset 0) is implied" + // This behavior will follow from BitReader::read_u64(0) -> 0. + let extent_offset = iloc.read_u64(offset_size.to_bits())?; + let extent_length = iloc.read_u64(length_size.to_bits())?; + + // "If the length is not specified, or specified as zero, then the entire length of + // the source is implied" (ibid) + let start = base_offset.checked_add(extent_offset).ok_or(Error::InvalidData("offset calculation overflow"))?; + let extent_range = if extent_length == 0 { + ExtentRange::ToEnd(RangeFrom { start }) + } else { + let end = start.checked_add(extent_length).ok_or(Error::InvalidData("end calculation overflow"))?; + ExtentRange::WithLength(Range { start, end }) + }; + + vec_push(&mut extents, ItemLocationBoxExtent { extent_range })?; + } + + vec_push(&mut items, ItemLocationBoxItem { + item_id, + construction_method, + extents + })?; + } + + debug_assert_eq!(iloc.remaining(), 0); + + Ok(items) +} + /// Read the contents of a box, including sub boxes. /// /// Metadata is accumulated in the passed-through `MediaContext` struct, @@ -1020,7 +1650,7 @@ fn read_moov(f: &mut BMFFBox, context: &mut MediaContext) -> Result< } fn read_pssh(src: &mut BMFFBox) -> Result { - let len = src.bytes_left().try_into()?; + let len = src.bytes_left(); let mut box_content = read_buf(src, len)?; let (system_id, kid, data) = { let pssh = &mut Cursor::new(box_content.as_slice()); @@ -1038,8 +1668,8 @@ fn read_pssh(src: &mut BMFFBox) -> Result(src: &mut BMFFBox) -> Result { }; let codec_init_size = be_u16(src)?; - let codec_init = read_buf(src, codec_init_size.to_usize())?; + let codec_init = read_buf(src, codec_init_size.into())?; // TODO(rillian): validate field value ranges. Ok(VPxConfigBox { @@ -1682,7 +2312,7 @@ fn read_av1c(src: &mut BMFFBox) -> Result { }; let config_obus_size = src.bytes_left(); - let config_obus = read_buf(src, config_obus_size.try_into()?)?; + let config_obus = read_buf(src, config_obus_size)?; Ok(AV1ConfigBox { profile, @@ -1702,12 +2332,12 @@ fn read_av1c(src: &mut BMFFBox) -> Result { fn read_flac_metadata(src: &mut BMFFBox) -> Result { let temp = src.read_u8()?; let block_type = temp & 0x7f; - let length = be_u24(src)?; - if u64::from(length) > src.bytes_left() { + let length = be_u24(src)?.into(); + if length > src.bytes_left() { return Err(Error::InvalidData( "FLACMetadataBlock larger than parent box")); } - let data = read_buf(src, length.to_usize())?; + let data = read_buf(src, length)?; Ok(FLACMetadataBlock { block_type, data, @@ -1989,7 +2619,7 @@ fn read_esds(src: &mut BMFFBox) -> Result { // Subtract 4 extra to offset the members of fullbox not accounted for in // head.offset let esds_size = src.head.size.checked_sub(src.head.offset + 4).expect("offset invalid"); - let esds_array = read_buf(src, esds_size.try_into()?)?; + let esds_array = read_buf(src, esds_size)?; let mut es_data = ES_Descriptor::default(); find_descriptor(&esds_array, &mut es_data)?; @@ -2048,7 +2678,7 @@ fn read_dops(src: &mut BMFFBox) -> Result { } else { let stream_count = src.read_u8()?; let coupled_count = src.read_u8()?; - let channel_mapping = read_buf(src, output_channel_count.to_usize())?; + let channel_mapping = read_buf(src, output_channel_count.into())?; Some(ChannelMappingTable { stream_count, @@ -2123,7 +2753,7 @@ fn read_alac(src: &mut BMFFBox) -> Result { } let length = match src.bytes_left() { - x @ 24 | x @ 48 => x.try_into().expect("infallible conversion to usize"), + x @ 24 | x @ 48 => x, _ => return Err(Error::InvalidData("ALACSpecificBox magic cookie is the wrong size")), }; let data = read_buf(src, length)?; @@ -2198,7 +2828,7 @@ fn read_video_sample_entry(src: &mut BMFFBox) -> Result return Err(Error::InvalidData("malformed video sample entry")); } let avcc_size = b.head.size.checked_sub(b.head.offset).expect("offset invalid"); - let avcc = read_buf(&mut b.content, avcc_size.try_into()?)?; + let avcc = read_buf(&mut b.content, avcc_size)?; debug!("{:?} (avcc)", avcc); // TODO(kinetik): Parse avcC box? For now we just stash the data. codec_specific = Some(VideoCodecSpecific::AVCConfig(avcc)); @@ -2228,10 +2858,10 @@ fn read_video_sample_entry(src: &mut BMFFBox) -> Result // Subtract 4 extra to offset the members of fullbox not // accounted for in head.offset let esds_size = b.head.size.checked_sub(b.head.offset + 4).expect("offset invalid"); - let esds = read_buf(&mut b.content, esds_size.try_into()?)?; + let esds = read_buf(&mut b.content, esds_size)?; codec_specific = Some(VideoCodecSpecific::ESDSConfig(esds)); } - BoxType::ProtectionSchemeInformationBox => { + BoxType::ProtectionSchemeInfoBox => { if name != BoxType::ProtectedVisualSampleEntry { return Err(Error::InvalidData("malformed video sample entry")); } @@ -2371,7 +3001,7 @@ fn read_audio_sample_entry(src: &mut BMFFBox) -> Result codec_type = qt_esds.audio_codec; codec_specific = Some(AudioCodecSpecific::ES_Descriptor(qt_esds)); } - BoxType::ProtectionSchemeInformationBox => { + BoxType::ProtectionSchemeInfoBox => { if name != BoxType::ProtectedAudioSampleEntry { return Err(Error::InvalidData("malformed audio sample entry")); } @@ -2513,7 +3143,7 @@ fn read_tenc(src: &mut BMFFBox) -> Result { let default_constant_iv = match (default_is_encrypted, default_iv_size) { (1, 0) => { let default_constant_iv_size = src.read_u8()?; - Some(read_buf(src, default_constant_iv_size.to_usize())?) + Some(read_buf(src, default_constant_iv_size.into())?) }, _ => None, }; @@ -2696,7 +3326,7 @@ fn read_ilst_multiple_u8_data(src: &mut BMFFBox) -> Result { - data.push(read_ilst_data(&mut b)?); + vec_push(&mut data, read_ilst_data(&mut b)?)?; } _ => skip_box_content(&mut b)?, }; @@ -2708,7 +3338,7 @@ fn read_ilst_multiple_u8_data(src: &mut BMFFBox) -> Result(src: &mut BMFFBox) -> Result> { // Skip past the padding bytes skip(&mut src.content, src.head.offset)?; - let size = src.content.limit().try_into()?; + let size = src.content.limit(); read_buf(&mut src.content, size) } @@ -2719,19 +3349,18 @@ fn skip(src: &mut T, bytes: u64) -> Result<()> { } /// Read size bytes into a Vector or return error. -fn read_buf(src: &mut T, size: usize) -> Result> { +fn read_buf(src: &mut T, size: u64) -> Result> { if size > BUF_SIZE_LIMIT { return Err(Error::InvalidData("read_buf size exceeds BUF_SIZE_LIMIT")); } - if let Ok(mut buf) = allocate_read_buf(size) { - let r = src.read(&mut buf)?; - if r != size { - return Err(Error::InvalidData("failed buffer read")); - } - return Ok(buf); + + let mut buf = vec![]; + let r: u64 = read_to_end(&mut src.take(size), &mut buf)?.try_into()?; + if r != size { + return Err(Error::InvalidData("failed buffer read")); } - Err(Error::OutOfMemory) + Ok(buf) } fn be_i16(src: &mut T) -> Result { diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index 7e76f986..0736596b 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -6,6 +6,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::io::Cursor; +use std::io::Read as _; +#[cfg(feature = "mp4parse_fallible")] +use std::convert::TryInto as _; use super::read_mp4; use super::MediaContext; use super::Error; @@ -1341,3 +1344,20 @@ fn read_stsd_lpcm() { } } + +#[test] +fn read_to_end_() { + let mut src = b"1234567890".take(5); + let mut buf = vec![]; + let bytes_read = super::read_to_end(&mut src, &mut buf).unwrap(); + assert_eq!(bytes_read, 5); + assert_eq!(buf, b"12345"); +} + +#[test] +#[cfg(feature = "mp4parse_fallible")] +fn read_to_end_oom() { + let mut src = b"1234567890".take(std::usize::MAX.try_into().expect("usize < u64")); + let mut buf = vec![]; + assert!(super::read_to_end(&mut src, &mut buf).is_err()); +} diff --git a/mp4parse/tests/afl.rs b/mp4parse/tests/afl.rs deleted file mode 100644 index d4ffec0d..00000000 --- a/mp4parse/tests/afl.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Regression tests from American Fuzzy Lop test cases. - -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/// These all caused panics at some point during development. - -extern crate mp4parse; - -use std::io::Cursor; - -/// https://github.com/mozilla/mp4parse-rust/issues/2 -/// -/// Test a box with 4-byte size, smaller than the smallest header. -#[test] -fn fuzz_2() { - let mut c = Cursor::new(b"\x00\x00\x00\x04\xa6\x00\x04\xa6".to_vec()); - let mut context = mp4parse::MediaContext::new(); - let _ = mp4parse::read_mp4(&mut c, &mut context); -} - -/// https://github.com/mozilla/mp4parse-rust/issues/4 -/// -/// Test a large (64 bit) box header with zero declared size. -#[test] -fn fuzz_4() { - let mut c = Cursor::new(b"\x00\x00\x00\x01\x30\x30\x30\x30\x00\x00\x00\x00\x00\x00\x00\x00".to_vec()); - let mut context = mp4parse::MediaContext::new(); - let _ = mp4parse::read_mp4(&mut c, &mut context); -} - -/// https://github.com/mozilla/mp4parse-rust/issues/5 -/// -/// Declares 202116104 compatible brands but does not supply them, -/// verifying read is properly bounded at the end of the stream. -#[test] -fn fuzz_5() { - let mut c = Cursor::new(b"\x30\x30\x30\x30\x66\x74\x79\x70\x30\x30\x30\x30\x30\x30\x30\x30".to_vec()); - let mut context = mp4parse::MediaContext::new(); - let _ = mp4parse::read_mp4(&mut c, &mut context); -} - -/// https://github.com/mozilla/mp4parse-rust/issues/6 -/// -/// Declares an ftyp box with a single invalid (short - 3 byte) compatible -/// brand and excludes the extra 3 bytes from the stream. -#[test] -fn fuzz_6() { - let mut c = Cursor::new(b"\x00\x00\x00\x13\x66\x74\x79\x70\x30\x30\x30\x30\x30\x30\x30\x30".to_vec()); - let mut context = mp4parse::MediaContext::new(); - let _ = mp4parse::read_mp4(&mut c, &mut context); -} diff --git a/mp4parse/tests/public.rs b/mp4parse/tests/public.rs index 6bd33987..1bf97e9d 100644 --- a/mp4parse/tests/public.rs +++ b/mp4parse/tests/public.rs @@ -8,6 +8,7 @@ extern crate mp4parse as mp4; use std::io::{Cursor, Read}; use std::fs::File; +use std::path::Path; static MINI_MP4: &str = "tests/minimal.mp4"; static MINI_MP4_WITH_METADATA: &str = "tests/metadata.mp4"; @@ -27,6 +28,10 @@ static VIDEO_EME_CENC_MP4: &str = "tests/bipbop_480wp_1001kbps-cenc-video-key1-i static AUDIO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_audio_init.mp4"; static VIDEO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_video_init.mp4"; static VIDEO_AV1_MP4: &str = "tests/tiny_av1.mp4"; +static IMAGE_AVIF: &str = "av1-avif/testFiles/Microsoft/Monochrome.avif"; +static IMAGE_AVIF_GRID: &str = "av1-avif/testFiles/Microsoft/Summer_in_Tomsk_720p_5x4_grid.avif"; +static MICROSOFT_AVIF_TEST_DIR: &str = "av1-avif/testFiles/Microsoft"; +static NETFLIX_AVIF_TEST_DIR: &str = "av1-avif/testFiles/Netflix/avif"; // Adapted from https://github.com/GuillaumeGomez/audio-video-metadata/blob/9dff40f565af71d5502e03a2e78ae63df95cfd40/src/metadata.rs#L53 #[test] @@ -594,3 +599,42 @@ fn public_video_av1() { } } } + +#[test] +fn public_avif_primary_item() { + let context = &mut mp4::AvifContext::new(); + let input = &mut File::open(IMAGE_AVIF).expect("Unknown file"); + mp4::read_avif(input, context).expect("read_avif failed"); + assert_eq!(context.primary_item.len(), 6979); + assert_eq!(context.primary_item[0..4], [0x12, 0x00, 0x0a, 0x0a]); +} + +#[test] +#[ignore] // Remove when we add support; see https://github.com/mozilla/mp4parse-rust/issues/198 +fn public_avif_primary_item_is_grid() { + let context = &mut mp4::AvifContext::new(); + let input = &mut File::open(IMAGE_AVIF_GRID).expect("Unknown file"); + mp4::read_avif(input, context).expect("read_avif failed"); + // Add some additional checks +} + +#[test] +fn public_avif_read_samples() { + env_logger::init(); + let microsoft = Path::new(MICROSOFT_AVIF_TEST_DIR).read_dir().expect("Cannot read AVIF test dir"); + let netflix = Path::new(NETFLIX_AVIF_TEST_DIR).read_dir().expect("Cannot read AVIF test dir"); + for entry in microsoft.chain(netflix) { + let path = entry.expect("AVIF entry").path(); + if path.extension().expect("no extension") != "avif" { + eprintln!("Skipping {:?}", path); + continue; // Skip ReadMe.txt, etc. + } + if path == Path::new(IMAGE_AVIF_GRID) { + eprintln!("Skipping {:?}", path); + continue; // Remove when public_avif_primary_item_is_grid passes + } + let context = &mut mp4::AvifContext::new(); + let input = &mut File::open(path).expect("Unknow file"); + mp4::read_avif(input, context).expect("read_avif failed"); + } +} diff --git a/mp4parse_capi/Cargo.toml b/mp4parse_capi/Cargo.toml index 395e3e0b..d3b073d2 100644 --- a/mp4parse_capi/Cargo.toml +++ b/mp4parse_capi/Cargo.toml @@ -34,7 +34,6 @@ env_logger = "0.5.3" cbindgen = "0.5.2" [features] -fuzz = ["mp4parse/fuzz"] # Enable mp4parse_fallible to use fallible memory allocation rather than # panicking on OOM. Note that this is only safe within Gecko where the system # allocator has been globally overridden (see BMO 1457359). diff --git a/mp4parse_capi/examples/afl-capi.rs b/mp4parse_capi/examples/afl-capi.rs deleted file mode 100644 index 01a5d966..00000000 --- a/mp4parse_capi/examples/afl-capi.rs +++ /dev/null @@ -1,105 +0,0 @@ -extern crate mp4parse_capi; - -use mp4parse_capi::*; - -#[cfg(feature = "fuzz")] -#[macro_use] -extern crate abort_on_panic; - -use std::io::Read; - -extern fn vec_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { - let input: &mut std::io::Cursor> = unsafe { &mut *(userdata as *mut _) }; - - let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; - match input.read(&mut buf) { - Ok(n) => n as isize, - Err(_) => -1, - } -} - -fn doit() { - let mut input = Vec::new(); - std::io::stdin().read_to_end(&mut input).unwrap(); - let mut cursor = std::io::Cursor::new(input); - let io = Mp4parseIo { read: Some(vec_read), userdata: &mut cursor as *mut _ as *mut std::os::raw::c_void }; - unsafe { - let context = mp4parse_new(&io); - let rv = mp4parse_read(context); - if rv == Mp4parseStatus::Ok { - let count = { - let mut count = 0; - let rv = mp4parse_get_track_count(context, &mut count); - assert_eq!(rv, Mp4parseStatus::Ok); - count - }; - for track in 0..count { - let mut info = Mp4parseTrackInfo { - track_type: Mp4parseTrackType::Video, - track_id: 0, - duration: 0, - media_time: 0, - }; - let rv = mp4parse_get_track_info(context, track, &mut info); - if rv == Mp4parseStatus::Ok { - println!("track {}: id={} duration={} media_time={}", - track, info.track_id, info.duration, info.media_time); - match info.track_type { - Mp4parseTrackType::Video => { - let mut video = Mp4parseTrackVideoInfo::default(); - let rv = mp4parse_get_track_video_info(context, track, &mut video); - if rv == Mp4parseStatus::Ok { - println!(" video: display={}x{} rotation={}", - video.display_width, - video.display_height, - video.rotation); - for i in 0 .. video.sample_info_count as isize { - let info = &*video.sample_info.offset(i); - println!(" sample info[{}]: codec={:?} image={}x{}", - i, - info.codec_type, - info.image_width, - info.image_height); - } - } - } - Mp4parseTrackType::Audio => { - let mut audio = Default::default(); - let rv = mp4parse_get_track_audio_info(context, track, &mut audio); - if rv == Mp4parseStatus::Ok { - println!(" audio:"); - for i in 0 .. audio.sample_info_count as isize { - let info = &*audio.sample_info.offset(i); - println!(" sample info[{}]: codec={:?} channels={} \ - bit depth={} sample rate={} profile={}", - i, - info.codec_type, - info.channels, - info.bit_depth, - info.sample_rate, - info.profile); - } - } - } - Mp4parseTrackType::Metadata => { - println!(" metadata found (TODO)"); - } - } - } - } - } - mp4parse_free(context); - } -} - -#[cfg(feature = "fuzz")] -fn main() { - abort_on_panic!({ - doit(); - }); -} - -#[cfg(not(feature = "fuzz"))] -fn main() { - doit(); -} diff --git a/mp4parse_capi/examples/afl.rs b/mp4parse_capi/examples/afl.rs deleted file mode 100644 index 942d9d04..00000000 --- a/mp4parse_capi/examples/afl.rs +++ /dev/null @@ -1,27 +0,0 @@ -extern crate mp4parse; - -#[cfg(feature = "fuzz")] -#[macro_use] -extern crate abort_on_panic; - -use std::io::{Cursor, Read}; - -fn doit() -> bool { - let mut input = Vec::new(); - std::io::stdin().read_to_end(&mut input).unwrap(); - let mut reader = Cursor::new(&input); - let mut context = mp4parse::MediaContext::new(); - mp4parse::read_mp4(&mut reader, &mut context).is_ok() -} - -#[cfg(feature = "fuzz")] -fn main() { - abort_on_panic!({ - doit(); - }); -} - -#[cfg(not(feature = "fuzz"))] -fn main() { - doit(); -} diff --git a/mp4parse_capi/fuzz/Cargo.toml b/mp4parse_capi/fuzz/Cargo.toml index c1288ebc..2ebaa094 100644 --- a/mp4parse_capi/fuzz/Cargo.toml +++ b/mp4parse_capi/fuzz/Cargo.toml @@ -21,3 +21,7 @@ members = ["."] [[bin]] name = "fuzz_target_1" path = "fuzz_targets/fuzz_target_1.rs" + +[[bin]] +name = "avif" +path = "fuzz_targets/avif.rs" diff --git a/mp4parse_capi/fuzz/fuzz_targets/avif.rs b/mp4parse_capi/fuzz/fuzz_targets/avif.rs new file mode 100644 index 00000000..934ad210 --- /dev/null +++ b/mp4parse_capi/fuzz/fuzz_targets/avif.rs @@ -0,0 +1,39 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use mp4parse_capi::*; +use std::convert::TryInto; +use std::io::Read; + +type CursorType<'a> = std::io::Cursor<&'a [u8]>; + +extern "C" fn vec_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut CursorType = unsafe { &mut *(userdata as *mut _) }; + + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n.try_into().expect("invalid conversion"), + Err(_) => -1, + } +} + +fuzz_target!(|data: &[u8]| { + let mut cursor: CursorType = std::io::Cursor::new(data); + let io = Mp4parseIo { + read: Some(vec_read), + userdata: &mut cursor as *mut _ as *mut std::os::raw::c_void, + }; + unsafe { + let context = mp4parse_avif_new(&io); + + if mp4parse_avif_read(context) != Mp4parseStatus::Ok { + mp4parse_avif_free(context); + return; + } + + let mut primary_item = Default::default(); + mp4parse_avif_get_primary_item(context, &mut primary_item); + + mp4parse_avif_free(context); + } +}); diff --git a/mp4parse_capi/fuzz/mp4.dict b/mp4parse_capi/fuzz/mp4.dict index 2b35309a..cebd28bb 100644 --- a/mp4parse_capi/fuzz/mp4.dict +++ b/mp4parse_capi/fuzz/mp4.dict @@ -1,4 +1,9 @@ ftyp="\x66\x74\x79\x70" +mdat="\x6d\x64\x61\x74" +pitm="\x70\x69\x74\x6d" +iinf="\x69\x69\x6e\x66" +infe="\x69\x6e\x66\x65" +iloc="\x69\x6c\x6f\x63" moov="\x6d\x6f\x6f\x76" mvhd="\x6d\x76\x68\x64" trak="\x74\x72\x61\x6b" diff --git a/mp4parse_capi/src/lib.rs b/mp4parse_capi/src/lib.rs index bb2aa45a..ec686884 100644 --- a/mp4parse_capi/src/lib.rs +++ b/mp4parse_capi/src/lib.rs @@ -45,8 +45,10 @@ use num_traits::{PrimInt, Zero}; // Symbols we need from our rust api. use mp4parse::MediaContext; +use mp4parse::AvifContext; use mp4parse::TrackType; use mp4parse::read_mp4; +use mp4parse::read_avif; use mp4parse::Error; use mp4parse::SampleEntry; use mp4parse::AudioCodecSpecific; @@ -59,6 +61,7 @@ use mp4parse::serialize_opus_header; use mp4parse::CodecType; use mp4parse::Track; use mp4parse::vec_push; +use mp4parse::extend_from_slice; #[repr(C)] #[derive(PartialEq, Debug)] @@ -331,6 +334,34 @@ impl Mp4parseParser { } } +pub struct Mp4parseAvifParser { + context: AvifContext, + io: Mp4parseIo, + poisoned: bool, +} + +impl Mp4parseAvifParser { + fn context(&self) -> &AvifContext { + &self.context + } + + fn context_mut(&mut self) -> &mut AvifContext { + &mut self.context + } + + fn io_mut(&mut self) -> &mut Mp4parseIo { + &mut self.io + } + + fn poisoned(&self) -> bool { + self.poisoned + } + + fn set_poisoned(&mut self, poisoned: bool) { + self.poisoned = poisoned; + } +} + #[repr(C)] #[derive(Clone)] pub struct Mp4parseIo { @@ -385,6 +416,32 @@ pub unsafe extern fn mp4parse_new(io: *const Mp4parseIo) -> *mut Mp4parseParser Box::into_raw(parser) } +/// Allocate an `Mp4parseAvifParser*` to read from the supplied `Mp4parseIo`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the io pointer given to it. +/// The caller should ensure that the `Mp4ParseIo` struct passed in is a valid +/// pointer. The caller should also ensure the members of io are valid: the +/// `read` function should be sanely implemented, and the `userdata` pointer +/// should be valid. +#[no_mangle] +pub unsafe extern fn mp4parse_avif_new(io: *const Mp4parseIo) -> *mut Mp4parseAvifParser { + if io.is_null() || (*io).userdata.is_null() { + return std::ptr::null_mut(); + } + if (*io).read.is_none() { + return std::ptr::null_mut(); + } + let parser = Box::new(Mp4parseAvifParser { + context: AvifContext::new(), + io: (*io).clone(), + poisoned: false, + }); + + Box::into_raw(parser) +} + /// Free an `Mp4parseParser*` allocated by `mp4parse_new()`. /// /// # Safety @@ -398,6 +455,19 @@ pub unsafe extern fn mp4parse_free(parser: *mut Mp4parseParser) { let _ = Box::from_raw(parser); } +/// Free an `Mp4parseAvifParser*` allocated by `mp4parse_avif_new()`. +/// +/// # Safety +/// +/// This function is unsafe because it creates a box from a raw pointer. +/// Callers should ensure that the parser pointer points to a valid +/// `Mp4parseAvifParser` created by `mp4parse_avif_new`. +#[no_mangle] +pub unsafe extern fn mp4parse_avif_free(parser: *mut Mp4parseAvifParser) { + assert!(!parser.is_null()); + let _ = Box::from_raw(parser); +} + /// Run the `Mp4parseParser*` allocated by `mp4parse_new()` until EOF or error. /// /// # Safety @@ -419,7 +489,46 @@ pub unsafe extern fn mp4parse_read(parser: *mut Mp4parseParser) -> Mp4parseStatu match r { Ok(_) => Mp4parseStatus::Ok, Err(Error::NoMoov) | Err(Error::InvalidData(_)) => { - // Block further calls. We've probable lost sync. + // Block further calls. We've probably lost sync. + (*parser).set_poisoned(true); + Mp4parseStatus::Invalid + } + Err(Error::Unsupported(_)) => Mp4parseStatus::Unsupported, + Err(Error::UnexpectedEOF) => Mp4parseStatus::Eof, + Err(Error::Io(_)) => { + // Block further calls after a read failure. + // Getting std::io::ErrorKind::UnexpectedEof is normal + // but our From trait implementation should have converted + // those to our Error::UnexpectedEOF variant. + (*parser).set_poisoned(true); + Mp4parseStatus::Io + }, + Err(Error::OutOfMemory) => Mp4parseStatus::Oom, + } +} + +/// Run the `Mp4parseAvifParser*` allocated by `mp4parse_avif_new()` until EOF or error. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the raw parser pointer +/// passed to it. Callers should ensure that the parser pointer points to a +/// valid `Mp4parseAvifParser`. +#[no_mangle] +pub unsafe extern fn mp4parse_avif_read(parser: *mut Mp4parseAvifParser) -> Mp4parseStatus { + // Validate arguments from C. + if parser.is_null() || (*parser).poisoned() { + return Mp4parseStatus::BadArg; + } + + let context = (*parser).context_mut(); + let io = (*parser).io_mut(); + + let r = read_avif(io, context); + match r { + Ok(_) => Mp4parseStatus::Ok, + Err(Error::NoMoov) | Err(Error::InvalidData(_)) => { // Add Error::NoPitm or something + // Block further calls. We've probably lost sync. (*parser).set_poisoned(true); Mp4parseStatus::Invalid } @@ -731,7 +840,10 @@ pub unsafe extern fn mp4parse_get_track_audio_info(parser: *mut Mp4parseParser, }; } } - audio_sample_infos.push(sample_info); + let res = vec_push(&mut audio_sample_infos, sample_info); + if res.is_err() { + return Mp4parseStatus::Oom; + } } (*parser).audio_track_sample_descriptions.insert(track_index, audio_sample_infos); @@ -869,7 +981,10 @@ pub unsafe extern fn mp4parse_get_track_video_info(parser: *mut Mp4parseParser, }; } } - video_sample_infos.push(sample_info); + let res = vec_push(&mut video_sample_infos, sample_info); + if res.is_err() { + return Mp4parseStatus::Oom; + } } (*parser).video_track_sample_descriptions.insert(track_index, video_sample_infos); @@ -888,6 +1003,33 @@ pub unsafe extern fn mp4parse_get_track_video_info(parser: *mut Mp4parseParser, Mp4parseStatus::Ok } +/// Return a pointer to the primary item parsed by previous `mp4parse_avif_read()` call. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences both the parser and +/// primary_item raw pointers passed into it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseAvifParser`, and that the primary_item +/// pointer points to a valid `Mp4parseByteData`. If there was not a previous +/// successful call to `mp4parse_avif_read()`, no guarantees are made as to +/// the state of `primary_item`. +#[no_mangle] +pub unsafe extern fn mp4parse_avif_get_primary_item(parser: *mut Mp4parseAvifParser, primary_item: *mut Mp4parseByteData) -> Mp4parseStatus { + if parser.is_null() || (*parser).poisoned() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *primary_item = Default::default(); + + let context = (*parser).context(); + + // TODO: check for a valid parsed context. See https://github.com/mozilla/mp4parse-rust/issues/195 + (*primary_item).set_data(&context.primary_item); + + return Mp4parseStatus::Ok; +} + /// Fill the supplied `Mp4parseByteData` with index information from `track`. /// /// # Safety @@ -978,11 +1120,11 @@ impl<'a> Iterator for TimeOffsetIterator<'a> { self.cur_sample_range = match iter.next() { Some(v) => { offset_version = v.time_offset; - (0 .. v.sample_count) + 0 .. v.sample_count }, _ => { offset_version = mp4parse::TimeOffsetVersion::Version0(0); - (0 .. 0) + 0 .. 0 }, }; @@ -1029,9 +1171,9 @@ impl<'a> Iterator for TimeToSampleIterator<'a> { self.cur_sample_count = match self.stts_iter.next() { Some(v) => { self.cur_sample_delta = v.sample_delta; - (0 .. v.sample_count) + 0 .. v.sample_count }, - _ => (0 .. 0), + _ => 0 .. 0, }; self.cur_sample_count.next() @@ -1382,7 +1524,13 @@ pub unsafe extern fn mp4parse_get_pssh_info(parser: *mut Mp4parseParser, info: * } pssh_data.extend_from_slice(pssh.system_id.as_slice()); pssh_data.extend_from_slice(data_len.as_slice()); - pssh_data.extend_from_slice(pssh.box_content.as_slice()); + // The previous two calls have known, small sizes, but pssh_data has + // arbitrary size based on untrusted input, so use fallible allocation + let res = extend_from_slice(pssh_data, pssh.box_content.as_slice()); + + if res.is_err() { + return Mp4parseStatus::Oom; + } } info.data.set_data(pssh_data);