1use std::borrow::Cow;
6
7use crate::derived::Mif1Compliance;
8use crate::error::{AvifError as Error, Result};
9
10use crate::box_parser::{b, find_box, iter_boxes, parse_full_box, type_str, BoxType};
11use crate::meta::{
12 Amve, Cclv, Clli, Colr, IlocExtent, Ispe, ItemInfo, ItemLocation, Mdcv, Meta, Pasp, Pixi,
13 Property,
14};
15use oxideav_heif::{FileType, HeifFile, HeifFileRef};
16
17const FTYP: BoxType = b(b"ftyp");
18const META: BoxType = b(b"meta");
19const HDLR: BoxType = b(b"hdlr");
20const PITM: BoxType = b(b"pitm");
21const IINF: BoxType = b(b"iinf");
22const INFE: BoxType = b(b"infe");
23const ILOC: BoxType = b(b"iloc");
24const IPRP: BoxType = b(b"iprp");
25
26pub const BRAND_AVIF: BoxType = b(b"avif");
28pub const BRAND_AVIS: BoxType = b(b"avis");
30pub const BRAND_AVIO: BoxType = b(b"avio");
32pub const BRAND_MIF1: BoxType = b(b"mif1");
34pub const BRAND_MSF1: BoxType = b(b"msf1");
36pub const BRAND_MIAF: BoxType = b(b"miaf");
39pub const BRAND_MA1B: BoxType = b(b"MA1B");
41pub const BRAND_MA1A: BoxType = b(b"MA1A");
43
44pub const ITEM_TYPE_AV01: BoxType = b(b"av01");
45pub const ITEM_TYPE_GRID: BoxType = b(b"grid");
46
47#[non_exhaustive]
55#[derive(Clone, Debug)]
56pub struct AvifFile<'a> {
57 pub major_brand: BoxType,
58 pub minor_version: u32,
59 pub compatible_brands: Vec<BoxType>,
60 pub meta: Meta,
61 pub primary_item_id: u32,
62 pub primary_item: ItemInfo,
63 pub primary_item_data: Cow<'a, [u8]>,
73 pub av1c: Option<Vec<u8>>,
77 pub ispe: Option<Ispe>,
78 pub colr: Option<Colr>,
79 pub pixi: Option<Pixi>,
80 pub pasp: Option<Pasp>,
81 pub mdcv: Option<Mdcv>,
83 pub clli: Option<Clli>,
85 pub cclv: Option<Cclv>,
87 pub amve: Option<Amve>,
89}
90
91impl<'a> AvifFile<'a> {
92 #[allow(clippy::too_many_arguments)]
97 pub fn new(
98 major_brand: BoxType,
99 minor_version: u32,
100 compatible_brands: Vec<BoxType>,
101 meta: Meta,
102 primary_item_id: u32,
103 primary_item: ItemInfo,
104 primary_item_data: Cow<'a, [u8]>,
105 av1c: Option<Vec<u8>>,
106 ispe: Option<Ispe>,
107 colr: Option<Colr>,
108 pixi: Option<Pixi>,
109 pasp: Option<Pasp>,
110 mdcv: Option<Mdcv>,
111 clli: Option<Clli>,
112 cclv: Option<Cclv>,
113 amve: Option<Amve>,
114 ) -> Self {
115 Self {
116 major_brand,
117 minor_version,
118 compatible_brands,
119 meta,
120 primary_item_id,
121 primary_item,
122 primary_item_data,
123 av1c,
124 ispe,
125 colr,
126 pixi,
127 pasp,
128 mdcv,
129 clli,
130 cclv,
131 amve,
132 }
133 }
134}
135
136#[non_exhaustive]
141pub struct AvifHeader<'a> {
142 pub file: &'a [u8],
143 pub major_brand: BoxType,
144 pub minor_version: u32,
145 pub compatible_brands: Vec<BoxType>,
146 pub meta: Meta,
147 pub heif: HeifFileRef<'a>,
154}
155
156impl<'a> AvifHeader<'a> {
157 #[allow(clippy::too_many_arguments)]
162 pub fn new(
163 file: &'a [u8],
164 major_brand: BoxType,
165 minor_version: u32,
166 compatible_brands: Vec<BoxType>,
167 meta: Meta,
168 heif: HeifFileRef<'a>,
169 ) -> Self {
170 Self {
171 file,
172 major_brand,
173 minor_version,
174 compatible_brands,
175 meta,
176 heif,
177 }
178 }
179}
180
181impl AvifHeader<'_> {
182 pub fn item_data(&self, item_id: u32) -> Result<Cow<'_, [u8]>> {
186 if self.meta.location_by_id(item_id).is_none() {
187 return Err(Error::invalid(format!(
188 "avif: item {item_id} missing in iloc"
189 )));
190 }
191 Ok(self.heif.item_data(item_id)?)
192 }
193}
194
195pub fn parse_header(file: &[u8]) -> Result<AvifHeader<'_>> {
196 let (ftyp_payload, _) =
200 find_box(file, &FTYP)?.ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
201 let (major_brand, minor_version, compatible_brands) = parse_ftyp(ftyp_payload)?;
202 classify_brands(&major_brand, &compatible_brands)?;
203 let heif = HeifFile::parse_borrowed(file)?;
204 let container = heif
205 .meta
206 .as_ref()
207 .ok_or_else(|| Error::invalid("avif: missing meta"))?;
208 let mut meta = Meta::from_container(container)?;
209 if let Some(meta_payload) = heif.top_level_payload(&META) {
210 let (_version, _flags, body) = parse_full_box(meta_payload)?;
211 if let Some((grpl, _)) = find_box(body, &b(b"grpl"))? {
212 meta.grpl = Some(grpl.to_vec());
213 }
214 }
215 Ok(AvifHeader {
216 file,
217 major_brand,
218 minor_version,
219 compatible_brands,
220 meta,
221 heif,
222 })
223}
224
225fn brands_of(ft: &FileType) -> (BoxType, u32, Vec<BoxType>) {
227 (
228 ft.major_brand,
229 ft.minor_version,
230 ft.compatible_brands.clone(),
231 )
232}
233
234pub fn item_bytes<'a>(file: &'a [u8], loc: &ItemLocation) -> Result<&'a [u8]> {
243 resolve_item_bytes(file, loc)
244}
245
246pub fn item_bytes_with_idat<'a>(
261 file: &'a [u8],
262 idat: Option<&[u8]>,
263 loc: &ItemLocation,
264) -> Result<Cow<'a, [u8]>> {
265 resolve_item_bytes_with_idat(file, idat, loc)
266}
267
268pub fn audit_mif1(file: &[u8]) -> Result<Mif1Compliance> {
284 let mut ftyp_payload: Option<&[u8]> = None;
286 let mut meta_payload: Option<&[u8]> = None;
287 for hdr in iter_boxes(file) {
288 let hdr = hdr?;
289 let payload = &file[hdr.payload_start..hdr.end()];
290 match &hdr.box_type {
291 x if x == &FTYP => ftyp_payload = Some(payload),
292 x if x == &META => meta_payload = Some(payload),
293 _ => {}
294 }
295 }
296 let mut out = Mif1Compliance::default();
297 let Some(ftyp) = ftyp_payload else {
298 return Err(Error::invalid("avif: audit_mif1 missing ftyp"));
299 };
300 let (major, _minor, compat) = parse_ftyp(ftyp)?;
301 out.claims_mif1 = major == BRAND_MIF1 || compat.contains(&BRAND_MIF1);
302 let Some(meta_p) = meta_payload else {
303 return Ok(out);
304 };
305 let (_v, _f, meta_body) = parse_full_box(meta_p)?;
306 for hdr in iter_boxes(meta_body) {
309 let hdr = hdr?;
310 let payload = &meta_body[hdr.payload_start..hdr.end()];
311 match &hdr.box_type {
312 x if x == &HDLR => out.has_hdlr = true,
313 x if x == &PITM => out.has_pitm = true,
314 x if x == &IINF => {
315 out.has_iinf = true;
316 out.infe_count = count_infe(payload)?;
317 }
318 x if x == &ILOC => out.has_iloc = true,
319 x if x == &IPRP => out.has_iprp = true,
320 _ => {}
321 }
322 }
323 Ok(out)
324}
325
326fn count_infe(payload: &[u8]) -> Result<usize> {
329 let (version, _flags, body) = parse_full_box(payload)?;
330 let _declared = match version {
334 0 => {
335 if body.len() < 2 {
336 0
337 } else {
338 u16::from_be_bytes([body[0], body[1]]) as usize
339 }
340 }
341 _ => {
342 if body.len() < 4 {
343 0
344 } else {
345 u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize
346 }
347 }
348 };
349 let start = if version == 0 { 2 } else { 4 };
351 if body.len() < start {
352 return Ok(0);
353 }
354 let tail = &body[start..];
355 let mut n = 0;
356 for hdr in iter_boxes(tail) {
357 let hdr = hdr?;
358 if hdr.box_type == INFE {
359 n += 1;
360 }
361 }
362 Ok(n)
363}
364
365pub fn parse(file: &[u8]) -> Result<AvifFile<'_>> {
372 let hdr = parse_header(file)?;
373 let AvifHeader {
374 file: _,
375 major_brand,
376 minor_version,
377 compatible_brands,
378 meta,
379 heif,
380 } = hdr;
381
382 let primary_id = meta
383 .primary_item_id
384 .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
385 let primary_info = meta
386 .item_by_id(primary_id)
387 .ok_or_else(|| Error::invalid("avif: pitm references unknown item"))?
388 .clone();
389 if primary_info.item_type != ITEM_TYPE_AV01 {
390 return Err(Error::unsupported(format!(
391 "avif: primary item type '{}' != 'av01'",
392 type_str(&primary_info.item_type)
393 )));
394 }
395
396 if meta.location_by_id(primary_id).is_none() {
397 return Err(Error::invalid("avif: primary item missing in iloc"));
398 }
399 let primary_data = primary_payload(file, &heif, primary_id)?;
400
401 let av1c = match meta.property_for(primary_id, b"av1C") {
402 Some(Property::Av1C(bytes)) => Some(bytes.clone()),
403 _ => None,
404 };
405 let ispe = match meta.property_for(primary_id, b"ispe") {
406 Some(Property::Ispe(v)) => Some(*v),
407 _ => None,
408 };
409 let colr = match meta.property_for(primary_id, b"colr") {
410 Some(Property::Colr(v)) => Some(v.clone()),
411 _ => None,
412 };
413 let pixi = match meta.property_for(primary_id, b"pixi") {
414 Some(Property::Pixi(v)) => Some(v.clone()),
415 _ => None,
416 };
417 let pasp = match meta.property_for(primary_id, b"pasp") {
418 Some(Property::Pasp(v)) => Some(*v),
419 _ => None,
420 };
421 let mdcv = match meta.property_for(primary_id, b"mdcv") {
422 Some(Property::Mdcv(v)) => Some(*v),
423 _ => None,
424 };
425 let clli = match meta.property_for(primary_id, b"clli") {
426 Some(Property::Clli(v)) => Some(*v),
427 _ => None,
428 };
429 let cclv = match meta.property_for(primary_id, b"cclv") {
430 Some(Property::Cclv(v)) => Some(*v),
431 _ => None,
432 };
433 let amve = match meta.property_for(primary_id, b"amve") {
434 Some(Property::Amve(v)) => Some(*v),
435 _ => None,
436 };
437
438 Ok(AvifFile {
439 major_brand,
440 minor_version,
441 compatible_brands,
442 meta,
443 primary_item_id: primary_id,
444 primary_item: primary_info,
445 primary_item_data: primary_data,
446 av1c,
447 ispe,
448 colr,
449 pixi,
450 pasp,
451 mdcv,
452 clli,
453 cclv,
454 amve,
455 })
456}
457
458pub(crate) fn parse_ftyp(payload: &[u8]) -> Result<(BoxType, u32, Vec<BoxType>)> {
459 let ft = FileType::parse(FTYP, payload)?;
460 Ok(brands_of(&ft))
461}
462
463fn primary_payload<'a>(
467 file: &'a [u8],
468 heif: &HeifFileRef<'_>,
469 item_id: u32,
470) -> Result<Cow<'a, [u8]>> {
471 if let Ok(spans) = heif.item_file_spans(item_id) {
472 if let [(start, end)] = spans.as_slice() {
473 if let Some(slice) = file.get(*start..*end) {
474 return Ok(Cow::Borrowed(slice));
475 }
476 }
477 }
478 Ok(Cow::Owned(heif.item_data_owned(item_id)?))
479}
480
481#[doc(hidden)]
485#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
486pub struct BrandClass {
487 pub is_image: bool,
489 pub is_sequence: bool,
491 pub is_intra_only: bool,
493 pub is_miaf: bool,
496 pub has_mif1: bool,
498 pub has_msf1: bool,
500 pub is_baseline_profile: bool,
502 pub is_advanced_profile: bool,
504}
505
506impl BrandClass {
507 #[allow(clippy::too_many_arguments)]
512 pub fn new(
513 is_image: bool,
514 is_sequence: bool,
515 is_intra_only: bool,
516 is_miaf: bool,
517 has_mif1: bool,
518 has_msf1: bool,
519 is_baseline_profile: bool,
520 is_advanced_profile: bool,
521 ) -> Self {
522 Self {
523 is_image,
524 is_sequence,
525 is_intra_only,
526 is_miaf,
527 has_mif1,
528 has_msf1,
529 is_baseline_profile,
530 is_advanced_profile,
531 }
532 }
533}
534
535pub fn classify_brands(major: &BoxType, compat: &[BoxType]) -> Result<BrandClass> {
552 let mut cls = BrandClass::default();
553 let candidates: Vec<&BoxType> = std::iter::once(major).chain(compat.iter()).collect();
554 for brand in &candidates {
555 match **brand {
556 x if x == BRAND_AVIF => cls.is_image = true,
557 x if x == BRAND_AVIS => cls.is_sequence = true,
558 x if x == BRAND_AVIO => cls.is_intra_only = true,
559 x if x == BRAND_MIAF => cls.is_miaf = true,
560 x if x == BRAND_MIF1 => cls.has_mif1 = true,
561 x if x == BRAND_MSF1 => cls.has_msf1 = true,
562 x if x == BRAND_MA1B => cls.is_baseline_profile = true,
563 x if x == BRAND_MA1A => cls.is_advanced_profile = true,
564 _ => {}
565 }
566 }
567 let usable =
574 cls.is_image || cls.is_sequence || cls.has_mif1 || cls.has_msf1 || cls.is_intra_only;
575 if !usable {
576 return Err(Error::invalid(format!(
577 "avif: ftyp major='{}' compatible_brands=[{}] declares no AVIF/HEIF brand \
578 (need one of avif, avis, avio, mif1, msf1)",
579 type_str(major),
580 compat.iter().map(type_str).collect::<Vec<_>>().join(",")
581 )));
582 }
583 Ok(cls)
584}
585
586fn resolve_item_bytes<'a>(file: &'a [u8], loc: &ItemLocation) -> Result<&'a [u8]> {
587 if loc.construction_method != 0 {
588 return Err(Error::unsupported(format!(
589 "avif: iloc construction_method {} not supported (only file-offset is handled)",
590 loc.construction_method
591 )));
592 }
593 match loc.extents.len() {
594 0 => Err(Error::invalid("avif: iloc entry has no extents")),
595 1 => {
596 let e = &loc.extents[0];
598 let start = loc
599 .base_offset
600 .checked_add(e.offset)
601 .ok_or_else(|| Error::invalid("avif: iloc offset overflow"))?;
602 let end = start
603 .checked_add(e.length)
604 .ok_or_else(|| Error::invalid("avif: iloc length overflow"))?;
605 let (start, end) = (start as usize, end as usize);
606 if end > file.len() {
607 return Err(Error::invalid(format!(
608 "avif: iloc extent {start}..{end} exceeds file length {}",
609 file.len()
610 )));
611 }
612 Ok(&file[start..end])
613 }
614 _ => {
615 Err(Error::unsupported(format!(
632 "avif: item {} has {} extents; multi-extent items require item_bytes_owned()",
633 loc.id,
634 loc.extents.len()
635 )))
636 }
637 }
638}
639
640fn backing_for<'a>(file: &'a [u8], idat: Option<&'a [u8]>, loc: &ItemLocation) -> Result<&'a [u8]> {
644 match loc.construction_method {
645 0 => Ok(file),
646 1 => idat.ok_or_else(|| {
647 Error::invalid(format!(
648 "avif: item {} uses idat construction_method but the meta box has no idat",
649 loc.id
650 ))
651 }),
652 m => Err(Error::unsupported(format!(
653 "avif: iloc construction_method {m} not supported (only file-offset and idat)",
654 ))),
655 }
656}
657
658fn resolve_item_bytes_with_idat<'a>(
665 file: &'a [u8],
666 idat: Option<&[u8]>,
667 loc: &ItemLocation,
668) -> Result<Cow<'a, [u8]>> {
669 if loc.extents.is_empty() {
670 return Err(Error::invalid("avif: iloc entry has no extents"));
671 }
672 if loc.construction_method == 0 && loc.extents.len() == 1 {
675 let (start, end) = extent_range(file, loc, &loc.extents[0], 0)?;
676 return Ok(Cow::Borrowed(&file[start..end]));
677 }
678 let backing = backing_for(file, idat, loc)?;
681 let mut out = Vec::new();
682 for (i, e) in loc.extents.iter().enumerate() {
683 let (start, end) = extent_range(backing, loc, e, i)?;
684 out.extend_from_slice(&backing[start..end]);
685 }
686 Ok(Cow::Owned(out))
687}
688
689fn extent_range(
691 backing: &[u8],
692 loc: &ItemLocation,
693 e: &IlocExtent,
694 i: usize,
695) -> Result<(usize, usize)> {
696 let start = loc
697 .base_offset
698 .checked_add(e.offset)
699 .ok_or_else(|| Error::invalid(format!("avif: iloc extent {i} offset overflow")))?;
700 let end = start
701 .checked_add(e.length)
702 .ok_or_else(|| Error::invalid(format!("avif: iloc extent {i} length overflow")))?;
703 let (start, end) = (start as usize, end as usize);
704 if end > backing.len() {
705 return Err(Error::invalid(format!(
706 "avif: iloc extent {i} {start}..{end} exceeds backing length {}",
707 backing.len()
708 )));
709 }
710 Ok((start, end))
711}
712
713pub fn item_bytes_owned(file: &[u8], loc: &ItemLocation) -> Result<Vec<u8>> {
724 if loc.construction_method != 0 {
725 return Err(Error::unsupported(format!(
726 "avif: iloc construction_method {} not supported",
727 loc.construction_method
728 )));
729 }
730 resolve_item_bytes_with_idat(file, None, loc).map(Cow::into_owned)
731}
732
733pub fn item_bytes_owned_with_idat(
740 file: &[u8],
741 idat: Option<&[u8]>,
742 loc: &ItemLocation,
743) -> Result<Vec<u8>> {
744 resolve_item_bytes_with_idat(file, idat, loc).map(Cow::into_owned)
745}
746
747const ILOC_REF: BoxType = b(b"iloc");
751
752const MAX_ILOC_DEPTH: usize = 16;
758
759pub fn item_bytes_owned_full(file: &[u8], meta: &Meta, item_id: u32) -> Result<Vec<u8>> {
780 resolve_full(file, meta, item_id, 0)
781}
782
783fn resolve_full(file: &[u8], meta: &Meta, item_id: u32, depth: usize) -> Result<Vec<u8>> {
784 if depth > MAX_ILOC_DEPTH {
785 return Err(Error::invalid(format!(
786 "avif: iloc construction_method 2 chain exceeds depth {MAX_ILOC_DEPTH} (cycle?) at item {item_id}"
787 )));
788 }
789 let loc = meta
790 .location_by_id(item_id)
791 .ok_or_else(|| Error::invalid(format!("avif: item {item_id} missing in iloc")))?;
792
793 if loc.construction_method != 2 {
794 return item_bytes_owned_with_idat(file, meta.idat.as_deref(), loc);
797 }
798
799 if loc.extents.is_empty() {
800 return Err(Error::invalid(format!(
801 "avif: item {item_id} construction_method 2 entry has no extents"
802 )));
803 }
804
805 let refs = meta.iref_targets(&ILOC_REF, item_id);
807 if refs.is_empty() {
808 return Err(Error::invalid(format!(
809 "avif: item {item_id} uses construction_method 2 but has no 'iloc' item reference"
810 )));
811 }
812
813 let mut out = Vec::new();
814 for (i, e) in loc.extents.iter().enumerate() {
815 let one_based = if e.extent_index == 0 {
817 1
818 } else {
819 e.extent_index
820 };
821 let idx = (one_based as usize)
822 .checked_sub(1)
823 .filter(|&z| z < refs.len())
824 .ok_or_else(|| {
825 Error::invalid(format!(
826 "avif: item {item_id} extent {i} extent_index {one_based} out of range (1..={})",
827 refs.len()
828 ))
829 })?;
830 let ref_id = refs[idx];
831 if ref_id == item_id {
832 return Err(Error::invalid(format!(
833 "avif: item {item_id} construction_method 2 self-references (infinite recursion)"
834 )));
835 }
836 let origin = resolve_full(file, meta, ref_id, depth + 1)?;
838 let start = loc.base_offset.checked_add(e.offset).ok_or_else(|| {
841 Error::invalid(format!("avif: item {item_id} extent {i} offset overflow"))
842 })?;
843 let start = usize::try_from(start).map_err(|_| {
844 Error::invalid(format!("avif: item {item_id} extent {i} offset too large"))
845 })?;
846 if start > origin.len() {
847 return Err(Error::invalid(format!(
848 "avif: item {item_id} extent {i} offset {start} exceeds referenced item {ref_id} length {}",
849 origin.len()
850 )));
851 }
852 let end = if e.length == 0 {
853 origin.len()
854 } else {
855 let len = usize::try_from(e.length).map_err(|_| {
856 Error::invalid(format!("avif: item {item_id} extent {i} length too large"))
857 })?;
858 let end = start.checked_add(len).ok_or_else(|| {
859 Error::invalid(format!("avif: item {item_id} extent {i} length overflow"))
860 })?;
861 if end > origin.len() {
862 return Err(Error::invalid(format!(
863 "avif: item {item_id} extent {i} {start}..{end} exceeds referenced item {ref_id} length {}",
864 origin.len()
865 )));
866 }
867 end
868 };
869 out.extend_from_slice(&origin[start..end]);
870 }
871 Ok(out)
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::meta::{IrefEntry, ItemLocation, Meta};
878
879 const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/monochrome.avif");
880
881 fn cm0_loc(id: u32, offset: u64, length: u64) -> ItemLocation {
884 ItemLocation {
885 id,
886 construction_method: 0,
887 data_reference_index: 0,
888 base_offset: 0,
889 extents: vec![IlocExtent {
890 offset,
891 length,
892 extent_index: 0,
893 }],
894 }
895 }
896
897 fn cm2_loc(id: u32, extent_index: u64, offset: u64, length: u64) -> ItemLocation {
901 ItemLocation {
902 id,
903 construction_method: 2,
904 data_reference_index: 0,
905 base_offset: 0,
906 extents: vec![IlocExtent {
907 offset,
908 length,
909 extent_index,
910 }],
911 }
912 }
913
914 fn iloc_iref(from_id: u32, to_ids: Vec<u32>) -> IrefEntry {
915 IrefEntry {
916 reference_type: *b"iloc",
917 from_id,
918 to_ids,
919 }
920 }
921
922 fn mk_meta(locations: Vec<ItemLocation>, irefs: Vec<IrefEntry>, idat: Option<Vec<u8>>) -> Meta {
926 Meta {
927 locations,
928 irefs,
929 idat,
930 ..Meta::default()
931 }
932 }
933
934 #[test]
935 fn cm2_whole_referenced_item() {
936 let file: Vec<u8> = (0u8..16).collect();
940 let meta = mk_meta(
941 vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 0, 0)],
942 vec![iloc_iref(2, vec![1])],
943 None,
944 );
945 let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
946 assert_eq!(got, (4u8..12).collect::<Vec<u8>>());
947 }
948
949 #[test]
950 fn cm2_sub_range_of_referenced_item() {
951 let file: Vec<u8> = (0u8..16).collect();
954 let meta = mk_meta(
955 vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 2, 3)],
956 vec![iloc_iref(2, vec![1])],
957 None,
958 );
959 let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
960 assert_eq!(got, vec![6, 7, 8]);
961 }
962
963 #[test]
964 fn cm2_extent_index_zero_implies_one() {
965 let file: Vec<u8> = (0u8..16).collect();
967 let meta = mk_meta(
968 vec![cm0_loc(1, 4, 8), cm2_loc(2, 0, 0, 0)],
969 vec![iloc_iref(2, vec![1])],
970 None,
971 );
972 let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
973 assert_eq!(got, (4u8..12).collect::<Vec<u8>>());
974 }
975
976 #[test]
977 fn cm2_selects_second_iloc_reference() {
978 let file: Vec<u8> = (0u8..32).collect();
980 let meta = mk_meta(
981 vec![cm0_loc(1, 0, 8), cm0_loc(3, 20, 4), cm2_loc(2, 2, 0, 0)],
982 vec![iloc_iref(2, vec![1, 3])],
983 None,
984 );
985 let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
986 assert_eq!(got, (20u8..24).collect::<Vec<u8>>());
987 }
988
989 #[test]
990 fn cm2_origin_resolves_through_idat() {
991 let file: Vec<u8> = vec![0xff; 8];
994 let mut base = cm0_loc(1, 4, 8);
995 base.construction_method = 1; let meta = mk_meta(
997 vec![base, cm2_loc(2, 1, 0, 0)],
998 vec![iloc_iref(2, vec![1])],
999 Some((100u8..116).collect()),
1000 );
1001 let got = item_bytes_owned_full(&file, &meta, 2).unwrap();
1002 assert_eq!(got, (104u8..112).collect::<Vec<u8>>());
1003 }
1004
1005 #[test]
1006 fn cm2_missing_iref_errors() {
1007 let file: Vec<u8> = (0u8..16).collect();
1008 let meta = mk_meta(vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 0, 0)], vec![], None);
1010 let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
1011 assert!(format!("{err}").contains("no 'iloc' item reference"));
1012 }
1013
1014 #[test]
1015 fn cm2_extent_index_out_of_range_errors() {
1016 let file: Vec<u8> = (0u8..16).collect();
1017 let meta = mk_meta(
1018 vec![cm0_loc(1, 4, 8), cm2_loc(2, 5, 0, 0)],
1019 vec![iloc_iref(2, vec![1])],
1020 None,
1021 );
1022 let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
1023 assert!(format!("{err}").contains("out of range"));
1024 }
1025
1026 #[test]
1027 fn cm2_self_reference_rejected() {
1028 let file: Vec<u8> = (0u8..16).collect();
1029 let meta = mk_meta(
1030 vec![cm2_loc(2, 1, 0, 0)],
1031 vec![iloc_iref(2, vec![2])], None,
1033 );
1034 let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
1035 assert!(format!("{err}").contains("self-reference"));
1036 }
1037
1038 #[test]
1039 fn cm2_cycle_bounded_by_depth() {
1040 let file: Vec<u8> = (0u8..16).collect();
1042 let meta = mk_meta(
1043 vec![cm2_loc(2, 1, 0, 0), cm2_loc(3, 1, 0, 0)],
1044 vec![iloc_iref(2, vec![3]), iloc_iref(3, vec![2])],
1045 None,
1046 );
1047 let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
1048 let msg = format!("{err}");
1049 assert!(
1050 msg.contains("depth") || msg.contains("recursion"),
1051 "got: {msg}"
1052 );
1053 }
1054
1055 #[test]
1056 fn cm2_sub_range_out_of_bounds_errors() {
1057 let file: Vec<u8> = (0u8..16).collect();
1059 let meta = mk_meta(
1060 vec![cm0_loc(1, 4, 8), cm2_loc(2, 1, 6, 4)],
1061 vec![iloc_iref(2, vec![1])],
1062 None,
1063 );
1064 let err = item_bytes_owned_full(&file, &meta, 2).unwrap_err();
1065 assert!(format!("{err}").contains("exceeds referenced item"));
1066 }
1067
1068 #[test]
1069 fn non_cm2_unaffected_by_full_resolver() {
1070 let file: Vec<u8> = (0u8..16).collect();
1072 let meta = mk_meta(vec![cm0_loc(1, 3, 5)], vec![], None);
1073 let got = item_bytes_owned_full(&file, &meta, 1).unwrap();
1074 assert_eq!(got, (3u8..8).collect::<Vec<u8>>());
1075 }
1076
1077 #[test]
1078 fn classify_brands_baseline_profile() {
1079 let cls = classify_brands(
1081 &BRAND_AVIF,
1082 &[BRAND_MIF1, BRAND_AVIF, BRAND_MIAF, BRAND_MA1B],
1083 )
1084 .unwrap();
1085 assert!(cls.is_image);
1086 assert!(cls.is_miaf);
1087 assert!(cls.has_mif1);
1088 assert!(cls.is_baseline_profile);
1089 assert!(!cls.is_sequence);
1090 assert!(!cls.is_advanced_profile);
1091 }
1092
1093 #[test]
1094 fn classify_brands_advanced_profile() {
1095 let cls = classify_brands(&BRAND_AVIF, &[BRAND_MIF1, BRAND_MIAF, BRAND_MA1A]).unwrap();
1096 assert!(cls.is_advanced_profile);
1097 assert!(!cls.is_baseline_profile);
1098 assert!(cls.is_image);
1099 }
1100
1101 #[test]
1102 fn classify_brands_sequence_with_intra_only() {
1103 let cls = classify_brands(
1105 &BRAND_AVIS,
1106 &[BRAND_AVIO, BRAND_MSF1, BRAND_MIAF, BRAND_MA1B],
1107 )
1108 .unwrap();
1109 assert!(cls.is_sequence);
1110 assert!(cls.is_intra_only);
1111 assert!(cls.has_msf1);
1112 assert!(cls.is_miaf);
1113 assert!(cls.is_baseline_profile);
1114 assert!(!cls.is_image);
1115 }
1116
1117 #[test]
1118 fn classify_brands_rejects_no_avif_or_heif_brand() {
1119 let err = classify_brands(b"mp42", &[*b"isom"]).unwrap_err();
1122 let msg = format!("{err}");
1123 assert!(
1124 msg.contains("declares no AVIF/HEIF brand"),
1125 "expected AVIF/HEIF rejection, got: {msg}"
1126 );
1127 assert!(msg.contains("mp42"));
1129 assert!(msg.contains("isom"));
1130 }
1131
1132 #[test]
1133 fn classify_brands_accepts_mif1_only() {
1134 let cls = classify_brands(&BRAND_MIF1, &[]).unwrap();
1137 assert!(cls.has_mif1);
1138 assert!(!cls.is_image);
1139 assert!(!cls.is_sequence);
1140 }
1141
1142 #[test]
1143 fn classify_brands_lone_miaf_without_anchor_is_rejected() {
1144 let err = classify_brands(&BRAND_MIAF, &[]).unwrap_err();
1148 assert!(format!("{err}").contains("declares no AVIF/HEIF brand"));
1149 }
1150
1151 #[test]
1152 fn parses_monochrome_fixture() {
1153 let img = parse(FIXTURE).expect("parse avif");
1154 assert_eq!(&img.major_brand, b"avif");
1155 assert!(img.compatible_brands.iter().any(|b| b == b"mif1"));
1156 assert_eq!(img.primary_item_id, 1);
1157 assert_eq!(&img.primary_item.item_type, b"av01");
1158 let ispe = img.ispe.expect("ispe for primary item");
1159 assert_eq!(ispe.width, 1280);
1160 assert_eq!(ispe.height, 720);
1161 let av1c = img.av1c.as_deref().expect("av1C property");
1163 assert_eq!(av1c[0], 0x81);
1164 let obu_type = (img.primary_item_data[0] >> 3) & 0x0f;
1170 assert_eq!(obu_type, 2, "first OBU should be temporal delimiter");
1171 let pixi = img.pixi.as_ref().expect("pixi");
1173 assert_eq!(pixi.bits_per_channel, vec![8]);
1174 assert!(img.mdcv.is_none(), "SDR fixture must not have mdcv");
1176 assert!(img.clli.is_none(), "SDR fixture must not have clli");
1177 assert!(img.cclv.is_none(), "SDR fixture must not have cclv");
1178 assert!(img.amve.is_none(), "SDR fixture must not have amve");
1179 }
1180
1181 #[test]
1184 fn item_bytes_owned_single_extent_matches_item_bytes() {
1185 use crate::meta::{IlocExtent, ItemLocation};
1186 let data = b"hello world";
1187 let loc = ItemLocation {
1189 id: 1,
1190 construction_method: 0,
1191 data_reference_index: 0,
1192 base_offset: 0,
1193 extents: vec![IlocExtent {
1194 offset: 0,
1195 length: 11,
1196 extent_index: 0,
1197 }],
1198 };
1199 let slice = item_bytes(data, &loc).unwrap();
1200 let owned = item_bytes_owned(data, &loc).unwrap();
1201 assert_eq!(slice, owned.as_slice());
1202 assert_eq!(owned, b"hello world");
1203 }
1204
1205 #[test]
1207 fn item_bytes_owned_multi_extent_concatenates() {
1208 use crate::meta::{IlocExtent, ItemLocation};
1209 let data = b"AAAA____BBBB"; let loc = ItemLocation {
1211 id: 1,
1212 construction_method: 0,
1213 data_reference_index: 0,
1214 base_offset: 0,
1215 extents: vec![
1216 IlocExtent {
1217 offset: 0,
1218 length: 4,
1219 extent_index: 0,
1220 },
1221 IlocExtent {
1222 offset: 8,
1223 length: 4,
1224 extent_index: 0,
1225 },
1226 ],
1227 };
1228 let owned = item_bytes_owned(data, &loc).unwrap();
1229 assert_eq!(owned, b"AAAABBBB");
1230 let err = item_bytes(data, &loc).unwrap_err();
1232 assert!(
1233 matches!(err, crate::error::AvifError::Unsupported(_)),
1234 "item_bytes must return Unsupported for multi-extent: {err:?}"
1235 );
1236 }
1237
1238 #[test]
1240 fn item_bytes_owned_rejects_idat_method() {
1241 use crate::meta::{IlocExtent, ItemLocation};
1242 let loc = ItemLocation {
1243 id: 1,
1244 construction_method: 1, data_reference_index: 0,
1246 base_offset: 0,
1247 extents: vec![IlocExtent {
1248 offset: 0,
1249 length: 4,
1250 extent_index: 0,
1251 }],
1252 };
1253 let err = item_bytes_owned(b"data", &loc).unwrap_err();
1254 assert!(matches!(err, crate::error::AvifError::Unsupported(_)));
1255 }
1256
1257 #[test]
1263 fn item_bytes_with_idat_single_extent_idat() {
1264 use crate::meta::{IlocExtent, ItemLocation};
1265 let file = b"\x00\x00\x00\x00"; let idat = b"....HELLO...."; let loc = ItemLocation {
1268 id: 7,
1269 construction_method: 1,
1270 data_reference_index: 0,
1271 base_offset: 4,
1272 extents: vec![IlocExtent {
1273 offset: 0,
1274 length: 5,
1275 extent_index: 0,
1276 }],
1277 };
1278 let bytes = item_bytes_with_idat(file, Some(idat), &loc).unwrap();
1279 assert_eq!(&*bytes, b"HELLO");
1280 assert!(matches!(bytes, Cow::Owned(_)), "idat items are owned");
1281 let owned = item_bytes_owned_with_idat(file, Some(idat), &loc).unwrap();
1283 assert_eq!(owned, b"HELLO");
1284 }
1285
1286 #[test]
1289 fn item_bytes_with_idat_cm0_is_borrowed() {
1290 use crate::meta::{IlocExtent, ItemLocation};
1291 let file = b"xxxHELLOxxx";
1292 let loc = ItemLocation {
1293 id: 1,
1294 construction_method: 0,
1295 data_reference_index: 0,
1296 base_offset: 3,
1297 extents: vec![IlocExtent {
1298 offset: 0,
1299 length: 5,
1300 extent_index: 0,
1301 }],
1302 };
1303 let bytes = item_bytes_with_idat(file, None, &loc).unwrap();
1304 assert_eq!(&*bytes, b"HELLO");
1305 assert!(
1306 matches!(bytes, Cow::Borrowed(_)),
1307 "cm=0 single-extent must stay zero-copy"
1308 );
1309 }
1310
1311 #[test]
1314 fn item_bytes_with_idat_multi_extent_idat() {
1315 use crate::meta::{IlocExtent, ItemLocation};
1316 let idat = b"AAAA____BBBB"; let loc = ItemLocation {
1318 id: 2,
1319 construction_method: 1,
1320 data_reference_index: 0,
1321 base_offset: 0,
1322 extents: vec![
1323 IlocExtent {
1324 offset: 0,
1325 length: 4,
1326 extent_index: 0,
1327 },
1328 IlocExtent {
1329 offset: 8,
1330 length: 4,
1331 extent_index: 0,
1332 },
1333 ],
1334 };
1335 let bytes = item_bytes_with_idat(b"", Some(idat), &loc).unwrap();
1336 assert_eq!(&*bytes, b"AAAABBBB");
1337 }
1338
1339 #[test]
1342 fn item_bytes_with_idat_missing_idat_errors() {
1343 use crate::meta::{IlocExtent, ItemLocation};
1344 let loc = ItemLocation {
1345 id: 9,
1346 construction_method: 1,
1347 data_reference_index: 0,
1348 base_offset: 0,
1349 extents: vec![IlocExtent {
1350 offset: 0,
1351 length: 2,
1352 extent_index: 0,
1353 }],
1354 };
1355 let err = item_bytes_with_idat(b"data", None, &loc).unwrap_err();
1356 assert!(
1357 matches!(err, crate::error::AvifError::InvalidData(_)),
1358 "missing idat for a cm=1 item must be Invalid: {err:?}"
1359 );
1360 }
1361
1362 #[test]
1365 fn item_bytes_with_idat_extent_out_of_bounds() {
1366 use crate::meta::{IlocExtent, ItemLocation};
1367 let idat = b"short";
1368 let loc = ItemLocation {
1369 id: 3,
1370 construction_method: 1,
1371 data_reference_index: 0,
1372 base_offset: 0,
1373 extents: vec![IlocExtent {
1374 offset: 0,
1375 length: 100,
1376 extent_index: 0,
1377 }],
1378 };
1379 let err = item_bytes_with_idat(b"", Some(idat), &loc).unwrap_err();
1380 assert!(matches!(err, crate::error::AvifError::InvalidData(_)));
1381 }
1382
1383 #[test]
1389 fn audit_mif1_monochrome_fixture_is_compliant() {
1390 let m = audit_mif1(FIXTURE).expect("audit");
1391 assert!(m.claims_mif1, "fixture has mif1 in compatible_brands");
1392 assert!(m.has_hdlr);
1393 assert!(m.has_pitm);
1394 assert!(m.has_iinf);
1395 assert!(m.has_iloc);
1396 assert!(m.has_iprp);
1397 assert!(m.infe_count >= 1);
1398 assert!(m.is_compliant());
1399 assert!(m.missing().is_empty());
1400 }
1401
1402 #[test]
1406 fn audit_mif1_ftyp_only_reports_brand_no_boxes() {
1407 let mut buf = Vec::new();
1409 let payload_len: u32 = 4 + 4 + 4; let size: u32 = 8 + payload_len;
1411 buf.extend_from_slice(&size.to_be_bytes());
1412 buf.extend_from_slice(b"ftyp");
1413 buf.extend_from_slice(b"mif1");
1414 buf.extend_from_slice(&0u32.to_be_bytes());
1415 buf.extend_from_slice(b"mif1");
1416 let m = audit_mif1(&buf).expect("audit");
1417 assert!(m.claims_mif1);
1418 assert!(!m.is_compliant());
1419 let missing = m.missing();
1420 assert!(missing.contains(&"hdlr"));
1421 assert!(missing.contains(&"pitm"));
1422 }
1423}