Add Rust implementation Change-Id: I749b335ee4f123d3adb7d9d92d7c14eb9948d90c
diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..099f53a --- /dev/null +++ b/Cargo.toml
@@ -0,0 +1,14 @@ +[package] +name = "avifinfo" +description = "Basic AVIF features parsing library" +version = "1.0.0" +authors = ["Yannis Guyon <yguyon@google.com>"] +repository = "https://aomedia.googlesource.com/libavifinfo" +keywords = ["avif"] +license = "Alliance for Open Media Patent License 1.0" +include = ["avifinfo.rs", "Cargo.toml", "README.md"] +edition = "2021" + +[lib] +name = "avifinfo" +path = "avifinfo.rs"
diff --git a/README.md b/README.md index d44001b..43d0b04 100644 --- a/README.md +++ b/README.md
@@ -63,6 +63,22 @@ php avifinfo_test.php ``` +## Rust implementation + +The Rust implementation of libavifinfo is similar to the C API. + +```shell +cargo build +``` + +See `tests/avifinfo_test.rs` for a usage example. + +### Rust test + +```shell +cargo test +``` + ## Development ### Submitting patches {#submitting-patches}
diff --git a/avifinfo.rs b/avifinfo.rs new file mode 100644 index 0000000..3c20319 --- /dev/null +++ b/avifinfo.rs
@@ -0,0 +1,884 @@ +// Copyright (c) 2024, Alliance for Open Media. All rights reserved +// +// This source code is subject to the terms of the BSD 2 Clause License and +// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License +// was not distributed with this source code in the LICENSE file, you can +// obtain it at www.aomedia.org/license/software. If the Alliance for Open +// Media Patent License 1.0 was not distributed with this source code in the +// PATENTS file, you can obtain it at www.aomedia.org/license/patent. + +//----------------------------------------------------------------------------- + +#[derive(PartialEq, Debug)] +pub enum AvifInfoError { + // The input bitstream was correctly parsed until now but bytes are missing. The request + // should be repeated with more input bytes. + NotEnoughData, + // The input bitstream was correctly parsed until now but it is too complex. The parsing was + // stopped to avoid any timeout or crash. + TooComplex, + // The input bitstream is not a valid AVIF file, truncated or not. + InvalidFile, +} + +// Ok means that the file was correctly parsed and the requested information was +// extracted. It is not guaranteed that the input bitstream is a valid complete +// AVIF file. +type AvifInfoResult<T> = Result<T, AvifInfoError>; + +#[derive(Debug, Default, PartialEq)] +pub struct Features { + // In number of pixels. Ignores crop and rotation. + pub width: u32, + pub height: u32, + + // Likely 8, 10 or 12 bits per channel per pixel. + pub bit_depth: u8, + + // Likely 1, 2, 3 or 4 channels: (1 monochrome or 3 colors) + (0 or 1 alpha) + pub num_channels: u8, + + // True if a gain map was found. + pub has_gainmap: bool, + + // Id of the gain map item. Assumes there is at most one. If there are several gain map items + // (e.g. because the main image is tiled and each tile has an independent gain map), then this + // is one of the ids, arbitrarily chosen. + pub gainmap_item_id: u8, + + // Start location in bytes of the primary item id, relative to the beginning of the given + // payload. The primary item id is a big endian number stored on bytes primary_item_id_location + // to primary_item_id_location+primary_item_id_bytes-1 inclusive. + pub primary_item_id_location: usize, + + // Number of bytes of the primary item id. + pub primary_item_id_bytes: u8, +} + +//------------------------------------------------------------------------------ + +// Status returned when reading the content of a box (or file). +#[derive(PartialEq)] +enum InternalError { + NotFound, // Input correctly parsed but information is missing or elsewhere. + Truncated, // Input correctly parsed until missing bytes to continue. + Aborted, // Input correctly parsed until stopped to avoid timeout or crash. + Invalid, // Input incorrectly parsed. +} + +// Ok means "Input correctly parsed and information retrieved". +type InternalResult<T> = Result<T, InternalError>; + +impl From<InternalError> for AvifInfoError { + fn from(error: InternalError) -> Self { + match error { + InternalError::NotFound | InternalError::Truncated => AvifInfoError::NotEnoughData, + InternalError::Aborted => AvifInfoError::TooComplex, + InternalError::Invalid => AvifInfoError::InvalidFile, + } + } +} + +// Be reasonable. Avoid timeouts and out-of-memory. +const AVIFINFO_MAX_NUM_BOXES: u32 = 4096; +// InternalFeatures uses u8 to store values. +const AVIFINFO_MAX_VALUE: u8 = u8::MAX; +// Maximum number of stored associations. Past that, they are skipped. +const AVIFINFO_MAX_TILES: usize = 16; +const AVIFINFO_MAX_PROPS: usize = 32; +const AVIFINFO_MAX_FEATURES: usize = 8; +const AVIFINFO_UNDEFINED: u8 = 0; + +//------------------------------------------------------------------------------ +// Streamed input struct and helper functions. + +struct Stream<'a> { + // The available bytes. + data: Option<&'a [u8]>, + // The known size of the parent box, if any. + size: Option<usize>, + // How many bytes were read or skipped. + offset: usize, +} + +impl Stream<'_> { + fn read(&mut self, num_bytes: usize) -> InternalResult<&[u8]> { + if num_bytes == 0 { + return Ok(&[]); + } + let offset = self.offset; + self.skip(num_bytes)?; + match &self.data { + Some(data) if self.offset <= data.len() => Ok(&data[offset..self.offset]), + _ => Err(InternalError::Truncated), + } + } + + fn read_u8(&mut self) -> InternalResult<u8> { + Ok(self.read(1)?[0]) + } + + fn read_u16(&mut self) -> InternalResult<u16> { + Ok(u16::from_be_bytes(self.read(2)?.try_into().unwrap())) + } + + fn read_u24(&mut self) -> InternalResult<u32> { + Ok((self.read_uint(1)? << 16) | self.read_uint(2)?) + } + + fn read_u32(&mut self) -> InternalResult<u32> { + Ok(u32::from_be_bytes(self.read(4)?.try_into().unwrap())) + } + + fn read_u64(&mut self) -> InternalResult<u64> { + Ok(u64::from_be_bytes(self.read(8)?.try_into().unwrap())) + } + + fn read_uint(&mut self, num_bytes: u8) -> InternalResult<u32> { + match num_bytes { + 1 => Ok(self.read_u8()? as u32), + 2 => Ok(self.read_u16()? as u32), + 4 => Ok(self.read_u32()?), + _ => Err(InternalError::Aborted), + } + } + + fn read_4cc(&mut self) -> InternalResult<&[u8; 4]> { + Ok(self.read(4)?.try_into().unwrap()) + } + + fn skip(&mut self, num_bytes: usize) -> InternalResult<()> { + self.offset = self.offset.checked_add(num_bytes).ok_or(InternalError::Aborted)?; + if let Some(size) = self.size { + if self.offset > size { + return Err(InternalError::Invalid); + } + } + Ok(()) + } + + fn num_read_bytes(&self) -> usize { + self.offset // Includes the unchecked skipped bytes. + } + + fn has_more_bytes(&self) -> bool { + match self.size { + Some(size) => self.offset < size, + None => true, + } + } + + // Returns a portion of the stream. The size of the portion is either given + // or all remaining bytes are returned. + fn substream(&mut self, num_bytes: Option<usize>) -> InternalResult<Stream> { + let offset = self.offset; + let num_transferred_bytes = num_bytes.unwrap_or(match &self.data { + Some(data) => data.len().saturating_sub(offset), + None => 0, + }); + self.skip(num_transferred_bytes)?; + self.offset = offset.checked_add(num_transferred_bytes).ok_or(InternalError::Aborted)?; + Ok(Stream { + data: if let Some(data) = &self.data { + let available_size = data.len().saturating_sub(offset); + let size = std::cmp::min(available_size, num_transferred_bytes); + if size != 0 { Some(&data[offset..offset + size]) } else { None } + } else { + None + }, + size: num_bytes, + offset: 0, + }) + } +} + +//------------------------------------------------------------------------------ +// Features are parsed into temporary property associations. + +#[derive(Default)] +struct InternalTile { + tile_item_id: u8, + parent_item_id: u8, + dimg_idx: u8, // Index of this association in the dimg box (0-based). +} + +#[derive(Default)] +struct InternalProp { + property_index: u8, + item_id: u8, +} + +#[derive(Default)] +struct InternalDimProp { + property_index: u8, + width: u32, + height: u32, +} + +#[derive(Default)] +struct InternalChanProp { + property_index: u8, + bit_depth: u8, + num_channels: u8, +} + +#[derive(Default)] +struct InternalFeatures { + has_primary_item: bool, // True if "pitm" was parsed. + has_alpha: bool, // True if an alpha "auxC" was parsed. + gainmap_property_index: u8, // Index of the gain map auxC property. + primary_item_id: u8, + primary_item_features: Features, // Deduced from the data below. + data_was_skipped: bool, // True if some loops/indices were skipped. + tone_mapped_item_id: u8, // Id of the "tmap" box, > 0 if present. + iinf_parsed: bool, // True if the "iinf" (item info) box was parsed. + iref_parsed: bool, // True if the "iref" (item reference) box was parsed. + + num_tiles: usize, + tiles: [InternalTile; AVIFINFO_MAX_TILES], + num_props: usize, + props: [InternalProp; AVIFINFO_MAX_PROPS], + num_dim_props: usize, + dim_props: [InternalDimProp; AVIFINFO_MAX_FEATURES], + num_chan_props: usize, + chan_props: [InternalChanProp; AVIFINFO_MAX_FEATURES], +} + +impl InternalFeatures { + fn get_item_features(&mut self, target_item_id: u8, tile_depth: u32) -> InternalResult<()> { + for prop_item in 0..self.num_props { + if self.props[prop_item].item_id != target_item_id { + continue; + } + let property_index = self.props[prop_item].property_index; + + // Retrieve the width and height of the primary item if not already done. + if target_item_id == self.primary_item_id + && (self.primary_item_features.width == AVIFINFO_UNDEFINED as u32 + || self.primary_item_features.height == AVIFINFO_UNDEFINED as u32) + { + for i in 0..self.num_dim_props { + if self.dim_props[i].property_index != property_index { + continue; + } + self.primary_item_features.width = self.dim_props[i].width; + self.primary_item_features.height = self.dim_props[i].height; + if self.primary_item_features.bit_depth != AVIFINFO_UNDEFINED + && self.primary_item_features.num_channels != AVIFINFO_UNDEFINED + { + return Ok(()); + } + break; + } + } + // Retrieve the bit depth and number of channels of the target item if not + // already done. + if self.primary_item_features.bit_depth == AVIFINFO_UNDEFINED + || self.primary_item_features.num_channels == AVIFINFO_UNDEFINED + { + for i in 0..self.num_chan_props { + if self.chan_props[i].property_index != property_index { + continue; + } + self.primary_item_features.bit_depth = self.chan_props[i].bit_depth; + self.primary_item_features.num_channels = self.chan_props[i].num_channels; + if self.primary_item_features.width != AVIFINFO_UNDEFINED as u32 + && self.primary_item_features.height != AVIFINFO_UNDEFINED as u32 + { + return Ok(()); + } + break; + } + } + } + + // Check for the bit_depth and num_channels in a tile if not yet found. + if tile_depth < 3 { + for tile in 0..self.num_tiles { + if self.tiles[tile].parent_item_id != target_item_id { + continue; + } + match self.get_item_features(self.tiles[tile].tile_item_id, tile_depth + 1) { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} // Keep searching. + Err(error) => return Err(error), + } + } + } + Err(InternalError::NotFound) + } + + // Generates the primary_item_features. + // Returns InternalError::NotFound if there is not enough information. + fn get_primary_item_features(&mut self) -> InternalResult<()> { + // Nothing to do without the primary item ID. + if !self.has_primary_item { + return Err(InternalError::NotFound); + } + // Early exit. + if self.num_dim_props == 0 || self.num_chan_props == 0 { + return Err(InternalError::NotFound); + } + + // Look for a gain map. + // HEIF scheme: gain map is a hidden input of a derived item. + if self.tone_mapped_item_id != AVIFINFO_UNDEFINED { + for tile in 0..self.num_tiles { + if self.tiles[tile].parent_item_id == self.tone_mapped_item_id + && self.tiles[tile].dimg_idx == 1 + { + self.primary_item_features.has_gainmap = true; + self.primary_item_features.gainmap_item_id = self.tiles[tile].tile_item_id; + break; + } + } + } + // Adobe scheme: gain map is an auxiliary item. + if !self.primary_item_features.has_gainmap && self.gainmap_property_index > 0 { + for prop_item in 0..self.num_props { + if self.props[prop_item].property_index == self.gainmap_property_index { + self.primary_item_features.has_gainmap = true; + self.primary_item_features.gainmap_item_id = self.props[prop_item].item_id; + break; + } + } + } + // If the gain map has not been found but we haven't read all the relevant + // metadata, we might still find one later and cannot stop now. + if !self.primary_item_features.has_gainmap + && (!self.iinf_parsed + || (self.tone_mapped_item_id != AVIFINFO_UNDEFINED && !self.iref_parsed)) + { + return Err(InternalError::NotFound); + } + + self.get_item_features(self.primary_item_id, /* tile_depth= */ 0)?; + + // "auxC" is parsed before the "ipma" properties so it is known now, if any. + if self.has_alpha { + self.primary_item_features.num_channels += 1; + } + Ok(()) + } +} + +//------------------------------------------------------------------------------ +// Box header parsing and various size checks. + +struct InternalBox { + box_type: [u8; 4], // Four characters. + version: u8, // 0 or actual version if this is a full box. + flags: u32, // 0 or actual value if this is a full box. + content_size: Option<usize>, // If known, size of the box in bytes, header exclusive. +} + +// Reads the header of a box starting at the beginning of a stream. +fn parse_box( + nesting_level: u32, + stream: &mut Stream, + num_parsed_boxes: &mut u32, +) -> InternalResult<InternalBox> { + // See ISO/IEC 14496-12:2012(E) 4.2 + let mut box_header_size = 8usize; // box 32-bit size + 32-bit type (at least) + let mut box_size: Option<usize> = + Some(stream.read_u32()?.try_into().or(Err(InternalError::Aborted))?); + let mut box_type = *stream.read_4cc()?; + // box_size==1 means 64-bit size should be read after the box type. + // box_size==0 means this box extends to all remaining bytes. + if box_size == Some(1) { + box_header_size += 8; + box_size = Some(stream.read_u64()?.try_into().or(Err(InternalError::Aborted))?); + } else if box_size == Some(0) { + if nesting_level != 0 { + // ISO/IEC 14496-12 4.2.2: + // if size is 0, then this box shall be in a top-level box + // (i.e. not contained in another box) + return Err(InternalError::Invalid); + } + box_size = None; + } + // 16 bytes of usertype should be read here if the box type is 'uuid'. + // 'uuid' boxes are skipped so usertype is part of the skipped body. + + let has_fullbox_header = matches!( + &box_type, + b"meta" | b"pitm" | b"ipma" | b"ispe" | b"pixi" | b"iref" | b"auxC" | b"iinf" | b"infe" + ); + if has_fullbox_header { + box_header_size += 4; + } + let content_size = match box_size { + Some(box_size) => { + Some(box_size.checked_sub(box_header_size).ok_or(InternalError::Invalid)?) + } + None => None, + }; + if let Some(size) = stream.size { + if content_size.unwrap_or(box_header_size) > size.saturating_sub(stream.offset) { + return Err(InternalError::Invalid); + } + } + // get_features() can be called on a full stream or on a stream + // where the 'ftyp' box was already read. Do not count 'ftyp' boxes towards + // AVIFINFO_MAX_NUM_BOXES, so that this function returns the same status in + // both situations (because of the AVIFINFO_MAX_NUM_BOXES check that would + // compare a different box count otherwise). This is fine because top-level + // 'ftyp' boxes are just skipped anyway. + if nesting_level != 0 || &box_type != b"ftyp" { + // Avoid timeouts. The maximum number of parsed boxes is arbitrary. + *num_parsed_boxes += 1; + if *num_parsed_boxes >= AVIFINFO_MAX_NUM_BOXES { + return Err(InternalError::Aborted); + } + } + + let mut version = 0; + let mut flags = 0; + if has_fullbox_header { + version = stream.read_u8()?; + flags = stream.read_u24()?; + // See AV1 Image File Format (AVIF) 8.1 + // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when + // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged). + let is_parsable = (&box_type == b"meta" && version == 0) + || (&box_type == b"pitm" && version <= 1) + || (&box_type == b"ipma" && version <= 1) + || (&box_type == b"ispe" && version == 0) + || (&box_type == b"pixi" && version == 0) + || (&box_type == b"iref" && version <= 1) + || (&box_type == b"auxC" && version == 0) + || (&box_type == b"iinf" && version <= 1) + || (&box_type == b"infe" && version <= 2); + // Instead of considering this file as invalid, skip unparsable boxes. + if !is_parsable { + box_type = *b"skip"; // FreeSpaceBox. To be ignored by readers. + } + } + + Ok(InternalBox { box_type, version, flags, content_size }) +} + +//------------------------------------------------------------------------------ + +impl InternalFeatures { + // Parses a stream of an 'ipco' box. + // 'ispe' is used for width and height, 'pixi' and 'av1C' are used for bit depth + // and number of channels, and 'auxC' is used for alpha. + fn parse_ipco( + &mut self, + nesting_level: u32, + stream: &mut Stream, + num_parsed_boxes: &mut u32, + ) -> InternalResult<()> { + let mut box_index = 1u8; // 1-based index. Used for iterating over properties. + while stream.has_more_bytes() { + let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?; + let mut box_stream = stream.substream(box_features.content_size)?; + + match &box_features.box_type { + b"ispe" => { + // See ISO/IEC 23008-12:2017(E) 6.5.3.2 + let width = box_stream.read_u32()?; + let height = box_stream.read_u32()?; + if width == 0 || height == 0 { + return Err(InternalError::Invalid); + } + if self.num_dim_props < AVIFINFO_MAX_FEATURES { + self.dim_props[self.num_dim_props].property_index = box_index; + self.dim_props[self.num_dim_props].width = width; + self.dim_props[self.num_dim_props].height = height; + self.num_dim_props += 1; + } else { + self.data_was_skipped = true; + } + } + b"pixi" => { + // See ISO/IEC 23008-12:2017(E) 6.5.6.2 + let num_channels = box_stream.read_u8()?; + if num_channels == 0 || num_channels > 3 { + return Err(InternalError::Invalid); + } + let bit_depth = box_stream.read_u8()?; + if bit_depth == 0 { + return Err(InternalError::Invalid); + } + for _ in 1..num_channels { + // Bit depth should be the same for all channels. + if box_stream.read_u8()? != bit_depth { + return Err(InternalError::Invalid); + } + } + if self.num_chan_props < AVIFINFO_MAX_FEATURES { + self.chan_props[self.num_chan_props].property_index = box_index; + self.chan_props[self.num_chan_props].bit_depth = bit_depth; + self.chan_props[self.num_chan_props].num_channels = num_channels; + self.num_chan_props += 1; + } else { + self.data_was_skipped = true; + } + } + b"av1C" => { + // See AV1 Codec ISO Media File Format Binding 2.3.1 + // at https://aomediacodec.github.io/av1-isobmff/#av1c + // Only parse the necessary third byte. Assume that the others are valid. + box_stream.skip(2)?; + let data = box_stream.read_u8()?; + let high_bitdepth = (data & 0x40) != 0; + let twelve_bit = (data & 0x20) != 0; + let monochrome = (data & 0x10) != 0; + if twelve_bit && !high_bitdepth { + return Err(InternalError::Invalid); + } + if self.num_chan_props < AVIFINFO_MAX_FEATURES { + self.chan_props[self.num_chan_props].property_index = box_index; + self.chan_props[self.num_chan_props].bit_depth = + if high_bitdepth { if twelve_bit { 12 } else { 10 } } else { 8 }; + self.chan_props[self.num_chan_props].num_channels = + if monochrome { 1 } else { 3 }; + self.num_chan_props += 1; + } else { + self.data_was_skipped = true; + } + } + b"auxC" => { + // See AV1 Image File Format (AVIF) 4 + // at https://aomediacodec.github.io/av1-avif/#auxiliary-images + const ALPHA_STR: &[u8] = b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0"; + const GAINMAP_STR: &[u8] = b"urn:com:photo:aux:hdrgainmap\0"; + // Check for a gain map or for an alpha plane. + let content_size = box_features.content_size.unwrap(); + let data = box_stream.read(std::cmp::min(content_size, ALPHA_STR.len()))?; + if &data[..std::cmp::min(data.len(), ALPHA_STR.len())] == ALPHA_STR { + // Note: It is unlikely but possible that this alpha plane does not belong + // to the primary item or a tile. Ignore this issue. + self.has_alpha = true; + } else if &data[..std::cmp::min(data.len(), GAINMAP_STR.len())] == GAINMAP_STR { + // Note: It is unlikely but possible that this gain map does not belong to + // the primary item or a tile. Ignore this issue. + self.gainmap_property_index = box_index; + } + } + _ => {} + } + + if box_index == AVIFINFO_MAX_VALUE { + self.data_was_skipped = true; + return Err(InternalError::NotFound); + } + box_index += 1; + } + Err(InternalError::NotFound) + } + + // Parses a stream of an 'iprp' box. + // The 'ipco' box contains the properties which are linked to items by the + // 'ipma' box. + fn parse_iprp( + &mut self, + nesting_level: u32, + stream: &mut Stream, + num_parsed_boxes: &mut u32, + ) -> InternalResult<()> { + while stream.has_more_bytes() { + let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?; + let mut box_stream = stream.substream(box_features.content_size)?; + + match &box_features.box_type { + b"ipco" => { + match self.parse_ipco(nesting_level + 1, &mut box_stream, num_parsed_boxes) { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} // Keep searching. + Err(error) => return Err(error), + } + } + b"ipma" => { + // See ISO/IEC 23008-12:2017(E) 9.3.2 + let entry_count = box_stream.read_u32()?; + let id_num_bytes = if box_features.version < 1 { 2 } else { 4 }; + let index_num_bytes = if box_features.flags & 1 != 0 { 2 } else { 1 }; + let essential_bit_mask = + if box_features.flags & 1 != 0 { 0x8000 } else { 0x80 }; + + for entry in 0..entry_count as usize { + if entry >= AVIFINFO_MAX_PROPS || self.num_props >= AVIFINFO_MAX_PROPS { + self.data_was_skipped = true; + break; + } + let item_id = box_stream.read_uint(id_num_bytes)?; + let association_count = box_stream.read_u8()? as usize; + let mut property = 0usize; + while property < association_count { + if property >= AVIFINFO_MAX_PROPS + || self.num_props >= AVIFINFO_MAX_PROPS + { + self.data_was_skipped = true; + break; + } + let value = box_stream.read_uint(index_num_bytes)?; + // let essential = (value & essential_bit_mask); // Unused. + let property_index = value & (!essential_bit_mask); + if property_index <= AVIFINFO_MAX_VALUE as u32 + && item_id <= AVIFINFO_MAX_VALUE as u32 + { + self.props[self.num_props].property_index = property_index as u8; + self.props[self.num_props].item_id = item_id as u8; + self.num_props += 1; + } else { + self.data_was_skipped = true; + } + property += 1; + } + if property < association_count { + break; // Do not read garbage. + } + } + + // If all features are available now, do not look further. + match self.get_primary_item_features() { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} + Err(error) => return Err(error), + } + } + _ => {} + } + } + Err(InternalError::NotFound) + } + + // Parses a stream of an 'iref' box. + // The 'dimg' boxes contain links between tiles and their parent items, which + // can be used to infer bit depth and number of channels for the primary item + // when the latter does not have these properties. + fn parse_iref( + &mut self, + nesting_level: u32, + stream: &mut Stream, + num_parsed_boxes: &mut u32, + ) -> InternalResult<()> { + self.iref_parsed = true; + + while stream.has_more_bytes() { + let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?; + let mut box_stream = stream.substream(box_features.content_size)?; + + if let b"dimg" = &box_features.box_type { + // See ISO/IEC 14496-12:2015(E) 8.11.12.2 + let num_bytes_per_id = if box_features.version == 0 { 2 } else { 4 }; + let from_item_id = box_stream.read_uint(num_bytes_per_id)?; + let reference_count = box_stream.read_u16()?; + for i in 0..reference_count { + if i as usize >= AVIFINFO_MAX_TILES { + self.data_was_skipped = true; + break; + } + let to_item_id = box_stream.read_uint(num_bytes_per_id)?; + if from_item_id <= AVIFINFO_MAX_VALUE as u32 + && to_item_id <= AVIFINFO_MAX_VALUE as u32 + && self.num_tiles < AVIFINFO_MAX_TILES + { + self.tiles[self.num_tiles].tile_item_id = to_item_id as u8; + self.tiles[self.num_tiles].parent_item_id = from_item_id as u8; + self.tiles[self.num_tiles].dimg_idx = i as u8; + self.num_tiles += 1; + } else { + self.data_was_skipped = true; + } + } + + // If all features are available now, do not look further. + match self.get_primary_item_features() { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} + Err(error) => return Err(error), + } + } + } + Err(InternalError::NotFound) + } + + // Parses a stream of an 'iinf' box. + fn parse_iinf( + &mut self, + nesting_level: u32, + stream: &mut Stream, + box_version: u8, + num_parsed_boxes: &mut u32, + ) -> InternalResult<()> { + self.iinf_parsed = true; + + let num_bytes_per_entry_count = if box_version == 0 { 2 } else { 4 }; + let entry_count = stream.read_uint(num_bytes_per_entry_count)?; + for _ in 0..entry_count { + let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?; + let mut box_stream = stream.substream(box_features.content_size)?; + + if let b"infe" = &box_features.box_type { + // See ISO/IEC 14496-12:2015(E) 8.11.6.2 + let num_bytes_per_id = if box_features.version == 2 { 2 } else { 4 }; + let item_id = box_stream.read_uint(num_bytes_per_id)?; + + // Skip item_protection_index. + box_stream.skip(2)?; + + if box_stream.read_4cc()? == b"tmap" { + // Tone Mapped Image: indicates the presence of a gain map. + if item_id <= AVIFINFO_MAX_VALUE as u32 { + self.tone_mapped_item_id = item_id as u8; + } else { + self.data_was_skipped = true; + } + } + } + + if !stream.has_more_bytes() { + break; // Ignore entry_count bigger than box. + } + } + Err(InternalError::NotFound) + } + + // Parses a stream of a 'meta' box. It looks for the primary item ID in the + // 'pitm' box and recurses into other boxes to find the features. + fn parse_meta( + &mut self, + nesting_level: u32, + stream_offset: usize, + stream: &mut Stream, + num_parsed_boxes: &mut u32, + ) -> InternalResult<()> { + while stream.has_more_bytes() { + let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?; + let box_header_size = stream.num_read_bytes(); + let mut box_stream = stream.substream(box_features.content_size)?; + + match &box_features.box_type { + b"pitm" => { + // See ISO/IEC 14496-12:2015(E) 8.11.4.2 + let num_bytes_per_id = if box_features.version == 0 { 2 } else { 4 }; + let primary_item_id_location = + stream_offset.checked_add(box_header_size).ok_or(InternalError::Aborted)?; + let primary_item_id = box_stream.read_uint(num_bytes_per_id)?; + if primary_item_id > AVIFINFO_MAX_VALUE as u32 { + return Err(InternalError::Aborted); + } + self.has_primary_item = true; + self.primary_item_id = primary_item_id as u8; + self.primary_item_features.primary_item_id_location = primary_item_id_location; + self.primary_item_features.primary_item_id_bytes = num_bytes_per_id; + } + b"iprp" => { + match self.parse_iprp(nesting_level + 1, &mut box_stream, num_parsed_boxes) { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} // Keep searching. + Err(error) => return Err(error), + } + } + b"iref" => { + match self.parse_iref(nesting_level + 1, &mut box_stream, num_parsed_boxes) { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} // Keep searching. + Err(error) => return Err(error), + } + } + b"iinf" => { + match self.parse_iinf( + nesting_level + 1, + &mut box_stream, + box_features.version, + num_parsed_boxes, + ) { + Ok(()) => return Ok(()), + Err(InternalError::NotFound) => {} // Keep searching. + Err(error) => return Err(error), + } + } + _ => {} + } + } + // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one 'meta'. + Err(if self.data_was_skipped { InternalError::Aborted } else { InternalError::Invalid }) + } +} + +//------------------------------------------------------------------------------ + +// Parses a file stream. The file type is checked through the 'ftyp' box. +fn parse_ftyp(stream: &mut Stream) -> InternalResult<()> { + let mut num_parsed_boxes = 0u32; + let nesting_level = 0; + let box_features = parse_box(nesting_level, stream, &mut num_parsed_boxes)?; + if &box_features.box_type != b"ftyp" { + return Err(InternalError::Invalid); + } + // Consider a FileTypeBox running till the end of the file as invalid, + // because it should be first and a MetaBox should follow. + let content_size = box_features.content_size.ok_or(InternalError::Invalid)?; + // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1 + if content_size < 8 { + // major_brand,minor_version + return Err(InternalError::Invalid); + } + for i in 0..content_size / 4 { + let brand = stream.read_4cc()?; + if i == 1 { + continue; // Skip minor_version. + } + if brand == b"avif" || brand == b"avis" { + stream.skip(content_size - (i * 4 + 4))?; + return Ok(()); + } + if i > 32 { + return Err(InternalError::Aborted); // Be reasonable. + } + } + Err(InternalError::Invalid) +} + +// Parses a file stream. Features are extracted from the 'meta' box. +impl InternalFeatures { + fn parse_file(&mut self, stream: &mut Stream) -> InternalResult<()> { + let mut num_parsed_boxes = 0u32; + loop { + let box_features = + parse_box(/* nesting_level= */ 0, stream, &mut num_parsed_boxes)?; + let offset = stream.num_read_bytes(); + let mut box_stream = stream.substream(box_features.content_size)?; + + if &box_features.box_type == b"meta" { + return self.parse_meta( + /* nesting_level= */ 1, + offset, + &mut box_stream, + &mut num_parsed_boxes, + ); + } else if box_features.content_size.is_none() { + // This non-MetaBox runs till the end of the file. 'meta' is missing. + return Err(InternalError::Invalid); + } + } + } +} + +//------------------------------------------------------------------------------ +// Fixed-size input public API + +pub fn identify(data: &[u8]) -> AvifInfoResult<()> { + match parse_ftyp(&mut Stream { data: Some(data), size: None, offset: 0 }) { + Ok(()) => Ok(()), + Err(error) => Err(error.into()), + } +} + +pub fn get_features(data: &[u8]) -> AvifInfoResult<Features> { + let mut features = InternalFeatures { ..Default::default() }; + match features.parse_file(&mut Stream { data: Some(data), size: None, offset: 0 }) { + Ok(()) => Ok(features.primary_item_features), + Err(error) => Err(error.into()), + } +} + +//------------------------------------------------------------------------------ +// Streamed input API + +// There is no streamed input API yet.
diff --git a/tests/avifinfo_test.rs b/tests/avifinfo_test.rs new file mode 100644 index 0000000..a3d685b --- /dev/null +++ b/tests/avifinfo_test.rs
@@ -0,0 +1,306 @@ +// Copyright (c) 2024, Alliance for Open Media. All rights reserved +// +// This source code is subject to the terms of the BSD 2 Clause License and +// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License +// was not distributed with this source code in the LICENSE file, you can +// obtain it at www.aomedia.org/license/software. If the Alliance for Open +// Media Patent License 1.0 was not distributed with this source code in the +// PATENTS file, you can obtain it at www.aomedia.org/license/patent. + +use avifinfo::{get_features, identify, AvifInfoError, Features}; +use std::{fs::File, io::Read}; + +#[cfg(test)] +fn load_file(path: &str) -> Vec<u8> { + let mut bytes = Vec::new(); + File::open(path).unwrap().read_to_end(&mut bytes).unwrap(); + bytes +} + +//------------------------------------------------------------------------------ +// Positive tests + +#[test] +fn single_pixel() { + let file = load_file("tests/avifinfo_test_1x1.avif"); + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 1, + height: 1, + bit_depth: 8, + num_channels: 3, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn with_alpha() { + let file = load_file("tests/avifinfo_test_2x2_alpha.avif"); + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 2, + height: 2, + bit_depth: 8, + num_channels: 4, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn with_gainmap() { + let file = load_file("tests/avifinfo_test_20x20_gainmap.avif"); + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 20, + height: 20, + bit_depth: 8, + num_channels: 3, + has_gainmap: true, + gainmap_item_id: 2, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn set_primary_item_id_to_be_gainmap_item_id() { + let mut file = load_file("tests/avifinfo_test_20x20_gainmap.avif"); + file[97] = 2; + // TODO(maryla-uc): find a small test file with a gainmap that is smaller + // than the main image. + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 20, + height: 20, + bit_depth: 8, + num_channels: 1, // the gainmap is monochrome + has_gainmap: true, + gainmap_item_id: 2, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn with_gainmap_tmap() { + for file_path in [ + "tests/avifinfo_test_12x34_gainmap_tmap.avif", + "tests/avifinfo_test_12x34_gainmap_tmap_iref_after_iprp.avif", + ] { + let file = load_file(file_path); + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 12, + height: 34, + bit_depth: 10, + num_channels: 4, + has_gainmap: true, + gainmap_item_id: 4, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); + } +} + +#[test] +fn no_pixi_10b() { + // Same as above but "meta" box size is stored as 64 bits, "av1C" has + // 'high_bitdepth' set to true, "pixi" was renamed to "pixy" and "mdat" size + // is 0 (extends to the end of the file). + let file = load_file("tests/avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif"); + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 1, + height: 1, + bit_depth: 10, + num_channels: 3, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 104, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn enough_bytes() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + // Truncate 'input' just after the required information (discard AV1 box). + let mdat_position = file.windows(4).position(|window| window == b"mdat"); + file.resize(mdat_position.unwrap(), 0); + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 1, + height: 1, + bit_depth: 8, + num_channels: 3, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn metabox_is_big() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + let meta_position = file.windows(4).position(|window| window == b"meta"); + // 32-bit "1" then 4-char "meta" then 64-bit size. + file[meta_position.unwrap() - 4] = 0; + file[meta_position.unwrap() - 3] = 0; + file[meta_position.unwrap() - 2] = 0; + file[meta_position.unwrap() - 1] = 1; + for _ in 0..8 { + file.insert(meta_position.unwrap() + 4, 1); + } + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 1, + height: 1, + bit_depth: 8, + num_channels: 3, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 104, + primary_item_id_bytes: 2, + }) + ); +} + +#[test] +fn metabox_runs_till_end_of_file() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + let meta_position = file.windows(4).position(|window| window == b"meta"); + // 32-bit "0" then 4-char "meta". + file[meta_position.unwrap() - 4] = 0; + file[meta_position.unwrap() - 3] = 0; + file[meta_position.unwrap() - 2] = 0; + file[meta_position.unwrap() - 1] = 0; + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!( + get_features(file.as_slice()), + Ok(Features { + width: 1, + height: 1, + bit_depth: 8, + num_channels: 3, + has_gainmap: false, + gainmap_item_id: 0, + primary_item_id_location: 96, + primary_item_id_bytes: 2, + }) + ); +} + +//------------------------------------------------------------------------------ +// Negative tests + +#[test] +fn empty() { + assert_eq!(identify(&[]), Err(AvifInfoError::NotEnoughData)); + assert_eq!(get_features(&[]), Err(AvifInfoError::NotEnoughData)); +} + +#[test] +fn not_enough_bytes() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + // Truncate 'input' before having all the required information. + let ipma_position = file.windows(4).position(|window| window == b"ipma"); + file.resize(ipma_position.unwrap(), 0); + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::NotEnoughData)); +} + +#[test] +fn broken() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + // Change "ispe" to "aspe". + let ispe_position = file.windows(4).position(|window| window == b"ispe"); + file[ispe_position.unwrap()] = b'a'; + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile)); +} + +#[test] +fn metabox_is_too_big() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + let meta_position = file.windows(4).position(|window| window == b"meta"); + // 32-bit "1" then 4-char "meta" then 64-bit size. + file[meta_position.unwrap() - 4] = 0; + file[meta_position.unwrap() - 3] = 0; + file[meta_position.unwrap() - 2] = 0; + file[meta_position.unwrap() - 1] = 1; + for _ in 0..8 { + file.insert(meta_position.unwrap() + 4, 255); + } + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::TooComplex)); +} + +#[test] +fn filetypebox_runs_till_end_of_file() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + let ftyp_position = file.windows(4).position(|window| window == b"ftyp"); + file[ftyp_position.unwrap() - 1] = 0; + + assert_eq!(identify(file.as_slice()), Err(AvifInfoError::InvalidFile)); + assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile)); +} + +#[test] +fn imagespatialextentsproperty_runs_till_end_of_file() { + let mut file = load_file("tests/avifinfo_test_1x1.avif"); + let ispe_position = file.windows(4).position(|window| window == b"ispe"); + file[ispe_position.unwrap() - 1] = 0; + + assert_eq!(identify(file.as_slice()), Ok(())); + assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile)); +} + +#[test] +fn too_many_boxes() { + // Create a valid-ish input with too many boxes to parse. + let mut input: Vec<u8> = + vec![0, 0, 0, 16, b'f', b't', b'y', b'p', b'a', b'v', b'i', b'f', 0, 0, 0, 0]; + let num_boxes = 12345; + input.reserve(input.len() + num_boxes * 8); + for _ in 0..num_boxes { + input.extend_from_slice(&[0, 0, 0, 8, b'a', b'b', b'c', b'd']); + } + + assert_eq!(identify(input.as_slice()), Ok(())); + assert_eq!(get_features(input.as_slice()), Err(AvifInfoError::TooComplex)); +}