use std::borrow::Cow;
use crate::derived::Mif1Compliance;
use crate::error::{AvifError as Error, Result};
use crate::box_parser::{b, find_box, iter_boxes, parse_full_box, type_str, BoxType};
use crate::meta::{
Amve, Cclv, Clli, Colr, IlocExtent, Ispe, ItemInfo, ItemLocation, Mdcv, Meta, Pasp, Pixi,
Property,
};
use oxideav_heif::{FileType, HeifFile, HeifFileRef};
const FTYP: BoxType = b(b"ftyp");
const META: BoxType = b(b"meta");
const HDLR: BoxType = b(b"hdlr");
const PITM: BoxType = b(b"pitm");
const IINF: BoxType = b(b"iinf");
const INFE: BoxType = b(b"infe");
const ILOC: BoxType = b(b"iloc");
const IPRP: BoxType = b(b"iprp");
pub const BRAND_AVIF: BoxType = b(b"avif");
pub const BRAND_AVIS: BoxType = b(b"avis");
pub const BRAND_AVIO: BoxType = b(b"avio");
pub const BRAND_MIF1: BoxType = b(b"mif1");
pub const BRAND_MSF1: BoxType = b(b"msf1");
pub const BRAND_MIAF: BoxType = b(b"miaf");
pub const BRAND_MA1B: BoxType = b(b"MA1B");
pub const BRAND_MA1A: BoxType = b(b"MA1A");
pub const ITEM_TYPE_AV01: BoxType = b(b"av01");
pub const ITEM_TYPE_GRID: BoxType = b(b"grid");
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct AvifFile<'a> {
pub major_brand: BoxType,
pub minor_version: u32,
pub compatible_brands: Vec<BoxType>,
pub meta: Meta,
pub primary_item_id: u32,
pub primary_item: ItemInfo,
pub primary_item_data: Cow<'a, [u8]>,
pub av1c: Option<Vec<u8>>,
pub ispe: Option<Ispe>,
pub colr: Option<Colr>,
pub pixi: Option<Pixi>,
pub pasp: Option<Pasp>,
pub mdcv: Option<Mdcv>,
pub clli: Option<Clli>,
pub cclv: Option<Cclv>,
pub amve: Option<Amve>,
}
impl<'a> AvifFile<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
major_brand: BoxType,
minor_version: u32,
compatible_brands: Vec<BoxType>,
meta: Meta,
primary_item_id: u32,
primary_item: ItemInfo,
primary_item_data: Cow<'a, [u8]>,
av1c: Option<Vec<u8>>,
ispe: Option<Ispe>,
colr: Option<Colr>,
pixi: Option<Pixi>,
pasp: Option<Pasp>,
mdcv: Option<Mdcv>,
clli: Option<Clli>,
cclv: Option<Cclv>,
amve: Option<Amve>,
) -> Self {
Self {
major_brand,
minor_version,
compatible_brands,
meta,
primary_item_id,
primary_item,
primary_item_data,
av1c,
ispe,
colr,
pixi,
pasp,
mdcv,
clli,
cclv,
amve,
}
}
}
#[non_exhaustive]
pub struct AvifHeader<'a> {
pub file: &'a [u8],
pub major_brand: BoxType,
pub minor_version: u32,
pub compatible_brands: Vec<BoxType>,
pub meta: Meta,
pub heif: HeifFileRef<'a>,
}
impl<'a> AvifHeader<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
file: &'a [u8],
major_brand: BoxType,
minor_version: u32,
compatible_brands: Vec<BoxType>,
meta: Meta,
heif: HeifFileRef<'a>,
) -> Self {
Self {
file,
major_brand,
minor_version,
compatible_brands,
meta,
heif,
}
}
}
impl AvifHeader<'_> {
pub fn item_data(&self, item_id: u32) -> Result<Cow<'_, [u8]>> {
if self.meta.location_by_id(item_id).is_none() {
return Err(Error::invalid(format!(
"avif: item {item_id} missing in iloc"
)));
}
Ok(self.heif.item_data(item_id)?)
}
}
pub fn parse_header(file: &[u8]) -> Result<AvifHeader<'_>> {
let (ftyp_payload, _) =
find_box(file, &FTYP)?.ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
let (major_brand, minor_version, compatible_brands) = parse_ftyp(ftyp_payload)?;
classify_brands(&major_brand, &compatible_brands)?;
let heif = HeifFile::parse_borrowed(file)?;
let container = heif
.meta
.as_ref()
.ok_or_else(|| Error::invalid("avif: missing meta"))?;
let mut meta = Meta::from_container(container)?;
if let Some(meta_payload) = heif.top_level_payload(&META) {
let (_version, _flags, body) = parse_full_box(meta_payload)?;
if let Some((grpl, _)) = find_box(body, &b(b"grpl"))? {
meta.grpl = Some(grpl.to_vec());
}
}
Ok(AvifHeader {
file,
major_brand,
minor_version,
compatible_brands,
meta,
heif,
})
}
fn brands_of(ft: &FileType) -> (BoxType, u32, Vec<BoxType>) {
(
ft.major_brand,
ft.minor_version,
ft.compatible_brands.clone(),
)
}
pub fn item_bytes<'a>(file: &'a [u8], loc: &ItemLocation) -> Result<&'a [u8]> {
resolve_item_bytes(file, loc)
}
pub fn item_bytes_with_idat<'a>(
file: &'a [u8],
idat: Option<&[u8]>,
loc: &ItemLocation,
) -> Result<Cow<'a, [u8]>> {
resolve_item_bytes_with_idat(file, idat, loc)
}
pub fn audit_mif1(file: &[u8]) -> Result<Mif1Compliance> {
let mut ftyp_payload: Option<&[u8]> = None;
let mut meta_payload: Option<&[u8]> = None;
for hdr in iter_boxes(file) {
let hdr = hdr?;
let payload = &file[hdr.payload_start..hdr.end()];
match &hdr.box_type {
x if x == &FTYP => ftyp_payload = Some(payload),
x if x == &META => meta_payload = Some(payload),
_ => {}
}
}
let mut out = Mif1Compliance::default();
let Some(ftyp) = ftyp_payload else {
return Err(Error::invalid("avif: audit_mif1 missing ftyp"));
};
let (major, _minor, compat) = parse_ftyp(ftyp)?;
out.claims_mif1 = major == BRAND_MIF1 || compat.contains(&BRAND_MIF1);
let Some(meta_p) = meta_payload else {
return Ok(out);
};
let (_v, _f, meta_body) = parse_full_box(meta_p)?;
for hdr in iter_boxes(meta_body) {
let hdr = hdr?;
let payload = &meta_body[hdr.payload_start..hdr.end()];
match &hdr.box_type {
x if x == &HDLR => out.has_hdlr = true,
x if x == &PITM => out.has_pitm = true,
x if x == &IINF => {
out.has_iinf = true;
out.infe_count = count_infe(payload)?;
}
x if x == &ILOC => out.has_iloc = true,
x if x == &IPRP => out.has_iprp = true,
_ => {}
}
}
Ok(out)
}
fn count_infe(payload: &[u8]) -> Result<usize> {
let (version, _flags, body) = parse_full_box(payload)?;
let _declared = match version {
0 => {
if body.len() < 2 {
0
} else {
u16::from_be_bytes([body[0], body[1]]) as usize
}
}
_ => {
if body.len() < 4 {
0
} else {
u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize
}
}
};
let start = if version == 0 { 2 } else { 4 };
if body.len() < start {
return Ok(0);
}
let tail = &body[start..];
let mut n = 0;
for hdr in iter_boxes(tail) {
let hdr = hdr?;
if hdr.box_type == INFE {
n += 1;
}
}
Ok(n)
}
pub fn parse(file: &[u8]) -> Result<AvifFile<'_>> {
let hdr = parse_header(file)?;
let AvifHeader {
file: _,
major_brand,
minor_version,
compatible_brands,
meta,
heif,
} = hdr;
let primary_id = meta
.primary_item_id
.ok_or_else(|| Error::invalid("avif: missing pitm"))?;
let primary_info = meta
.item_by_id(primary_id)
.ok_or_else(|| Error::invalid("avif: pitm references unknown item"))?
.clone();
if primary_info.item_type != ITEM_TYPE_AV01 {
return Err(Error::unsupported(format!(
"avif: primary item type '{}' != 'av01'",
type_str(&primary_info.item_type)
)));
}
if meta.location_by_id(primary_id).is_none() {
return Err(Error::invalid("avif: primary item missing in iloc"));
}
let primary_data = primary_payload(file, &heif, primary_id)?;
let av1c = match meta.property_for(primary_id, b"av1C") {
Some(Property::Av1C(bytes)) => Some(bytes.clone()),
_ => None,
};
let ispe = match meta.property_for(primary_id, b"ispe") {
Some(Property::Ispe(v)) => Some(*v),
_ => None,
};
let colr = match meta.property_for(primary_id, b"colr") {
Some(Property::Colr(v)) => Some(v.clone()),
_ => None,
};
let pixi = match meta.property_for(primary_id, b"pixi") {
Some(Property::Pixi(v)) => Some(v.clone()),
_ => None,
};
let pasp = match meta.property_for(primary_id, b"pasp") {
Some(Property::Pasp(v)) => Some(*v),
_ => None,
};
let mdcv = match meta.property_for(primary_id, b"mdcv") {
Some(Property::Mdcv(v)) => Some(*v),
_ => None,
};
let clli = match meta.property_for(primary_id, b"clli") {
Some(Property::Clli(v)) => Some(*v),
_ => None,
};
let cclv = match meta.property_for(primary_id, b"cclv") {
Some(Property::Cclv(v)) => Some(*v),
_ => None,
};
let amve = match meta.property_for(primary_id, b"amve") {
Some(Property::Amve(v)) => Some(*v),
_ => None,
};
Ok(AvifFile {
major_brand,
minor_version,
compatible_brands,
meta,
primary_item_id: primary_id,
primary_item: primary_info,
primary_item_data: primary_data,
av1c,
ispe,
colr,
pixi,
pasp,
mdcv,
clli,
cclv,
amve,
})
}
pub(crate) fn parse_ftyp(payload: &[u8]) -> Result<(BoxType, u32, Vec<BoxType>)> {
let ft = FileType::parse(FTYP, payload)?;
Ok(brands_of(&ft))
}
fn primary_payload<'a>(
file: &'a [u8],
heif: &HeifFileRef<'_>,
item_id: u32,
) -> Result<Cow<'a, [u8]>> {
if let Ok(spans) = heif.item_file_spans(item_id) {
if let [(start, end)] = spans.as_slice() {
if let Some(slice) = file.get(*start..*end) {
return Ok(Cow::Borrowed(slice));
}
}
}
Ok(Cow::Owned(heif.item_data_owned(item_id)?))
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BrandClass {
pub is_image: bool,
pub is_sequence: bool,
pub is_intra_only: bool,
pub is_miaf: bool,
pub has_mif1: bool,
pub has_msf1: bool,
pub is_baseline_profile: bool,
pub is_advanced_profile: bool,
}
impl BrandClass {
#[allow(clippy::too_many_arguments)]
pub fn new(
is_image: bool,
is_sequence: bool,
is_intra_only: bool,
is_miaf: bool,
has_mif1: bool,
has_msf1: bool,
is_baseline_profile: bool,
is_advanced_profile: bool,
) -> Self {
Self {
is_image,
is_sequence,
is_intra_only,
is_miaf,
has_mif1,
has_msf1,
is_baseline_profile,
is_advanced_profile,
}
}
}
pub fn classify_brands(major: &BoxType, compat: &[BoxType]) -> Result<BrandClass> {
let mut cls = BrandClass::default();
let candidates: Vec<&BoxType> = std::iter::once(major).chain(compat.iter()).collect();
for brand in &candidates {
match **brand {
x if x == BRAND_AVIF => cls.is_image = true,
x if x == BRAND_AVIS => cls.is_sequence = true,
x if x == BRAND_AVIO => cls.is_intra_only = true,
x if x == BRAND_MIAF => cls.is_miaf = true,
x if x == BRAND_MIF1 => cls.has_mif1 = true,
x if x == BRAND_MSF1 => cls.has_msf1 = true,
x if x == BRAND_MA1B => cls.is_baseline_profile = true,
x if x == BRAND_MA1A => cls.is_advanced_profile = true,
_ => {}
}
}
let usable =
cls.is_image || cls.is_sequence || cls.has_mif1 || cls.has_msf1 || cls.is_intra_only;
if !usable {
return Err(Error::invalid(format!(
"avif: ftyp major='{}' compatible_brands=[{}] declares no AVIF/HEIF brand \
(need one of avif, avis, avio, mif1, msf1)",
type_str(major),
compat.iter().map(type_str).collect::<Vec<_>>().join(",")
)));
}
Ok(cls)
}
fn resolve_item_bytes<'a>(file: &'a [u8], loc: &ItemLocation) -> Result<&'a [u8]> {
if loc.construction_method != 0 {
return Err(Error::unsupported(format!(
"avif: iloc construction_method {} not supported (only file-offset is handled)",
loc.construction_method
)));
}
match loc.extents.len() {
0 => Err(Error::invalid("avif: iloc entry has no extents")),
1 => {
let e = &loc.extents[0];
let start = loc
.base_offset
.checked_add(e.offset)
.ok_or_else(|| Error::invalid("avif: iloc offset overflow"))?;
let end = start
.checked_add(e.length)
.ok_or_else(|| Error::invalid("avif: iloc length overflow"))?;
let (start, end) = (start as usize, end as usize);
if end > file.len() {
return Err(Error::invalid(format!(
"avif: iloc extent {start}..{end} exceeds file length {}",
file.len()
)));
}
Ok(&file[start..end])
}
_ => {
Err(Error::unsupported(format!(
"avif: item {} has {} extents; multi-extent items require item_bytes_owned()",
loc.id,
loc.extents.len()
)))
}
}
}
fn backing_for<'a>(file: &'a [u8], idat: Option<&'a [u8]>, loc: &ItemLocation) -> Result<&'a [u8]> {
match loc.construction_method {
0 => Ok(file),
1 => idat.ok_or_else(|| {
Error::invalid(format!(
"avif: item {} uses idat construction_method but the meta box has no idat",
loc.id
))
}),
m => Err(Error::unsupported(format!(
"avif: iloc construction_method {m} not supported (only file-offset and idat)",
))),
}
}
fn resolve_item_bytes_with_idat<'a>(
file: &'a [u8],
idat: Option<&[u8]>,
loc: &ItemLocation,
) -> Result<Cow<'a, [u8]>> {
if loc.extents.is_empty() {
return Err(Error::invalid("avif: iloc entry has no extents"));
}
if loc.construction_method == 0 && loc.extents.len() == 1 {
let (start, end) = extent_range(file, loc, &loc.extents[0], 0)?;
return Ok(Cow::Borrowed(&file[start..end]));
}
let backing = backing_for(file, idat, loc)?;
let mut out = Vec::new();
for (i, e) in loc.extents.iter().enumerate() {
let (start, end) = extent_range(backing, loc, e, i)?;
out.extend_from_slice(&backing[start..end]);
}
Ok(Cow::Owned(out))
}
fn extent_range(
backing: &[u8],
loc: &ItemLocation,
e: &IlocExtent,
i: usize,
) -> Result<(usize, usize)> {
let start = loc
.base_offset
.checked_add(e.offset)
.ok_or_else(|| Error::invalid(format!("avif: iloc extent {i} offset overflow")))?;
let end = start
.checked_add(e.length)
.ok_or_else(|| Error::invalid(format!("avif: iloc extent {i} length overflow")))?;
let (start, end) = (start as usize, end as usize);
if end > backing.len() {
return Err(Error::invalid(format!(
"avif: iloc extent {i} {start}..{end} exceeds backing length {}",
backing.len()
)));
}
Ok((start, end))
}
pub fn item_bytes_owned(file: &[u8], loc: &ItemLocation) -> Result<Vec<u8>> {
if loc.construction_method != 0 {
return Err(Error::unsupported(format!(
"avif: iloc construction_method {} not supported",
loc.construction_method
)));
}
resolve_item_bytes_with_idat(file, None, loc).map(Cow::into_owned)
}
pub fn item_bytes_owned_with_idat(
file: &[u8],
idat: Option<&[u8]>,
loc: &ItemLocation,
) -> Result<Vec<u8>> {
resolve_item_bytes_with_idat(file, idat, loc).map(Cow::into_owned)
}
const ILOC_REF: BoxType = b(b"iloc");
const MAX_ILOC_DEPTH: usize = 16;
pub fn item_bytes_owned_full(file: &[u8], meta: &Meta, item_id: u32) -> Result<Vec<u8>> {
resolve_full(file, meta, item_id, 0)
}
fn resolve_full(file: &[u8], meta: &Meta, item_id: u32, depth: usize) -> Result<Vec<u8>> {
if depth > MAX_ILOC_DEPTH {
return Err(Error::invalid(format!(
"avif: iloc construction_method 2 chain exceeds depth {MAX_ILOC_DEPTH} (cycle?) at item {item_id}"
)));
}
let loc = meta
.location_by_id(item_id)
.ok_or_else(|| Error::invalid(format!("avif: item {item_id} missing in iloc")))?;
if loc.construction_method != 2 {
return item_bytes_owned_with_idat(file, meta.idat.as_deref(), loc);
}
if loc.extents.is_empty() {
return Err(Error::invalid(format!(
"avif: item {item_id} construction_method 2 entry has no extents"
)));
}
let refs = meta.iref_targets(&ILOC_REF, item_id);
if refs.is_empty() {
return Err(Error::invalid(format!(
"avif: item {item_id} uses construction_method 2 but has no 'iloc' item reference"
)));
}
let mut out = Vec::new();
for (i, e) in loc.extents.iter().enumerate() {
let one_based = if e.extent_index == 0 {
1
} else {
e.extent_index
};
let idx = (one_based as usize)
.checked_sub(1)
.filter(|&z| z < refs.len())
.ok_or_else(|| {
Error::invalid(format!(
"avif: item {item_id} extent {i} extent_index {one_based} out of range (1..={})",
refs.len()
))
})?;
let ref_id = refs[idx];
if ref_id == item_id {
return Err(Error::invalid(format!(
"avif: item {item_id} construction_method 2 self-references (infinite recursion)"
)));
}
let origin = resolve_full(file, meta, ref_id, depth + 1)?;
let start = loc.base_offset.checked_add(e.offset).ok_or_else(|| {
Error::invalid(format!("avif: item {item_id} extent {i} offset overflow"))
})?;
let start = usize::try_from(start).map_err(|_| {
Error::invalid(format!("avif: item {item_id} extent {i} offset too large"))
})?;
if start > origin.len() {
return Err(Error::invalid(format!(
"avif: item {item_id} extent {i} offset {start} exceeds referenced item {ref_id} length {}",
origin.len()
)));
}
let end = if e.length == 0 {
origin.len()
} else {
let len = usize::try_from(e.length).map_err(|_| {
Error::invalid(format!("avif: item {item_id} extent {i} length too large"))
})?;
let end = start.checked_add(len).ok_or_else(|| {
Error::invalid(format!("avif: item {item_id} extent {i} length overflow"))
})?;
if end > origin.len() {
return Err(Error::invalid(format!(
"avif: item {item_id} extent {i} {start}..{end} exceeds referenced item {ref_id} length {}",
origin.len()
)));
}
end
};
out.extend_from_slice(&origin[start..end]);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meta::{IrefEntry, ItemLocation, Meta};
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/monochrome.avif");
fn cm0_loc(id: u32, offset: u64, length: u64) -> ItemLocation {
ItemLocation {
id,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset,
length,
extent_index: 0,
}],
}
}
fn cm2_loc(id: u32, extent_index: u64, offset: u64, length: u64) -> ItemLocation {
ItemLocation {
id,
construction_method: 2,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset,
length,
extent_index,
}],
}
}
fn iloc_iref(from_id: u32, to_ids: Vec<u32>) -> IrefEntry {
IrefEntry {
reference_type: *b"iloc",
from_id,
to_ids,
}
}
fn mk_meta(locations: Vec<ItemLocation>, irefs: Vec<IrefEntry>, idat: Option<Vec<u8>>) -> Meta {
Meta {
locations,
irefs,
idat,
..Meta::default()
}
}
#[test]
fn cm2_whole_referenced_item() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 0, 0)],
vec![iloc_iref(2, vec![1])],
None,
);
let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
assert_eq!(got, (4u8..12).collect::<Vec<u8>>());
}
#[test]
fn cm2_sub_range_of_referenced_item() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 2, 3)],
vec![iloc_iref(2, vec![1])],
None,
);
let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
assert_eq!(got, vec![6, 7, 8]);
}
#[test]
fn cm2_extent_index_zero_implies_one() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm0_loc(1, 4, 8), cm2_loc(2, 0, 0, 0)],
vec![iloc_iref(2, vec![1])],
None,
);
let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
assert_eq!(got, (4u8..12).collect::<Vec<u8>>());
}
#[test]
fn cm2_selects_second_iloc_reference() {
let file: Vec<u8> = (0u8..32).collect();
let meta = mk_meta(
vec![cm0_loc(1, 0, 8), cm0_loc(3, 20, 4), cm2_loc(2, 2, 0, 0)],
vec![iloc_iref(2, vec![1, 3])],
None,
);
let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
assert_eq!(got, (20u8..24).collect::<Vec<u8>>());
}
#[test]
fn cm2_origin_resolves_through_idat() {
let file: Vec<u8> = vec![0xff; 8];
let mut base = cm0_loc(1, 4, 8);
base.construction_method = 1; let meta = mk_meta(
vec![base, cm2_loc(2, 1, 0, 0)],
vec![iloc_iref(2, vec![1])],
Some((100u8..116).collect()),
);
let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
assert_eq!(got, (104u8..112).collect::<Vec<u8>>());
}
#[test]
fn cm2_missing_iref_errors() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 0, 0)], vec![], None);
let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
assert!(format!("{err}").contains("no 'iloc' item reference"));
}
#[test]
fn cm2_extent_index_out_of_range_errors() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm0_loc(1, 4, 8), cm2_loc(2, 5, 0, 0)],
vec![iloc_iref(2, vec![1])],
None,
);
let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
assert!(format!("{err}").contains("out of range"));
}
#[test]
fn cm2_self_reference_rejected() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm2_loc(2, 1, 0, 0)],
vec![iloc_iref(2, vec![2])], None,
);
let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
assert!(format!("{err}").contains("self-reference"));
}
#[test]
fn cm2_cycle_bounded_by_depth() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm2_loc(2, 1, 0, 0), cm2_loc(3, 1, 0, 0)],
vec![iloc_iref(2, vec![3]), iloc_iref(3, vec![2])],
None,
);
let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("depth") || msg.contains("recursion"),
"got: {msg}"
);
}
#[test]
fn cm2_sub_range_out_of_bounds_errors() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(
vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 6, 4)],
vec![iloc_iref(2, vec![1])],
None,
);
let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
assert!(format!("{err}").contains("exceeds referenced item"));
}
#[test]
fn non_cm2_unaffected_by_full_resolver() {
let file: Vec<u8> = (0u8..16).collect();
let meta = mk_meta(vec![cm0_loc(1, 3, 5)], vec![], None);
let got = item_bytes_owned_full(&file, &meta, 1).unwrap();
assert_eq!(got, (3u8..8).collect::<Vec<u8>>());
}
#[test]
fn classify_brands_baseline_profile() {
let cls = classify_brands(
&BRAND_AVIF,
&[BRAND_MIF1, BRAND_AVIF, BRAND_MIAF, BRAND_MA1B],
)
.unwrap();
assert!(cls.is_image);
assert!(cls.is_miaf);
assert!(cls.has_mif1);
assert!(cls.is_baseline_profile);
assert!(!cls.is_sequence);
assert!(!cls.is_advanced_profile);
}
#[test]
fn classify_brands_advanced_profile() {
let cls = classify_brands(&BRAND_AVIF, &[BRAND_MIF1, BRAND_MIAF, BRAND_MA1A]).unwrap();
assert!(cls.is_advanced_profile);
assert!(!cls.is_baseline_profile);
assert!(cls.is_image);
}
#[test]
fn classify_brands_sequence_with_intra_only() {
let cls = classify_brands(
&BRAND_AVIS,
&[BRAND_AVIO, BRAND_MSF1, BRAND_MIAF, BRAND_MA1B],
)
.unwrap();
assert!(cls.is_sequence);
assert!(cls.is_intra_only);
assert!(cls.has_msf1);
assert!(cls.is_miaf);
assert!(cls.is_baseline_profile);
assert!(!cls.is_image);
}
#[test]
fn classify_brands_rejects_no_avif_or_heif_brand() {
let err = classify_brands(b"mp42", &[*b"isom"]).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("declares no AVIF/HEIF brand"),
"expected AVIF/HEIF rejection, got: {msg}"
);
assert!(msg.contains("mp42"));
assert!(msg.contains("isom"));
}
#[test]
fn classify_brands_accepts_mif1_only() {
let cls = classify_brands(&BRAND_MIF1, &[]).unwrap();
assert!(cls.has_mif1);
assert!(!cls.is_image);
assert!(!cls.is_sequence);
}
#[test]
fn classify_brands_lone_miaf_without_anchor_is_rejected() {
let err = classify_brands(&BRAND_MIAF, &[]).unwrap_err();
assert!(format!("{err}").contains("declares no AVIF/HEIF brand"));
}
#[test]
fn parses_monochrome_fixture() {
let img = parse(FIXTURE).expect("parse avif");
assert_eq!(&img.major_brand, b"avif");
assert!(img.compatible_brands.iter().any(|b| b == b"mif1"));
assert_eq!(img.primary_item_id, 1);
assert_eq!(&img.primary_item.item_type, b"av01");
let ispe = img.ispe.expect("ispe for primary item");
assert_eq!(ispe.width, 1280);
assert_eq!(ispe.height, 720);
let av1c = img.av1c.as_deref().expect("av1C property");
assert_eq!(av1c[0], 0x81);
let obu_type = (img.primary_item_data[0] >> 3) & 0x0f;
assert_eq!(obu_type, 2, "first OBU should be temporal delimiter");
let pixi = img.pixi.as_ref().expect("pixi");
assert_eq!(pixi.bits_per_channel, vec![8]);
assert!(img.mdcv.is_none(), "SDR fixture must not have mdcv");
assert!(img.clli.is_none(), "SDR fixture must not have clli");
assert!(img.cclv.is_none(), "SDR fixture must not have cclv");
assert!(img.amve.is_none(), "SDR fixture must not have amve");
}
#[test]
fn item_bytes_owned_single_extent_matches_item_bytes() {
use crate::meta::{IlocExtent, ItemLocation};
let data = b"hello world";
let loc = ItemLocation {
id: 1,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 11,
extent_index: 0,
}],
};
let slice = item_bytes(data, &loc).unwrap();
let owned = item_bytes_owned(data, &loc).unwrap();
assert_eq!(slice, owned.as_slice());
assert_eq!(owned, b"hello world");
}
#[test]
fn item_bytes_owned_multi_extent_concatenates() {
use crate::meta::{IlocExtent, ItemLocation};
let data = b"AAAA____BBBB"; let loc = ItemLocation {
id: 1,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![
IlocExtent {
offset: 0,
length: 4,
extent_index: 0,
},
IlocExtent {
offset: 8,
length: 4,
extent_index: 0,
},
],
};
let owned = item_bytes_owned(data, &loc).unwrap();
assert_eq!(owned, b"AAAABBBB");
let err = item_bytes(data, &loc).unwrap_err();
assert!(
matches!(err, crate::error::AvifError::Unsupported(_)),
"item_bytes must return Unsupported for multi-extent: {err:?}"
);
}
#[test]
fn item_bytes_owned_rejects_idat_method() {
use crate::meta::{IlocExtent, ItemLocation};
let loc = ItemLocation {
id: 1,
construction_method: 1, data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 4,
extent_index: 0,
}],
};
let err = item_bytes_owned(b"data", &loc).unwrap_err();
assert!(matches!(err, crate::error::AvifError::Unsupported(_)));
}
#[test]
fn item_bytes_with_idat_single_extent_idat() {
use crate::meta::{IlocExtent, ItemLocation};
let file = b"\x00\x00\x00\x00"; let idat = b"....HELLO...."; let loc = ItemLocation {
id: 7,
construction_method: 1,
data_reference_index: 0,
base_offset: 4,
extents: vec![IlocExtent {
offset: 0,
length: 5,
extent_index: 0,
}],
};
let bytes = item_bytes_with_idat(file, Some(idat), &loc).unwrap();
assert_eq!(&*bytes, b"HELLO");
assert!(matches!(bytes, Cow::Owned(_)), "idat items are owned");
let owned = item_bytes_owned_with_idat(file, Some(idat), &loc).unwrap();
assert_eq!(owned, b"HELLO");
}
#[test]
fn item_bytes_with_idat_cm0_is_borrowed() {
use crate::meta::{IlocExtent, ItemLocation};
let file = b"xxxHELLOxxx";
let loc = ItemLocation {
id: 1,
construction_method: 0,
data_reference_index: 0,
base_offset: 3,
extents: vec![IlocExtent {
offset: 0,
length: 5,
extent_index: 0,
}],
};
let bytes = item_bytes_with_idat(file, None, &loc).unwrap();
assert_eq!(&*bytes, b"HELLO");
assert!(
matches!(bytes, Cow::Borrowed(_)),
"cm=0 single-extent must stay zero-copy"
);
}
#[test]
fn item_bytes_with_idat_multi_extent_idat() {
use crate::meta::{IlocExtent, ItemLocation};
let idat = b"AAAA____BBBB"; let loc = ItemLocation {
id: 2,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![
IlocExtent {
offset: 0,
length: 4,
extent_index: 0,
},
IlocExtent {
offset: 8,
length: 4,
extent_index: 0,
},
],
};
let bytes = item_bytes_with_idat(b"", Some(idat), &loc).unwrap();
assert_eq!(&*bytes, b"AAAABBBB");
}
#[test]
fn item_bytes_with_idat_missing_idat_errors() {
use crate::meta::{IlocExtent, ItemLocation};
let loc = ItemLocation {
id: 9,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 2,
extent_index: 0,
}],
};
let err = item_bytes_with_idat(b"data", None, &loc).unwrap_err();
assert!(
matches!(err, crate::error::AvifError::InvalidData(_)),
"missing idat for a cm=1 item must be Invalid: {err:?}"
);
}
#[test]
fn item_bytes_with_idat_extent_out_of_bounds() {
use crate::meta::{IlocExtent, ItemLocation};
let idat = b"short";
let loc = ItemLocation {
id: 3,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 100,
extent_index: 0,
}],
};
let err = item_bytes_with_idat(b"", Some(idat), &loc).unwrap_err();
assert!(matches!(err, crate::error::AvifError::InvalidData(_)));
}
#[test]
fn audit_mif1_monochrome_fixture_is_compliant() {
let m = audit_mif1(FIXTURE).expect("audit");
assert!(m.claims_mif1, "fixture has mif1 in compatible_brands");
assert!(m.has_hdlr);
assert!(m.has_pitm);
assert!(m.has_iinf);
assert!(m.has_iloc);
assert!(m.has_iprp);
assert!(m.infe_count >= 1);
assert!(m.is_compliant());
assert!(m.missing().is_empty());
}
#[test]
fn audit_mif1_ftyp_only_reports_brand_no_boxes() {
let mut buf = Vec::new();
let payload_len: u32 = 4 + 4 + 4; let size: u32 = 8 + payload_len;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"ftyp");
buf.extend_from_slice(b"mif1");
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(b"mif1");
let m = audit_mif1(&buf).expect("audit");
assert!(m.claims_mif1);
assert!(!m.is_compliant());
let missing = m.missing();
assert!(missing.contains(&"hdlr"));
assert!(missing.contains(&"pitm"));
}
}