Skip to main content

otf_pixels_codec_avif/
boxes.rs

1//! The ISOBMFF box layer: the grammar every other AVIF structure is written in.
2//!
3//! An AVIF file is an ISO base media file (ISO/IEC 14496-12) carrying still
4//! images rather than tracks. Everything is a *box*: a length, a four-character
5//! type, and a payload that is either more boxes or leaf data. This module
6//! implements only that grammar — what the boxes *mean* is [`crate::meta`] and
7//! [`crate::props`].
8//!
9//! # Bounds
10//!
11//! Every read here is checked and returns [`PixelsError::Malformed`] rather
12//! than panicking, because every byte is attacker-controlled. The two classic
13//! ISOBMFF parser failures are a box whose declared size exceeds its parent's
14//! payload, and a box whose declared size is smaller than its own header —
15//! the second of which makes a naive parser loop forever. [`Reader::next_box`]
16//! rejects both.
17
18use core::fmt;
19use otf_pixels_core::{PixelsError, Result};
20
21/// A four-character box or brand identifier.
22///
23/// Compared as bytes, not as text: the specification defines these as four
24/// octets, and some real brands (`MA1A`) are case-sensitive in a way a
25/// lowercased comparison would lose.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct FourCc(pub [u8; 4]);
28
29impl FourCc {
30    /// The identifier for these four bytes.
31    #[must_use]
32    pub const fn new(bytes: &[u8; 4]) -> Self {
33        Self(*bytes)
34    }
35}
36
37impl fmt::Display for FourCc {
38    /// Renders printable ASCII as itself and anything else as an escape, so a
39    /// malformed-box message names the type without emitting control bytes
40    /// into a log.
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        for byte in self.0 {
43            if byte.is_ascii_graphic() || byte == b' ' {
44                write!(f, "{}", byte as char)?;
45            } else {
46                write!(f, "\\x{byte:02x}")?;
47            }
48        }
49        Ok(())
50    }
51}
52
53/// A box's type and the extent of its payload within the file.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct BoxHeader {
56    /// The four-character type.
57    pub kind: FourCc,
58    /// Offset of the payload's first byte, from the start of the file buffer.
59    pub payload_start: usize,
60    /// Length of the payload in bytes, excluding the header.
61    pub payload_len: usize,
62}
63
64impl BoxHeader {
65    /// The offset one past this box's last byte.
66    #[must_use]
67    pub const fn end(&self) -> usize {
68        // Both fields were bounds-checked against the buffer when the header
69        // was parsed, so this cannot overflow a `usize`.
70        self.payload_start.saturating_add(self.payload_len)
71    }
72}
73
74/// A checked cursor over a byte range of the file.
75///
76/// Holds the whole file buffer plus the window this reader is allowed to
77/// touch, so a [`BoxHeader`]'s absolute offsets stay meaningful when a child
78/// reader is handed to a nested parser.
79#[derive(Debug, Clone, Copy)]
80pub struct Reader<'a> {
81    /// The complete file, so absolute offsets from `iloc` resolve.
82    file: &'a [u8],
83    /// One past the last byte this cursor may read, absolute into `file`.
84    end: usize,
85    /// The cursor, as an absolute offset into `file`.
86    pos: usize,
87}
88
89impl<'a> Reader<'a> {
90    /// A reader over the whole of `file`.
91    #[must_use]
92    pub const fn new(file: &'a [u8]) -> Self {
93        Self {
94            file,
95            end: file.len(),
96            pos: 0,
97        }
98    }
99
100    /// A reader over `file[start..end]`, clamped to the file.
101    ///
102    /// Used to resolve an `iloc` extent, which names an absolute range that a
103    /// hostile file may place outside the data it actually shipped.
104    #[must_use]
105    pub fn window(file: &'a [u8], start: usize, end: usize) -> Self {
106        let end = end.min(file.len());
107        let start = start.min(end);
108        Self {
109            file,
110            end,
111            pos: start,
112        }
113    }
114
115    /// The complete file this reader was cut from.
116    #[must_use]
117    pub const fn file(&self) -> &'a [u8] {
118        self.file
119    }
120
121    /// The cursor's absolute offset within the file.
122    #[must_use]
123    pub const fn position(&self) -> usize {
124        self.pos
125    }
126
127    /// Bytes between the cursor and the end of this reader's window.
128    #[must_use]
129    pub const fn remaining(&self) -> usize {
130        self.end.saturating_sub(self.pos)
131    }
132
133    /// Whether the cursor has reached the end of the window.
134    #[must_use]
135    pub const fn is_empty(&self) -> bool {
136        self.remaining() == 0
137    }
138
139    /// The window's bytes from the cursor onward.
140    #[must_use]
141    pub fn rest(&self) -> &'a [u8] {
142        self.file.get(self.pos..self.end).unwrap_or(&[])
143    }
144
145    /// Take `len` bytes, advancing the cursor.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`PixelsError::Malformed`] if the window holds fewer than `len`
150    /// bytes from the cursor.
151    pub fn take(&mut self, len: usize) -> Result<&'a [u8]> {
152        let stop = self.pos.checked_add(len).ok_or_else(|| {
153            PixelsError::malformed(
154                "avif",
155                format!("a {len}-byte read overflows the file offset"),
156            )
157        })?;
158        if stop > self.end {
159            return Err(PixelsError::malformed(
160                "avif",
161                format!(
162                    "a {len}-byte read at offset {} runs past the end of its box, which holds {}",
163                    self.pos,
164                    self.remaining()
165                ),
166            ));
167        }
168        let bytes = self.file.get(self.pos..stop).ok_or_else(|| {
169            PixelsError::malformed("avif", format!("offset {stop} is outside the file"))
170        })?;
171        self.pos = stop;
172        Ok(bytes)
173    }
174
175    /// Advance the cursor by `len` bytes without returning them.
176    ///
177    /// # Errors
178    ///
179    /// As [`Reader::take`].
180    pub fn skip(&mut self, len: usize) -> Result<()> {
181        self.take(len).map(|_| ())
182    }
183
184    /// Read one byte.
185    ///
186    /// # Errors
187    ///
188    /// As [`Reader::take`].
189    pub fn u8(&mut self) -> Result<u8> {
190        self.take(1)?
191            .first()
192            .copied()
193            .ok_or_else(|| PixelsError::malformed("avif", "a one-byte read returned nothing"))
194    }
195
196    /// Read a big-endian `u16`. ISOBMFF is big-endian throughout.
197    ///
198    /// # Errors
199    ///
200    /// As [`Reader::take`].
201    pub fn u16(&mut self) -> Result<u16> {
202        let bytes: [u8; 2] = self.array()?;
203        Ok(u16::from_be_bytes(bytes))
204    }
205
206    /// Read a big-endian `u32`.
207    ///
208    /// # Errors
209    ///
210    /// As [`Reader::take`].
211    pub fn u32(&mut self) -> Result<u32> {
212        let bytes: [u8; 4] = self.array()?;
213        Ok(u32::from_be_bytes(bytes))
214    }
215
216    /// Read a big-endian `u64`.
217    ///
218    /// # Errors
219    ///
220    /// As [`Reader::take`].
221    pub fn u64(&mut self) -> Result<u64> {
222        let bytes: [u8; 8] = self.array()?;
223        Ok(u64::from_be_bytes(bytes))
224    }
225
226    /// Read a big-endian unsigned integer of `size` bytes, where `size` is 0,
227    /// 4 or 8.
228    ///
229    /// `iloc` encodes its offset and length field widths this way, and a width
230    /// of zero means the field is absent and reads as zero.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`PixelsError::Malformed`] for a width the specification does
235    /// not allow, or as [`Reader::take`].
236    pub fn uint(&mut self, size: u8) -> Result<u64> {
237        match size {
238            0 => Ok(0),
239            4 => self.u32().map(u64::from),
240            8 => self.u64(),
241            other => Err(PixelsError::malformed(
242                "avif",
243                format!("field width {other} is not one of 0, 4 or 8"),
244            )),
245        }
246    }
247
248    /// Read a four-character code.
249    ///
250    /// # Errors
251    ///
252    /// As [`Reader::take`].
253    pub fn fourcc(&mut self) -> Result<FourCc> {
254        self.array().map(FourCc)
255    }
256
257    /// Read a fixed-size array.
258    fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
259        let bytes = self.take(N)?;
260        let mut out = [0_u8; N];
261        // `take` returned exactly `N` bytes, so the lengths agree.
262        if bytes.len() != N {
263            return Err(PixelsError::malformed(
264                "avif",
265                format!("a {N}-byte read returned {} bytes", bytes.len()),
266            ));
267        }
268        out.copy_from_slice(bytes);
269        Ok(out)
270    }
271
272    /// Read a null-terminated UTF-8 string.
273    ///
274    /// Used by `infe` for item names and by `auxC` for the auxiliary type URN.
275    /// An unterminated string consumes the rest of the box, which is what
276    /// real files with a missing terminator intend and costs nothing to allow.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`PixelsError::Malformed`] if the bytes are not UTF-8.
281    pub fn cstring(&mut self) -> Result<&'a str> {
282        let rest = self.rest();
283        let len = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
284        let bytes = self.take(len)?;
285        // Step over the terminator when there was one.
286        if len < rest.len() {
287            self.skip(1)?;
288        }
289        core::str::from_utf8(bytes)
290            .map_err(|_| PixelsError::malformed("avif", "a box string is not valid UTF-8"))
291    }
292
293    /// Read the version and flags of a full box.
294    ///
295    /// # Errors
296    ///
297    /// As [`Reader::take`].
298    pub fn full_box(&mut self) -> Result<(u8, u32)> {
299        let word = self.u32()?;
300        // Version is the top octet, flags the low 24 bits.
301        let version = u8::try_from(word >> 24).unwrap_or(0);
302        Ok((version, word & 0x00ff_ffff))
303    }
304
305    /// Read the next box header, positioning the cursor at its payload.
306    ///
307    /// Returns `None` at the end of the window. A trailing run shorter than a
308    /// header is treated as padding and ends iteration rather than failing:
309    /// real files pad, and refusing them buys no safety.
310    ///
311    /// # Errors
312    ///
313    /// Returns [`PixelsError::Malformed`] if a box declares a size smaller
314    /// than its own header — which would make iteration loop forever — or one
315    /// that runs past the end of the enclosing box.
316    pub fn next_box(&mut self) -> Option<Result<BoxHeader>> {
317        // A header is 8 bytes; anything shorter is trailing padding.
318        if self.remaining() < 8 {
319            return None;
320        }
321        Some(self.read_box_header())
322    }
323
324    /// The fallible half of [`Reader::next_box`].
325    fn read_box_header(&mut self) -> Result<BoxHeader> {
326        let start = self.pos;
327        let size32 = self.u32()?;
328        let kind = self.fourcc()?;
329
330        // Size 1 means a 64-bit size follows the type; size 0 means the box
331        // runs to the end of the enclosing box.
332        let (total, header_len) = match size32 {
333            1 => {
334                let large = self.u64()?;
335                let total = usize::try_from(large).map_err(|_| {
336                    PixelsError::malformed(
337                        "avif",
338                        format!("box '{kind}' declares {large} bytes, more than this platform can address"),
339                    )
340                })?;
341                (total, 16_usize)
342            }
343            0 => (self.end.saturating_sub(start), 8_usize),
344            n => (usize::try_from(n).unwrap_or(0), 8_usize),
345        };
346
347        // A `uuid` box carries a 16-byte user type before its payload. We do
348        // not interpret any, but the header length must account for it so the
349        // payload extent is right.
350        let header_len = if kind == FourCc::new(b"uuid") {
351            self.skip(16)?;
352            header_len.saturating_add(16)
353        } else {
354            header_len
355        };
356
357        if total < header_len {
358            return Err(PixelsError::malformed(
359                "avif",
360                format!(
361                    "box '{kind}' declares {total} bytes, less than its own {header_len}-byte header"
362                ),
363            ));
364        }
365        let payload_len = total.saturating_sub(header_len);
366        let payload_start = self.pos;
367        let end = payload_start.checked_add(payload_len).ok_or_else(|| {
368            PixelsError::malformed(
369                "avif",
370                format!("box '{kind}' extends past the address space"),
371            )
372        })?;
373        if end > self.end {
374            return Err(PixelsError::malformed(
375                "avif",
376                format!(
377                    "box '{kind}' declares {total} bytes at offset {start}, running {} past the end of its parent",
378                    end.saturating_sub(self.end)
379                ),
380            ));
381        }
382
383        self.pos = end;
384        Ok(BoxHeader {
385            kind,
386            payload_start,
387            payload_len,
388        })
389    }
390
391    /// A reader over `header`'s payload.
392    #[must_use]
393    pub fn payload(&self, header: &BoxHeader) -> Reader<'a> {
394        Reader::window(self.file, header.payload_start, header.end())
395    }
396
397    /// Find the first child box of type `kind`, if any.
398    ///
399    /// Scans from the cursor and leaves the cursor where it stopped, so this
400    /// is for one-shot lookups rather than repeated probing of one container.
401    ///
402    /// # Errors
403    ///
404    /// As [`Reader::next_box`].
405    pub fn find(&mut self, kind: &[u8; 4]) -> Result<Option<Reader<'a>>> {
406        let wanted = FourCc::new(kind);
407        while let Some(header) = self.next_box() {
408            let header = header?;
409            if header.kind == wanted {
410                return Ok(Some(self.payload(&header)));
411            }
412        }
413        Ok(None)
414    }
415}
416
417#[cfg(test)]
418#[allow(
419    clippy::unwrap_used,
420    clippy::indexing_slicing,
421    reason = "tests operate on known-good values and assert shapes directly"
422)]
423mod tests {
424    use super::*;
425    use otf_pixels_core::ErrorCode;
426
427    /// Build a box: 4-byte size, 4-byte type, payload.
428    fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
429        let mut out = Vec::new();
430        let total = u32::try_from(8 + payload.len()).unwrap();
431        out.extend_from_slice(&total.to_be_bytes());
432        out.extend_from_slice(kind);
433        out.extend_from_slice(payload);
434        out
435    }
436
437    #[test]
438    fn reads_a_flat_sequence_of_boxes() {
439        let mut file = boxed(b"ftyp", b"avif0000");
440        file.extend_from_slice(&boxed(b"mdat", &[1, 2, 3]));
441
442        let mut reader = Reader::new(&file);
443        let first = reader.next_box().unwrap().unwrap();
444        assert_eq!(first.kind, FourCc::new(b"ftyp"));
445        assert_eq!(first.payload_len, 8);
446        assert_eq!(reader.payload(&first).rest(), b"avif0000");
447
448        let second = reader.next_box().unwrap().unwrap();
449        assert_eq!(second.kind, FourCc::new(b"mdat"));
450        assert_eq!(reader.payload(&second).rest(), &[1, 2, 3]);
451
452        assert!(reader.next_box().is_none());
453    }
454
455    #[test]
456    fn nested_boxes_are_bounded_by_their_parent() {
457        let inner = boxed(b"hdlr", b"pict");
458        let outer = boxed(b"meta", &inner);
459
460        let mut reader = Reader::new(&outer);
461        let meta = reader.next_box().unwrap().unwrap();
462        let mut children = reader.payload(&meta);
463        let hdlr = children.next_box().unwrap().unwrap();
464        assert_eq!(hdlr.kind, FourCc::new(b"hdlr"));
465        assert_eq!(children.payload(&hdlr).rest(), b"pict");
466        assert!(children.next_box().is_none());
467    }
468
469    /// A box declaring less than its own header would leave the cursor where
470    /// it was and iterate forever. This is the single most important bound in
471    /// the module.
472    #[test]
473    fn a_box_smaller_than_its_header_is_rejected() {
474        let mut file = Vec::new();
475        file.extend_from_slice(&3_u32.to_be_bytes());
476        file.extend_from_slice(b"junk");
477        file.extend_from_slice(&[0; 16]);
478
479        let mut reader = Reader::new(&file);
480        let error = reader.next_box().unwrap().unwrap_err();
481        assert_eq!(error.code(), ErrorCode::Malformed);
482        assert!(error.to_string().contains("less than its own"), "{error}");
483    }
484
485    #[test]
486    fn a_box_running_past_its_parent_is_rejected() {
487        // Declares 400 bytes inside a file holding 16.
488        let mut file = Vec::new();
489        file.extend_from_slice(&400_u32.to_be_bytes());
490        file.extend_from_slice(b"meta");
491        file.extend_from_slice(&[0; 8]);
492
493        let mut reader = Reader::new(&file);
494        let error = reader.next_box().unwrap().unwrap_err();
495        assert_eq!(error.code(), ErrorCode::Malformed);
496        assert!(error.to_string().contains("past the end"), "{error}");
497    }
498
499    #[test]
500    fn size_zero_runs_to_the_end_of_the_parent() {
501        let mut file = Vec::new();
502        file.extend_from_slice(&0_u32.to_be_bytes());
503        file.extend_from_slice(b"mdat");
504        file.extend_from_slice(&[7; 12]);
505
506        let mut reader = Reader::new(&file);
507        let header = reader.next_box().unwrap().unwrap();
508        assert_eq!(header.payload_len, 12);
509        assert_eq!(reader.payload(&header).rest(), &[7; 12]);
510        assert!(reader.next_box().is_none());
511    }
512
513    #[test]
514    fn a_sixty_four_bit_size_is_honoured() {
515        let mut file = Vec::new();
516        file.extend_from_slice(&1_u32.to_be_bytes());
517        file.extend_from_slice(b"mdat");
518        file.extend_from_slice(&20_u64.to_be_bytes());
519        file.extend_from_slice(&[9; 4]);
520
521        let mut reader = Reader::new(&file);
522        let header = reader.next_box().unwrap().unwrap();
523        assert_eq!(header.payload_len, 4);
524        assert_eq!(reader.payload(&header).rest(), &[9; 4]);
525    }
526
527    #[test]
528    fn a_uuid_box_accounts_for_its_user_type() {
529        let mut payload = Vec::from([0xAB; 16]);
530        payload.extend_from_slice(b"data");
531        let file = boxed(b"uuid", &payload);
532
533        let mut reader = Reader::new(&file);
534        let header = reader.next_box().unwrap().unwrap();
535        assert_eq!(header.kind, FourCc::new(b"uuid"));
536        assert_eq!(reader.payload(&header).rest(), b"data");
537    }
538
539    #[test]
540    fn trailing_padding_ends_iteration_rather_than_failing() {
541        let mut file = boxed(b"ftyp", b"avif");
542        file.extend_from_slice(&[0, 0, 0]);
543
544        let mut reader = Reader::new(&file);
545        assert!(reader.next_box().unwrap().is_ok());
546        assert!(reader.next_box().is_none());
547    }
548
549    #[test]
550    fn scalar_reads_are_bounded() {
551        let file = [0x01, 0x02, 0x03];
552        let mut reader = Reader::new(&file);
553        assert_eq!(reader.u16().unwrap(), 0x0102);
554        // One byte left, but a u32 wants four.
555        let error = reader.u32().unwrap_err();
556        assert_eq!(error.code(), ErrorCode::Malformed);
557        // The failed read did not advance the cursor past the window.
558        assert_eq!(reader.remaining(), 1);
559    }
560
561    #[test]
562    fn full_box_splits_version_from_flags() {
563        let file = [0x01, 0x00, 0x00, 0x0F];
564        let mut reader = Reader::new(&file);
565        let (version, flags) = reader.full_box().unwrap();
566        assert_eq!(version, 1);
567        assert_eq!(flags, 0x0F);
568    }
569
570    #[test]
571    fn uint_honours_the_declared_width() {
572        let file = [0x00, 0x00, 0x01, 0x00];
573        let mut reader = Reader::new(&file);
574        assert_eq!(reader.uint(0).unwrap(), 0);
575        assert_eq!(reader.uint(4).unwrap(), 256);
576
577        let mut reader = Reader::new(&file);
578        let error = reader.uint(3).unwrap_err();
579        assert_eq!(error.code(), ErrorCode::Malformed);
580    }
581
582    #[test]
583    fn cstring_stops_at_the_terminator_and_survives_a_missing_one() {
584        let file = *b"alpha\0beta";
585        let mut reader = Reader::new(&file);
586        assert_eq!(reader.cstring().unwrap(), "alpha");
587        // No terminator on the tail; it reads to the end of the window.
588        assert_eq!(reader.cstring().unwrap(), "beta");
589        assert!(reader.is_empty());
590    }
591
592    #[test]
593    fn a_window_outside_the_file_is_clamped_rather_than_panicking() {
594        let file = [1, 2, 3, 4];
595        let reader = Reader::window(&file, 100, 200);
596        assert!(reader.is_empty());
597        assert_eq!(reader.rest(), &[] as &[u8]);
598    }
599
600    #[test]
601    fn find_locates_a_child_by_type() {
602        let mut payload = boxed(b"hdlr", b"pict");
603        payload.extend_from_slice(&boxed(b"pitm", &[0, 0, 0, 0, 0, 1]));
604        let file = boxed(b"meta", &payload);
605
606        let mut reader = Reader::new(&file);
607        let meta = reader.next_box().unwrap().unwrap();
608        let found = reader.payload(&meta).find(b"pitm").unwrap();
609        assert!(found.is_some());
610        let missing = reader.payload(&meta).find(b"iloc").unwrap();
611        assert!(missing.is_none());
612    }
613
614    #[test]
615    fn fourcc_display_escapes_unprintable_bytes() {
616        assert_eq!(FourCc::new(b"ftyp").to_string(), "ftyp");
617        assert_eq!(
618            FourCc::new(&[0x00, 0x41, 0x1b, 0x42]).to_string(),
619            "\\x00A\\x1bB"
620        );
621    }
622}