Skip to main content

otf_pixels_codec_avif/
meta.rs

1//! The `meta` box: what items a file holds, where their bytes are, and how
2//! they relate.
3//!
4//! A HEIF file is not a picture with some metadata attached — it is a small
5//! database of *items*, one of which is nominated primary. An ordinary AVIF
6//! has one `av01` item and that is the image. A file with transparency has a
7//! second `av01` item carrying the alpha plane, joined to the first by an
8//! `auxl` reference. A large image may have a `grid` item whose pixels are a
9//! `dimg` reference to a list of tile items.
10//!
11//! Four boxes do the work:
12//!
13//! - `pitm` names the primary item.
14//! - `iinf` lists the items and their types.
15//! - `iloc` says where each item's bytes are, as extents in the file or in
16//!   `idat`.
17//! - `iref` records the relationships between items.
18
19use crate::boxes::{FourCc, Reader};
20use crate::props::Properties;
21use otf_pixels_core::{PixelsError, Result};
22use std::borrow::Cow;
23
24/// The auxiliary type URN that marks an item as an alpha plane.
25pub const URN_ALPHA: &str = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
26
27/// An older URN for the same thing, written by encoders that predate the
28/// current registration and still found in the wild.
29pub const URN_ALPHA_LEGACY: &str = "urn:mpeg:hevc:2015:auxid:1";
30
31/// Where an item's bytes live.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Construction {
34    /// Offsets are absolute within the file. This is what almost every AVIF
35    /// uses: the extents point into `mdat`.
36    File,
37    /// Offsets are relative to the start of the `idat` box, which is how small
38    /// items are carried inside `meta` itself. A `grid` item's configuration
39    /// is normally stored this way.
40    Idat,
41    /// Offsets are relative to another item's data.
42    ///
43    /// Legal but vanishingly rare, and it invites reference cycles that a
44    /// resolver has to defend against. Reported [`PixelsError::Unsupported`]
45    /// rather than implemented speculatively.
46    Item,
47}
48
49/// One contiguous run of an item's bytes.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Extent {
52    /// Offset, interpreted according to the item's [`Construction`].
53    pub offset: u64,
54    /// Length in bytes.
55    pub length: u64,
56}
57
58/// One entry of the file's item table.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Item {
61    /// The item's identifier, unique within the file.
62    pub id: u32,
63    /// The item type: `av01` for a coded image, `grid` for a derived tiling,
64    /// `Exif` or `mime` for metadata.
65    pub kind: FourCc,
66    /// The item's name, which is informational.
67    pub name: String,
68    /// Whether the item is marked hidden and so must not be displayed on its
69    /// own. An alpha plane is normally hidden.
70    pub hidden: bool,
71    /// How this item's extents are addressed.
72    pub construction: Construction,
73    /// The runs of bytes that make up the item, in order.
74    pub extents: Vec<Extent>,
75}
76
77impl Item {
78    /// Whether this item is a coded AV1 image.
79    #[must_use]
80    pub fn is_coded_image(&self) -> bool {
81        self.kind == FourCc::new(b"av01")
82    }
83
84    /// Whether this item is a derived grid of tiles.
85    #[must_use]
86    pub fn is_grid(&self) -> bool {
87        self.kind == FourCc::new(b"grid")
88    }
89}
90
91/// One `iref` entry: a typed link from one item to others.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Reference {
94    /// The reference type. `dimg` links a derived image to its inputs, `auxl`
95    /// links an auxiliary item to the item it describes, `thmb` links a
96    /// thumbnail to its full-size image.
97    pub kind: FourCc,
98    /// The item the reference is from.
99    pub from: u32,
100    /// The items it points to, in order — which for `dimg` is the tile order.
101    pub to: Vec<u32>,
102}
103
104/// The parsed `meta` box.
105#[derive(Debug, Clone, Default)]
106pub struct Meta {
107    /// The primary item, from `pitm`.
108    pub primary: Option<u32>,
109    /// Every item in the file, in `iinf` order.
110    pub items: Vec<Item>,
111    /// Every `iref` entry.
112    pub references: Vec<Reference>,
113    /// The item properties, from `iprp`.
114    pub properties: Properties,
115    /// Where the `idat` payload starts in the file, and how long it is.
116    idat: Option<(usize, usize)>,
117}
118
119impl Meta {
120    /// Parse a `meta` box payload.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`PixelsError::Malformed`] if any child box is structurally
125    /// invalid.
126    pub fn parse(mut meta: Reader<'_>) -> Result<Self> {
127        // `meta` is a full box: version and flags come before its children.
128        let (_version, _flags) = meta.full_box()?;
129
130        let mut out = Self::default();
131        // `iloc` gives extents but not types and `iinf` the reverse, so both
132        // are collected and joined once the whole box has been read — the
133        // specification fixes no order between them.
134        let mut locations = Vec::new();
135        let mut infos = Vec::new();
136
137        while let Some(header) = meta.next_box() {
138            let header = header?;
139            let payload = meta.payload(&header);
140            match &header.kind.0 {
141                b"pitm" => out.primary = Some(parse_pitm(payload)?),
142                b"iinf" => infos = parse_iinf(payload)?,
143                b"iloc" => locations = parse_iloc(payload)?,
144                b"iref" => out.references = parse_iref(payload)?,
145                b"iprp" => out.properties = Properties::parse(payload)?,
146                b"idat" => out.idat = Some((header.payload_start, header.payload_len)),
147                _ => {}
148            }
149        }
150
151        out.items = join(infos, locations);
152        Ok(out)
153    }
154
155    /// The item with this ID, if the file has one.
156    #[must_use]
157    pub fn item(&self, id: u32) -> Option<&Item> {
158        self.items.iter().find(|item| item.id == id)
159    }
160
161    /// The primary item, resolved.
162    ///
163    /// Falls back to the first coded image or grid when `pitm` is absent.
164    /// A file without `pitm` is malformed, but the intent is unambiguous when
165    /// there is exactly one image, and rejecting it buys nothing.
166    #[must_use]
167    pub fn primary_item(&self) -> Option<&Item> {
168        self.primary.and_then(|id| self.item(id)).or_else(|| {
169            self.items
170                .iter()
171                .find(|item| item.is_coded_image() || item.is_grid())
172        })
173    }
174
175    /// The items `from` points to with a reference of this type.
176    #[must_use]
177    pub fn referenced(&self, from: u32, kind: &[u8; 4]) -> &[u32] {
178        let wanted = FourCc::new(kind);
179        self.references
180            .iter()
181            .find(|reference| reference.from == from && reference.kind == wanted)
182            .map_or(&[], |reference| reference.to.as_slice())
183    }
184
185    /// The alpha plane item for `id`, if the file carries one.
186    ///
187    /// An alpha item is an auxiliary item whose `auxl` reference points at
188    /// `id` and whose `auxC` property carries the alpha URN. Both halves are
189    /// checked: an `auxl` item with some other auxiliary type is a depth map
190    /// or a gain map, not transparency, and treating it as alpha would produce
191    /// a confidently wrong image.
192    #[must_use]
193    pub fn alpha_item(&self, id: u32) -> Option<&Item> {
194        self.references
195            .iter()
196            .filter(|reference| {
197                reference.kind == FourCc::new(b"auxl") && reference.to.contains(&id)
198            })
199            .find_map(|reference| {
200                let urn = self.properties.auxiliary_type(reference.from)?;
201                if urn == URN_ALPHA || urn == URN_ALPHA_LEGACY {
202                    self.item(reference.from)
203                } else {
204                    None
205                }
206            })
207    }
208
209    /// The bytes of `item`, resolved from `file`.
210    ///
211    /// Borrows when the item is one contiguous extent, which is the common
212    /// case, and concatenates otherwise.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`PixelsError::Unsupported`] for item-relative construction,
217    /// or [`PixelsError::Malformed`] if an extent falls outside the file or
218    /// outside `idat`.
219    pub fn item_data<'a>(&self, file: &'a [u8], item: &Item) -> Result<Cow<'a, [u8]>> {
220        let (base, region) = match item.construction {
221            Construction::File => (0_usize, file),
222            Construction::Idat => {
223                let (start, len) = self.idat.ok_or_else(|| {
224                    PixelsError::malformed(
225                        "avif",
226                        format!(
227                            "item {} is stored in idat, but the file has no idat box",
228                            item.id
229                        ),
230                    )
231                })?;
232                let region = file.get(start..start.saturating_add(len)).ok_or_else(|| {
233                    PixelsError::malformed("avif", "the idat box extends past the file")
234                })?;
235                (0_usize, region)
236            }
237            Construction::Item => {
238                return Err(PixelsError::unsupported(format!(
239                    "avif: item {} uses item-relative extent offsets, which this decoder does not resolve",
240                    item.id
241                )));
242            }
243        };
244        let _ = base;
245
246        // Extents may repeat, so their lengths can sum past the file size even
247        // though each is individually in range. Bound the total before any of
248        // it is copied.
249        let total: u64 = item.extents.iter().map(|extent| extent.length).sum();
250        if total > region.len() as u64 {
251            return Err(PixelsError::malformed(
252                "avif",
253                format!(
254                    "item {} declares {total} bytes across {} extents, more than the {} available",
255                    item.id,
256                    item.extents.len(),
257                    region.len()
258                ),
259            ));
260        }
261
262        let slice_of = |extent: &Extent| -> Result<&'a [u8]> {
263            let start = usize::try_from(extent.offset).map_err(|_| {
264                PixelsError::malformed(
265                    "avif",
266                    format!(
267                        "item {} starts at offset {}, beyond this platform's addressing",
268                        item.id, extent.offset
269                    ),
270                )
271            })?;
272            let len = usize::try_from(extent.length).map_err(|_| {
273                PixelsError::malformed(
274                    "avif",
275                    format!(
276                        "item {} declares a {}-byte extent, beyond this platform's addressing",
277                        item.id, extent.length
278                    ),
279                )
280            })?;
281            let end = start.checked_add(len).ok_or_else(|| {
282                PixelsError::malformed("avif", format!("item {}'s extent overflows", item.id))
283            })?;
284            region.get(start..end).ok_or_else(|| {
285                PixelsError::malformed(
286                    "avif",
287                    format!(
288                        "item {} has an extent at {start}..{end}, outside the {} bytes available",
289                        item.id,
290                        region.len()
291                    ),
292                )
293            })
294        };
295
296        match item.extents.as_slice() {
297            [] => Err(PixelsError::malformed(
298                "avif",
299                format!("item {} has no extents, so it has no data", item.id),
300            )),
301            [single] => slice_of(single).map(Cow::Borrowed),
302            many => {
303                let mut joined = Vec::with_capacity(usize::try_from(total).unwrap_or(0));
304                for extent in many {
305                    joined.extend_from_slice(slice_of(extent)?);
306                }
307                Ok(Cow::Owned(joined))
308            }
309        }
310    }
311}
312
313/// Join the type table from `iinf` with the extent table from `iloc`.
314///
315/// An item present in one but not the other is dropped: without a type it
316/// cannot be interpreted, and without extents it has no bytes.
317fn join(infos: Vec<(u32, FourCc, String, bool)>, locations: Vec<Located>) -> Vec<Item> {
318    infos
319        .into_iter()
320        .filter_map(|(id, kind, name, hidden)| {
321            let located = locations.iter().find(|located| located.id == id)?;
322            Some(Item {
323                id,
324                kind,
325                name,
326                hidden,
327                construction: located.construction,
328                extents: located.extents.clone(),
329            })
330        })
331        .collect()
332}
333
334/// An `iloc` row, before it is joined with its type.
335#[derive(Debug, Clone)]
336struct Located {
337    id: u32,
338    construction: Construction,
339    extents: Vec<Extent>,
340}
341
342/// Parse `pitm`, the primary item declaration.
343fn parse_pitm(mut payload: Reader<'_>) -> Result<u32> {
344    let (version, _flags) = payload.full_box()?;
345    if version == 0 {
346        payload.u16().map(u32::from)
347    } else {
348        payload.u32()
349    }
350}
351
352/// Parse `iinf` into `(id, type, name, hidden)` rows.
353fn parse_iinf(mut payload: Reader<'_>) -> Result<Vec<(u32, FourCc, String, bool)>> {
354    let (version, _flags) = payload.full_box()?;
355    let count = if version == 0 {
356        u32::from(payload.u16()?)
357    } else {
358        payload.u32()?
359    };
360
361    // Each `infe` is a box, so at least eight bytes. Bound the count against
362    // the box's real size before reserving anything.
363    if u64::from(count) * 8 > payload.remaining() as u64 {
364        return Err(PixelsError::malformed(
365            "avif",
366            format!(
367                "iinf declares {count} items, more than its {} remaining bytes can hold",
368                payload.remaining()
369            ),
370        ));
371    }
372
373    let mut out = Vec::with_capacity(count as usize);
374    while let Some(header) = payload.next_box() {
375        let header = header?;
376        if header.kind != FourCc::new(b"infe") {
377            continue;
378        }
379        out.push(parse_infe(payload.payload(&header))?);
380    }
381    Ok(out)
382}
383
384/// Parse one `infe` item information entry.
385fn parse_infe(mut payload: Reader<'_>) -> Result<(u32, FourCc, String, bool)> {
386    let (version, flags) = payload.full_box()?;
387    // Versions 0 and 1 predate typed items and cannot describe an `av01`
388    // item at all; every AVIF uses version 2 or 3.
389    if version < 2 {
390        return Err(PixelsError::malformed(
391            "avif",
392            format!("infe version {version} cannot carry an item type"),
393        ));
394    }
395    let id = if version == 2 {
396        u32::from(payload.u16()?)
397    } else {
398        payload.u32()?
399    };
400    let _protection = payload.u16()?;
401    let kind = payload.fourcc()?;
402    let name = payload.cstring()?.to_owned();
403    // Flag bit 0 marks the item hidden.
404    let hidden = flags & 1 == 1;
405    Ok((id, kind, name, hidden))
406}
407
408/// Parse `iloc`, the item location table.
409fn parse_iloc(mut payload: Reader<'_>) -> Result<Vec<Located>> {
410    let (version, _flags) = payload.full_box()?;
411
412    let sizes = payload.u8()?;
413    let offset_size = sizes >> 4;
414    let length_size = sizes & 0x0f;
415    let sizes = payload.u8()?;
416    let base_offset_size = sizes >> 4;
417    // The low nibble is the index size on versions 1 and 2, reserved on 0.
418    let index_size = if version == 1 || version == 2 {
419        sizes & 0x0f
420    } else {
421        0
422    };
423
424    let count = match version {
425        0 | 1 => u32::from(payload.u16()?),
426        2 => payload.u32()?,
427        other => {
428            return Err(PixelsError::malformed(
429                "avif",
430                format!("iloc version {other} is not one this format defines"),
431            ));
432        }
433    };
434
435    // Every row costs at least an ID, a data reference index and an extent
436    // count: six bytes on version 0. Bound before reserving.
437    if u64::from(count) * 6 > payload.remaining() as u64 {
438        return Err(PixelsError::malformed(
439            "avif",
440            format!(
441                "iloc declares {count} items, more than its {} remaining bytes can hold",
442                payload.remaining()
443            ),
444        ));
445    }
446
447    let mut out = Vec::with_capacity(count as usize);
448    for _ in 0..count {
449        let id = if version < 2 {
450            u32::from(payload.u16()?)
451        } else {
452            payload.u32()?
453        };
454
455        let construction = if version == 1 || version == 2 {
456            // Twelve reserved bits then a four-bit construction method.
457            let word = payload.u16()?;
458            match word & 0x0f {
459                0 => Construction::File,
460                1 => Construction::Idat,
461                2 => Construction::Item,
462                other => {
463                    return Err(PixelsError::malformed(
464                        "avif",
465                        format!("iloc construction method {other} is not one this format defines"),
466                    ));
467                }
468            }
469        } else {
470            Construction::File
471        };
472
473        let _data_reference_index = payload.u16()?;
474        let base_offset = payload.uint(base_offset_size)?;
475        let extent_count = payload.u16()?;
476
477        // An extent costs at least the offset and length widths declared in
478        // the header, so a zero-width declaration would let a huge count cost
479        // nothing to declare. Charge a minimum of one byte per extent.
480        let per_extent = u64::from(offset_size)
481            .saturating_add(u64::from(length_size))
482            .saturating_add(u64::from(index_size))
483            .max(1);
484        if u64::from(extent_count) * per_extent > payload.remaining() as u64 {
485            return Err(PixelsError::malformed(
486                "avif",
487                format!(
488                    "item {id} declares {extent_count} extents, more than its {} remaining bytes can hold",
489                    payload.remaining()
490                ),
491            ));
492        }
493
494        let mut extents = Vec::with_capacity(usize::from(extent_count));
495        for _ in 0..extent_count {
496            if index_size > 0 {
497                let _extent_index = payload.uint(index_size)?;
498            }
499            let offset = payload.uint(offset_size)?;
500            let length = payload.uint(length_size)?;
501            // The base offset is added here so that everything downstream sees
502            // one absolute number.
503            let offset = base_offset.checked_add(offset).ok_or_else(|| {
504                PixelsError::malformed(
505                    "avif",
506                    format!("item {id} has an extent offset that overflows its base"),
507                )
508            })?;
509            extents.push(Extent { offset, length });
510        }
511
512        out.push(Located {
513            id,
514            construction,
515            extents,
516        });
517    }
518    Ok(out)
519}
520
521/// Parse `iref`, the item reference box.
522fn parse_iref(mut payload: Reader<'_>) -> Result<Vec<Reference>> {
523    let (version, _flags) = payload.full_box()?;
524    let mut out = Vec::new();
525
526    while let Some(header) = payload.next_box() {
527        let header = header?;
528        let mut reference = payload.payload(&header);
529        // Version 0 uses 16-bit item IDs throughout, version 1 uses 32-bit.
530        let from = if version == 0 {
531            u32::from(reference.u16()?)
532        } else {
533            reference.u32()?
534        };
535        let count = reference.u16()?;
536        let width = if version == 0 { 2_u64 } else { 4 };
537        if u64::from(count) * width > reference.remaining() as u64 {
538            return Err(PixelsError::malformed(
539                "avif",
540                format!(
541                    "a '{}' reference declares {count} targets, more than its {} remaining bytes can hold",
542                    header.kind,
543                    reference.remaining()
544                ),
545            ));
546        }
547        let mut to = Vec::with_capacity(usize::from(count));
548        for _ in 0..count {
549            to.push(if version == 0 {
550                u32::from(reference.u16()?)
551            } else {
552                reference.u32()?
553            });
554        }
555        out.push(Reference {
556            kind: header.kind,
557            from,
558            to,
559        });
560    }
561    Ok(out)
562}
563
564#[cfg(test)]
565#[allow(
566    clippy::unwrap_used,
567    clippy::indexing_slicing,
568    reason = "tests operate on known-good values and assert shapes directly"
569)]
570mod tests {
571    use super::*;
572    use otf_pixels_core::ErrorCode;
573
574    fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
575        let mut out = Vec::new();
576        let total = u32::try_from(8 + payload.len()).unwrap();
577        out.extend_from_slice(&total.to_be_bytes());
578        out.extend_from_slice(kind);
579        out.extend_from_slice(payload);
580        out
581    }
582
583    /// An `infe` version 2 entry.
584    fn infe(id: u16, kind: &[u8; 4], name: &str, hidden: bool) -> Vec<u8> {
585        let mut payload = vec![2, 0, 0, if hidden { 1 } else { 0 }];
586        payload.extend_from_slice(&id.to_be_bytes());
587        payload.extend_from_slice(&[0, 0]);
588        payload.extend_from_slice(kind);
589        payload.extend_from_slice(name.as_bytes());
590        payload.push(0);
591        boxed(b"infe", &payload)
592    }
593
594    /// An `iinf` version 0 wrapping the given entries.
595    fn iinf(entries: &[Vec<u8>]) -> Vec<u8> {
596        let mut payload = vec![0, 0, 0, 0];
597        payload.extend_from_slice(&u16::try_from(entries.len()).unwrap().to_be_bytes());
598        for entry in entries {
599            payload.extend_from_slice(entry);
600        }
601        boxed(b"iinf", &payload)
602    }
603
604    /// An `iloc` version 0 with 32-bit offsets and lengths, one extent each.
605    fn iloc(rows: &[(u16, u32, u32)]) -> Vec<u8> {
606        let mut payload = vec![0, 0, 0, 0];
607        payload.push(0x44); // offset_size 4, length_size 4
608        payload.push(0x00); // base_offset_size 0, reserved
609        payload.extend_from_slice(&u16::try_from(rows.len()).unwrap().to_be_bytes());
610        for (id, offset, length) in rows {
611            payload.extend_from_slice(&id.to_be_bytes());
612            payload.extend_from_slice(&[0, 0]); // data reference index
613            payload.extend_from_slice(&1_u16.to_be_bytes()); // extent count
614            payload.extend_from_slice(&offset.to_be_bytes());
615            payload.extend_from_slice(&length.to_be_bytes());
616        }
617        boxed(b"iloc", &payload)
618    }
619
620    fn pitm(id: u16) -> Vec<u8> {
621        let mut payload = vec![0, 0, 0, 0];
622        payload.extend_from_slice(&id.to_be_bytes());
623        boxed(b"pitm", &payload)
624    }
625
626    fn meta_box(children: &[Vec<u8>]) -> Vec<u8> {
627        let mut payload = vec![0, 0, 0, 0];
628        for child in children {
629            payload.extend_from_slice(child);
630        }
631        boxed(b"meta", &payload)
632    }
633
634    fn parse(file: &[u8]) -> Result<Meta> {
635        let mut reader = Reader::new(file);
636        let header = reader.next_box().unwrap().unwrap();
637        Meta::parse(reader.payload(&header))
638    }
639
640    #[test]
641    fn joins_the_type_table_with_the_extent_table() {
642        let file = meta_box(&[
643            pitm(1),
644            iinf(&[infe(1, b"av01", "color", false)]),
645            iloc(&[(1, 100, 42)]),
646        ]);
647        let meta = parse(&file).unwrap();
648
649        assert_eq!(meta.primary, Some(1));
650        assert_eq!(meta.items.len(), 1);
651        let item = &meta.items[0];
652        assert_eq!(item.id, 1);
653        assert!(item.is_coded_image());
654        assert_eq!(item.name, "color");
655        assert!(!item.hidden);
656        assert_eq!(item.construction, Construction::File);
657        assert_eq!(
658            item.extents,
659            vec![Extent {
660                offset: 100,
661                length: 42
662            }]
663        );
664    }
665
666    /// An item listed in `iinf` but absent from `iloc` has no bytes, and one
667    /// in `iloc` but not `iinf` has no type. Neither is decodable, so neither
668    /// survives the join.
669    #[test]
670    fn an_item_missing_from_either_table_is_dropped() {
671        let file = meta_box(&[
672            iinf(&[infe(1, b"av01", "a", false), infe(2, b"av01", "b", false)]),
673            iloc(&[(1, 0, 4), (3, 0, 4)]),
674        ]);
675        let meta = parse(&file).unwrap();
676        assert_eq!(meta.items.len(), 1);
677        assert_eq!(meta.items[0].id, 1);
678    }
679
680    /// Build a file whose `meta` box is followed by `data`, with `build`
681    /// given the absolute offset `data` will land at.
682    ///
683    /// The offset has to be known before the box is built, but the box's
684    /// length depends only on its shape and not on the offset value, so
685    /// building it twice settles the circularity.
686    fn file_with_data(build: impl Fn(u32) -> Vec<u8>, data: &[u8]) -> Vec<u8> {
687        let probe = build(0);
688        let base = u32::try_from(probe.len()).unwrap();
689        let mut file = build(base);
690        assert_eq!(file.len(), probe.len(), "the offset changed the box length");
691        file.extend_from_slice(data);
692        file
693    }
694
695    #[test]
696    fn resolves_item_data_by_borrowing_a_single_extent() {
697        let file = file_with_data(
698            |base| meta_box(&[iinf(&[infe(1, b"av01", "", false)]), iloc(&[(1, base, 3)])]),
699            &[0xDE, 0xAD, 0xBE],
700        );
701
702        let meta = parse(&file).unwrap();
703        let data = meta.item_data(&file, &meta.items[0]).unwrap();
704        assert_eq!(&*data, &[0xDE, 0xAD, 0xBE]);
705        assert!(matches!(data, Cow::Borrowed(_)));
706    }
707
708    #[test]
709    fn concatenates_multiple_extents() {
710        // Two extents, two bytes each, with two bytes of filler between them.
711        let file = file_with_data(
712            |base| {
713                let mut payload = vec![0, 0, 0, 0, 0x44, 0x00];
714                payload.extend_from_slice(&1_u16.to_be_bytes()); // one item
715                payload.extend_from_slice(&1_u16.to_be_bytes()); // item ID 1
716                payload.extend_from_slice(&[0, 0]);
717                payload.extend_from_slice(&2_u16.to_be_bytes()); // two extents
718                payload.extend_from_slice(&base.to_be_bytes());
719                payload.extend_from_slice(&2_u32.to_be_bytes());
720                payload.extend_from_slice(&(base + 4).to_be_bytes());
721                payload.extend_from_slice(&2_u32.to_be_bytes());
722                meta_box(&[
723                    iinf(&[infe(1, b"av01", "", false)]),
724                    boxed(b"iloc", &payload),
725                ])
726            },
727            &[1, 2, 0xFF, 0xFF, 3, 4],
728        );
729
730        let meta = parse(&file).unwrap();
731        let data = meta.item_data(&file, &meta.items[0]).unwrap();
732        assert_eq!(&*data, &[1, 2, 3, 4]);
733        assert!(matches!(data, Cow::Owned(_)));
734    }
735
736    #[test]
737    fn an_extent_outside_the_file_is_rejected() {
738        let file = meta_box(&[
739            iinf(&[infe(1, b"av01", "", false)]),
740            iloc(&[(1, 0, 100_000)]),
741        ]);
742        let meta = parse(&file).unwrap();
743        let error = meta.item_data(&file, &meta.items[0]).unwrap_err();
744        assert_eq!(error.code(), ErrorCode::Malformed);
745    }
746
747    /// Extents may repeat, so the sum of their lengths can exceed the file
748    /// even when each is individually in range. Without the total check, a
749    /// small file could name an arbitrarily large allocation.
750    #[test]
751    fn repeated_extents_cannot_sum_past_the_file() {
752        let mut payload = vec![0, 0, 0, 0, 0x44, 0x00];
753        payload.extend_from_slice(&1_u16.to_be_bytes());
754        payload.extend_from_slice(&1_u16.to_be_bytes());
755        payload.extend_from_slice(&[0, 0]);
756        payload.extend_from_slice(&64_u16.to_be_bytes()); // 64 extents
757        for _ in 0..64 {
758            payload.extend_from_slice(&0_u32.to_be_bytes());
759            payload.extend_from_slice(&40_u32.to_be_bytes());
760        }
761        let iloc = boxed(b"iloc", &payload);
762        let file = meta_box(&[iinf(&[infe(1, b"av01", "", false)]), iloc]);
763
764        let meta = parse(&file).unwrap();
765        let error = meta.item_data(&file, &meta.items[0]).unwrap_err();
766        assert_eq!(error.code(), ErrorCode::Malformed);
767        assert!(error.to_string().contains("more than the"), "{error}");
768    }
769
770    #[test]
771    fn idat_construction_resolves_against_the_idat_box() {
772        // iloc version 1 carries a construction method.
773        let mut payload = vec![1, 0, 0, 0, 0x44, 0x00];
774        payload.extend_from_slice(&1_u16.to_be_bytes());
775        payload.extend_from_slice(&1_u16.to_be_bytes()); // item ID
776        payload.extend_from_slice(&1_u16.to_be_bytes()); // construction: idat
777        payload.extend_from_slice(&[0, 0]);
778        payload.extend_from_slice(&1_u16.to_be_bytes()); // one extent
779        payload.extend_from_slice(&2_u32.to_be_bytes()); // offset within idat
780        payload.extend_from_slice(&2_u32.to_be_bytes());
781        let iloc = boxed(b"iloc", &payload);
782
783        let idat = boxed(b"idat", &[9, 9, 0xC0, 0xDE]);
784        let file = meta_box(&[iinf(&[infe(1, b"grid", "", false)]), iloc, idat]);
785
786        let meta = parse(&file).unwrap();
787        let data = meta.item_data(&file, &meta.items[0]).unwrap();
788        assert_eq!(&*data, &[0xC0, 0xDE]);
789    }
790
791    #[test]
792    fn item_relative_construction_is_unsupported_rather_than_guessed() {
793        let mut payload = vec![1, 0, 0, 0, 0x44, 0x00];
794        payload.extend_from_slice(&1_u16.to_be_bytes());
795        payload.extend_from_slice(&1_u16.to_be_bytes());
796        payload.extend_from_slice(&2_u16.to_be_bytes()); // construction: item
797        payload.extend_from_slice(&[0, 0]);
798        payload.extend_from_slice(&1_u16.to_be_bytes());
799        payload.extend_from_slice(&0_u32.to_be_bytes());
800        payload.extend_from_slice(&1_u32.to_be_bytes());
801        let iloc = boxed(b"iloc", &payload);
802        let file = meta_box(&[iinf(&[infe(1, b"av01", "", false)]), iloc]);
803
804        let meta = parse(&file).unwrap();
805        let error = meta.item_data(&file, &meta.items[0]).unwrap_err();
806        assert_eq!(error.code(), ErrorCode::Unsupported);
807    }
808
809    #[test]
810    fn references_are_read_and_queryable() {
811        let mut dimg = vec![];
812        dimg.extend_from_slice(&1_u16.to_be_bytes()); // from item 1
813        dimg.extend_from_slice(&2_u16.to_be_bytes()); // two targets
814        dimg.extend_from_slice(&2_u16.to_be_bytes());
815        dimg.extend_from_slice(&3_u16.to_be_bytes());
816
817        let mut payload = vec![0, 0, 0, 0];
818        payload.extend_from_slice(&boxed(b"dimg", &dimg));
819        let iref = boxed(b"iref", &payload);
820
821        let file = meta_box(&[
822            iinf(&[infe(1, b"grid", "", false)]),
823            iloc(&[(1, 0, 1)]),
824            iref,
825        ]);
826        let meta = parse(&file).unwrap();
827        assert_eq!(meta.referenced(1, b"dimg"), &[2, 3]);
828        assert_eq!(meta.referenced(1, b"auxl"), &[] as &[u32]);
829        assert_eq!(meta.referenced(9, b"dimg"), &[] as &[u32]);
830    }
831
832    /// An `auxl` item whose auxiliary type is not alpha is a depth or gain
833    /// map. Treating it as transparency would silently wreck the image.
834    #[test]
835    fn only_an_alpha_urn_makes_an_auxiliary_item_the_alpha_plane() {
836        fn build(urn: &str) -> Vec<u8> {
837            let mut auxl = vec![];
838            auxl.extend_from_slice(&2_u16.to_be_bytes()); // from item 2
839            auxl.extend_from_slice(&1_u16.to_be_bytes());
840            auxl.extend_from_slice(&1_u16.to_be_bytes()); // to item 1
841            let mut iref_payload = vec![0, 0, 0, 0];
842            iref_payload.extend_from_slice(&boxed(b"auxl", &auxl));
843
844            let mut auxc_payload = vec![0, 0, 0, 0];
845            auxc_payload.extend_from_slice(urn.as_bytes());
846            auxc_payload.push(0);
847            let ipco = boxed(b"auxC", &auxc_payload);
848            // Item 2 associates property 1.
849            let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 1, 0x01];
850            let mut iprp = boxed(b"ipco", &ipco);
851            iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
852
853            meta_box(&[
854                pitm(1),
855                iinf(&[
856                    infe(1, b"av01", "color", false),
857                    infe(2, b"av01", "alpha", true),
858                ]),
859                iloc(&[(1, 0, 1), (2, 0, 1)]),
860                boxed(b"iref", &iref_payload),
861                boxed(b"iprp", &iprp),
862            ])
863        }
864
865        let file = build(URN_ALPHA);
866        let meta = parse(&file).unwrap();
867        assert_eq!(meta.alpha_item(1).map(|item| item.id), Some(2));
868
869        let file = build(URN_ALPHA_LEGACY);
870        let meta = parse(&file).unwrap();
871        assert_eq!(meta.alpha_item(1).map(|item| item.id), Some(2));
872
873        let file = build("urn:mpeg:mpegB:cicp:systems:auxiliary:depth");
874        let meta = parse(&file).unwrap();
875        assert!(
876            meta.alpha_item(1).is_none(),
877            "a depth map must not be mistaken for an alpha plane"
878        );
879    }
880
881    #[test]
882    fn a_missing_pitm_falls_back_to_the_first_image_item() {
883        let file = meta_box(&[iinf(&[infe(4, b"av01", "", false)]), iloc(&[(4, 0, 1)])]);
884        let meta = parse(&file).unwrap();
885        assert_eq!(meta.primary, None);
886        assert_eq!(meta.primary_item().map(|item| item.id), Some(4));
887    }
888
889    #[test]
890    fn a_huge_declared_item_count_is_rejected_against_the_box_size() {
891        let mut payload = vec![0, 0, 0, 0, 0x44, 0x00];
892        payload.extend_from_slice(&0xFFFF_u16.to_be_bytes());
893        let file = meta_box(&[boxed(b"iloc", &payload)]);
894        let error = parse(&file).unwrap_err();
895        assert_eq!(error.code(), ErrorCode::Malformed);
896        assert!(error.to_string().contains("more than its"), "{error}");
897    }
898
899    #[test]
900    fn an_infe_too_old_to_carry_a_type_is_rejected() {
901        let mut payload = vec![1, 0, 0, 0];
902        payload.extend_from_slice(&1_u16.to_be_bytes());
903        payload.extend_from_slice(&[0, 0]);
904        let file = meta_box(&[iinf(&[boxed(b"infe", &payload)])]);
905        let error = parse(&file).unwrap_err();
906        assert_eq!(error.code(), ErrorCode::Malformed);
907        assert!(error.to_string().contains("item type"), "{error}");
908    }
909
910    #[test]
911    fn an_undefined_construction_method_is_rejected() {
912        let mut payload = vec![1, 0, 0, 0, 0x44, 0x00];
913        payload.extend_from_slice(&1_u16.to_be_bytes());
914        payload.extend_from_slice(&1_u16.to_be_bytes());
915        payload.extend_from_slice(&7_u16.to_be_bytes()); // no such method
916        payload.extend_from_slice(&[0, 0]);
917        payload.extend_from_slice(&0_u16.to_be_bytes());
918        let file = meta_box(&[boxed(b"iloc", &payload)]);
919        let error = parse(&file).unwrap_err();
920        assert_eq!(error.code(), ErrorCode::Malformed);
921        assert!(error.to_string().contains("construction method"), "{error}");
922    }
923}