1use core::fmt;
19use otf_pixels_core::{PixelsError, Result};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct FourCc(pub [u8; 4]);
28
29impl FourCc {
30 #[must_use]
32 pub const fn new(bytes: &[u8; 4]) -> Self {
33 Self(*bytes)
34 }
35}
36
37impl fmt::Display for FourCc {
38 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct BoxHeader {
56 pub kind: FourCc,
58 pub payload_start: usize,
60 pub payload_len: usize,
62}
63
64impl BoxHeader {
65 #[must_use]
67 pub const fn end(&self) -> usize {
68 self.payload_start.saturating_add(self.payload_len)
71 }
72}
73
74#[derive(Debug, Clone, Copy)]
80pub struct Reader<'a> {
81 file: &'a [u8],
83 end: usize,
85 pos: usize,
87}
88
89impl<'a> Reader<'a> {
90 #[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 #[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 #[must_use]
117 pub const fn file(&self) -> &'a [u8] {
118 self.file
119 }
120
121 #[must_use]
123 pub const fn position(&self) -> usize {
124 self.pos
125 }
126
127 #[must_use]
129 pub const fn remaining(&self) -> usize {
130 self.end.saturating_sub(self.pos)
131 }
132
133 #[must_use]
135 pub const fn is_empty(&self) -> bool {
136 self.remaining() == 0
137 }
138
139 #[must_use]
141 pub fn rest(&self) -> &'a [u8] {
142 self.file.get(self.pos..self.end).unwrap_or(&[])
143 }
144
145 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 pub fn skip(&mut self, len: usize) -> Result<()> {
181 self.take(len).map(|_| ())
182 }
183
184 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 pub fn u16(&mut self) -> Result<u16> {
202 let bytes: [u8; 2] = self.array()?;
203 Ok(u16::from_be_bytes(bytes))
204 }
205
206 pub fn u32(&mut self) -> Result<u32> {
212 let bytes: [u8; 4] = self.array()?;
213 Ok(u32::from_be_bytes(bytes))
214 }
215
216 pub fn u64(&mut self) -> Result<u64> {
222 let bytes: [u8; 8] = self.array()?;
223 Ok(u64::from_be_bytes(bytes))
224 }
225
226 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 pub fn fourcc(&mut self) -> Result<FourCc> {
254 self.array().map(FourCc)
255 }
256
257 fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
259 let bytes = self.take(N)?;
260 let mut out = [0_u8; N];
261 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 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 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 pub fn full_box(&mut self) -> Result<(u8, u32)> {
299 let word = self.u32()?;
300 let version = u8::try_from(word >> 24).unwrap_or(0);
302 Ok((version, word & 0x00ff_ffff))
303 }
304
305 pub fn next_box(&mut self) -> Option<Result<BoxHeader>> {
317 if self.remaining() < 8 {
319 return None;
320 }
321 Some(self.read_box_header())
322 }
323
324 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 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 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 #[must_use]
393 pub fn payload(&self, header: &BoxHeader) -> Reader<'a> {
394 Reader::window(self.file, header.payload_start, header.end())
395 }
396
397 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 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 #[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 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 let error = reader.u32().unwrap_err();
556 assert_eq!(error.code(), ErrorCode::Malformed);
557 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 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}