//! Bounds-checked byte readers and IFF chunk iteration for the format parsers. /// Little-endian reads at an offset, `None` past the end. There is no panicking variant on /// purpose: a parser maps `None` to its own truncation error. pub trait ByteExt { fn u8_at(&self, o: usize) -> Option; fn u16_at(&self, o: usize) -> Option; fn u32_at(&self, o: usize) -> Option; fn i32_at(&self, o: usize) -> Option; fn f32_at(&self, o: usize) -> Option; /// A length-checked sub-slice: `n` bytes starting at `o`. fn bytes_at(&self, o: usize, n: usize) -> Option<&[u8]>; } impl ByteExt for [u8] { #[inline] fn u8_at(&self, o: usize) -> Option { self.get(o).copied() } #[inline] fn u16_at(&self, o: usize) -> Option { Some(u16::from_le_bytes( self.get(o..o.checked_add(2)?)?.try_into().ok()?, )) } #[inline] fn u32_at(&self, o: usize) -> Option { Some(u32::from_le_bytes( self.get(o..o.checked_add(4)?)?.try_into().ok()?, )) } #[inline] fn i32_at(&self, o: usize) -> Option { self.u32_at(o).map(|v| v as i32) } #[inline] fn f32_at(&self, o: usize) -> Option { self.u32_at(o).map(f32::from_bits) } #[inline] fn bytes_at(&self, o: usize, n: usize) -> Option<&[u8]> { self.get(o..o.checked_add(n)?) } } /// Iterate IFF chunks as `(magic, payload)`, the magic as stored: reversed on disk, so MOHD is /// `b"DHOM"`. A truncated header ends iteration and an overrunning payload is clamped to the /// buffer end, a tolerance real 1.12 art needs. pub fn chunks(b: &[u8]) -> impl Iterator { let mut pos = 0usize; std::iter::from_fn(move || { if pos.checked_add(8)? > b.len() { return None; } let magic = [b[pos], b[pos + 1], b[pos + 2], b[pos + 3]]; let size = b.u32_at(pos + 4)? as usize; let start = pos + 8; // Saturating, so a 32-bit target cannot wrap either. let end = start.saturating_add(size).min(b.len()); pos = end; Some((magic, &b[start..end])) }) } /// Cap a count-driven reservation at what `avail` bytes could hold, so a corrupt count reserves at /// most the input's size; the caller still iterates its own count and fails at the checked read. /// An `elem_size` of 0 counts as 1. pub fn capped(count: usize, elem_size: usize, avail: usize) -> usize { count.min(avail / elem_size.max(1)) } #[cfg(test)] mod tests { use super::*; #[test] fn reads_in_bounds() { let b: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05]; assert_eq!(b.u8_at(4), Some(0x05)); assert_eq!(b.u16_at(0), Some(0x0201)); assert_eq!(b.u32_at(1), Some(0x05040302)); assert_eq!(b.i32_at(0), Some(0x04030201)); assert_eq!(b.f32_at(0), Some(f32::from_bits(0x04030201))); assert_eq!(b.bytes_at(2, 3), Some(&b[2..5])); } #[test] fn reads_out_of_bounds_are_none() { let b: &[u8] = &[0x01, 0x02, 0x03]; assert_eq!(b.u8_at(3), None); assert_eq!(b.u16_at(2), None); assert_eq!(b.u32_at(0), None); assert_eq!(b.bytes_at(1, 3), None); // Offsets near usize::MAX must not wrap into a "valid" range. assert_eq!(b.u32_at(usize::MAX - 1), None); assert_eq!(b.bytes_at(usize::MAX, 8), None); } #[test] fn chunk_iteration_walks_well_formed_input() { // Two chunks: "ABCD" (2 bytes) then "EFGH" (0 bytes). let mut b = Vec::new(); b.extend_from_slice(b"ABCD"); b.extend_from_slice(&2u32.to_le_bytes()); b.extend_from_slice(&[0xAA, 0xBB]); b.extend_from_slice(b"EFGH"); b.extend_from_slice(&0u32.to_le_bytes()); let got: Vec<_> = chunks(&b).collect(); assert_eq!(got.len(), 2); assert_eq!((got[0].0, got[0].1), (*b"ABCD", &[0xAA, 0xBB][..])); assert_eq!((got[1].0, got[1].1), (*b"EFGH", &[][..])); } #[test] fn chunk_iteration_is_lenient() { // Over-declared size clamps to the buffer end. let mut b = Vec::new(); b.extend_from_slice(b"ABCD"); b.extend_from_slice(&u32::MAX.to_le_bytes()); b.extend_from_slice(&[0x01]); let got: Vec<_> = chunks(&b).collect(); assert_eq!(got.len(), 1); assert_eq!(got[0].1, &[0x01][..]); // A truncated header (< 8 bytes remaining) stops iteration cleanly. assert_eq!(chunks(&b[..7]).count(), 0); assert_eq!(chunks(&[]).count(), 0); } #[test] fn capped_bounds_reservations() { assert_eq!(capped(10, 4, 1000), 10); // plausible count passes through assert_eq!(capped(u32::MAX as usize, 48, 96), 2); // corrupt count → what fits assert_eq!(capped(5, 0, 16), 5); // elem_size 0 treated as 1, no div-by-zero assert_eq!(capped(5, 4, 0), 0); // empty input reserves nothing } }