use core::fmt;
use otf_pixels_core::{PixelsError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FourCc(pub [u8; 4]);
impl FourCc {
#[must_use]
pub const fn new(bytes: &[u8; 4]) -> Self {
Self(*bytes)
}
}
impl fmt::Display for FourCc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
if byte.is_ascii_graphic() || byte == b' ' {
write!(f, "{}", byte as char)?;
} else {
write!(f, "\\x{byte:02x}")?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoxHeader {
pub kind: FourCc,
pub payload_start: usize,
pub payload_len: usize,
}
impl BoxHeader {
#[must_use]
pub const fn end(&self) -> usize {
self.payload_start.saturating_add(self.payload_len)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Reader<'a> {
file: &'a [u8],
end: usize,
pos: usize,
}
impl<'a> Reader<'a> {
#[must_use]
pub const fn new(file: &'a [u8]) -> Self {
Self {
file,
end: file.len(),
pos: 0,
}
}
#[must_use]
pub fn window(file: &'a [u8], start: usize, end: usize) -> Self {
let end = end.min(file.len());
let start = start.min(end);
Self {
file,
end,
pos: start,
}
}
#[must_use]
pub const fn file(&self) -> &'a [u8] {
self.file
}
#[must_use]
pub const fn position(&self) -> usize {
self.pos
}
#[must_use]
pub const fn remaining(&self) -> usize {
self.end.saturating_sub(self.pos)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.remaining() == 0
}
#[must_use]
pub fn rest(&self) -> &'a [u8] {
self.file.get(self.pos..self.end).unwrap_or(&[])
}
pub fn take(&mut self, len: usize) -> Result<&'a [u8]> {
let stop = self.pos.checked_add(len).ok_or_else(|| {
PixelsError::malformed(
"avif",
format!("a {len}-byte read overflows the file offset"),
)
})?;
if stop > self.end {
return Err(PixelsError::malformed(
"avif",
format!(
"a {len}-byte read at offset {} runs past the end of its box, which holds {}",
self.pos,
self.remaining()
),
));
}
let bytes = self.file.get(self.pos..stop).ok_or_else(|| {
PixelsError::malformed("avif", format!("offset {stop} is outside the file"))
})?;
self.pos = stop;
Ok(bytes)
}
pub fn skip(&mut self, len: usize) -> Result<()> {
self.take(len).map(|_| ())
}
pub fn u8(&mut self) -> Result<u8> {
self.take(1)?
.first()
.copied()
.ok_or_else(|| PixelsError::malformed("avif", "a one-byte read returned nothing"))
}
pub fn u16(&mut self) -> Result<u16> {
let bytes: [u8; 2] = self.array()?;
Ok(u16::from_be_bytes(bytes))
}
pub fn u32(&mut self) -> Result<u32> {
let bytes: [u8; 4] = self.array()?;
Ok(u32::from_be_bytes(bytes))
}
pub fn u64(&mut self) -> Result<u64> {
let bytes: [u8; 8] = self.array()?;
Ok(u64::from_be_bytes(bytes))
}
pub fn uint(&mut self, size: u8) -> Result<u64> {
match size {
0 => Ok(0),
4 => self.u32().map(u64::from),
8 => self.u64(),
other => Err(PixelsError::malformed(
"avif",
format!("field width {other} is not one of 0, 4 or 8"),
)),
}
}
pub fn fourcc(&mut self) -> Result<FourCc> {
self.array().map(FourCc)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
let bytes = self.take(N)?;
let mut out = [0_u8; N];
if bytes.len() != N {
return Err(PixelsError::malformed(
"avif",
format!("a {N}-byte read returned {} bytes", bytes.len()),
));
}
out.copy_from_slice(bytes);
Ok(out)
}
pub fn cstring(&mut self) -> Result<&'a str> {
let rest = self.rest();
let len = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
let bytes = self.take(len)?;
if len < rest.len() {
self.skip(1)?;
}
core::str::from_utf8(bytes)
.map_err(|_| PixelsError::malformed("avif", "a box string is not valid UTF-8"))
}
pub fn full_box(&mut self) -> Result<(u8, u32)> {
let word = self.u32()?;
let version = u8::try_from(word >> 24).unwrap_or(0);
Ok((version, word & 0x00ff_ffff))
}
pub fn next_box(&mut self) -> Option<Result<BoxHeader>> {
if self.remaining() < 8 {
return None;
}
Some(self.read_box_header())
}
fn read_box_header(&mut self) -> Result<BoxHeader> {
let start = self.pos;
let size32 = self.u32()?;
let kind = self.fourcc()?;
let (total, header_len) = match size32 {
1 => {
let large = self.u64()?;
let total = usize::try_from(large).map_err(|_| {
PixelsError::malformed(
"avif",
format!("box '{kind}' declares {large} bytes, more than this platform can address"),
)
})?;
(total, 16_usize)
}
0 => (self.end.saturating_sub(start), 8_usize),
n => (usize::try_from(n).unwrap_or(0), 8_usize),
};
let header_len = if kind == FourCc::new(b"uuid") {
self.skip(16)?;
header_len.saturating_add(16)
} else {
header_len
};
if total < header_len {
return Err(PixelsError::malformed(
"avif",
format!(
"box '{kind}' declares {total} bytes, less than its own {header_len}-byte header"
),
));
}
let payload_len = total.saturating_sub(header_len);
let payload_start = self.pos;
let end = payload_start.checked_add(payload_len).ok_or_else(|| {
PixelsError::malformed(
"avif",
format!("box '{kind}' extends past the address space"),
)
})?;
if end > self.end {
return Err(PixelsError::malformed(
"avif",
format!(
"box '{kind}' declares {total} bytes at offset {start}, running {} past the end of its parent",
end.saturating_sub(self.end)
),
));
}
self.pos = end;
Ok(BoxHeader {
kind,
payload_start,
payload_len,
})
}
#[must_use]
pub fn payload(&self, header: &BoxHeader) -> Reader<'a> {
Reader::window(self.file, header.payload_start, header.end())
}
pub fn find(&mut self, kind: &[u8; 4]) -> Result<Option<Reader<'a>>> {
let wanted = FourCc::new(kind);
while let Some(header) = self.next_box() {
let header = header?;
if header.kind == wanted {
return Ok(Some(self.payload(&header)));
}
}
Ok(None)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
use otf_pixels_core::ErrorCode;
fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let total = u32::try_from(8 + payload.len()).unwrap();
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(payload);
out
}
#[test]
fn reads_a_flat_sequence_of_boxes() {
let mut file = boxed(b"ftyp", b"avif0000");
file.extend_from_slice(&boxed(b"mdat", &[1, 2, 3]));
let mut reader = Reader::new(&file);
let first = reader.next_box().unwrap().unwrap();
assert_eq!(first.kind, FourCc::new(b"ftyp"));
assert_eq!(first.payload_len, 8);
assert_eq!(reader.payload(&first).rest(), b"avif0000");
let second = reader.next_box().unwrap().unwrap();
assert_eq!(second.kind, FourCc::new(b"mdat"));
assert_eq!(reader.payload(&second).rest(), &[1, 2, 3]);
assert!(reader.next_box().is_none());
}
#[test]
fn nested_boxes_are_bounded_by_their_parent() {
let inner = boxed(b"hdlr", b"pict");
let outer = boxed(b"meta", &inner);
let mut reader = Reader::new(&outer);
let meta = reader.next_box().unwrap().unwrap();
let mut children = reader.payload(&meta);
let hdlr = children.next_box().unwrap().unwrap();
assert_eq!(hdlr.kind, FourCc::new(b"hdlr"));
assert_eq!(children.payload(&hdlr).rest(), b"pict");
assert!(children.next_box().is_none());
}
#[test]
fn a_box_smaller_than_its_header_is_rejected() {
let mut file = Vec::new();
file.extend_from_slice(&3_u32.to_be_bytes());
file.extend_from_slice(b"junk");
file.extend_from_slice(&[0; 16]);
let mut reader = Reader::new(&file);
let error = reader.next_box().unwrap().unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("less than its own"), "{error}");
}
#[test]
fn a_box_running_past_its_parent_is_rejected() {
let mut file = Vec::new();
file.extend_from_slice(&400_u32.to_be_bytes());
file.extend_from_slice(b"meta");
file.extend_from_slice(&[0; 8]);
let mut reader = Reader::new(&file);
let error = reader.next_box().unwrap().unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("past the end"), "{error}");
}
#[test]
fn size_zero_runs_to_the_end_of_the_parent() {
let mut file = Vec::new();
file.extend_from_slice(&0_u32.to_be_bytes());
file.extend_from_slice(b"mdat");
file.extend_from_slice(&[7; 12]);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
assert_eq!(header.payload_len, 12);
assert_eq!(reader.payload(&header).rest(), &[7; 12]);
assert!(reader.next_box().is_none());
}
#[test]
fn a_sixty_four_bit_size_is_honoured() {
let mut file = Vec::new();
file.extend_from_slice(&1_u32.to_be_bytes());
file.extend_from_slice(b"mdat");
file.extend_from_slice(&20_u64.to_be_bytes());
file.extend_from_slice(&[9; 4]);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
assert_eq!(header.payload_len, 4);
assert_eq!(reader.payload(&header).rest(), &[9; 4]);
}
#[test]
fn a_uuid_box_accounts_for_its_user_type() {
let mut payload = Vec::from([0xAB; 16]);
payload.extend_from_slice(b"data");
let file = boxed(b"uuid", &payload);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
assert_eq!(header.kind, FourCc::new(b"uuid"));
assert_eq!(reader.payload(&header).rest(), b"data");
}
#[test]
fn trailing_padding_ends_iteration_rather_than_failing() {
let mut file = boxed(b"ftyp", b"avif");
file.extend_from_slice(&[0, 0, 0]);
let mut reader = Reader::new(&file);
assert!(reader.next_box().unwrap().is_ok());
assert!(reader.next_box().is_none());
}
#[test]
fn scalar_reads_are_bounded() {
let file = [0x01, 0x02, 0x03];
let mut reader = Reader::new(&file);
assert_eq!(reader.u16().unwrap(), 0x0102);
let error = reader.u32().unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert_eq!(reader.remaining(), 1);
}
#[test]
fn full_box_splits_version_from_flags() {
let file = [0x01, 0x00, 0x00, 0x0F];
let mut reader = Reader::new(&file);
let (version, flags) = reader.full_box().unwrap();
assert_eq!(version, 1);
assert_eq!(flags, 0x0F);
}
#[test]
fn uint_honours_the_declared_width() {
let file = [0x00, 0x00, 0x01, 0x00];
let mut reader = Reader::new(&file);
assert_eq!(reader.uint(0).unwrap(), 0);
assert_eq!(reader.uint(4).unwrap(), 256);
let mut reader = Reader::new(&file);
let error = reader.uint(3).unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
}
#[test]
fn cstring_stops_at_the_terminator_and_survives_a_missing_one() {
let file = *b"alpha\0beta";
let mut reader = Reader::new(&file);
assert_eq!(reader.cstring().unwrap(), "alpha");
assert_eq!(reader.cstring().unwrap(), "beta");
assert!(reader.is_empty());
}
#[test]
fn a_window_outside_the_file_is_clamped_rather_than_panicking() {
let file = [1, 2, 3, 4];
let reader = Reader::window(&file, 100, 200);
assert!(reader.is_empty());
assert_eq!(reader.rest(), &[] as &[u8]);
}
#[test]
fn find_locates_a_child_by_type() {
let mut payload = boxed(b"hdlr", b"pict");
payload.extend_from_slice(&boxed(b"pitm", &[0, 0, 0, 0, 0, 1]));
let file = boxed(b"meta", &payload);
let mut reader = Reader::new(&file);
let meta = reader.next_box().unwrap().unwrap();
let found = reader.payload(&meta).find(b"pitm").unwrap();
assert!(found.is_some());
let missing = reader.payload(&meta).find(b"iloc").unwrap();
assert!(missing.is_none());
}
#[test]
fn fourcc_display_escapes_unprintable_bytes() {
assert_eq!(FourCc::new(b"ftyp").to_string(), "ftyp");
assert_eq!(
FourCc::new(&[0x00, 0x41, 0x1b, 0x42]).to_string(),
"\\x00A\\x1bB"
);
}
}