Part of #41: the comments of the thirteen small crates (`benilla-adt`, `-blp`, `-buildstamp`, `-bytes`, `-dbc`, `-m2`, `-mpq`, `-srp`, `-visual`, `-wdt`, `-wmo`, `-worldview` and `benilla`) rewritten to the rule in `docs/METHOD.md`: what the code does and the reference fact behind it, in a line. 2,176 comment lines become 1,029. - The citations that were only a private record number go first, mechanically, each edit shown to be a pure removal. - The rewrite corrects the comments that stated wrong facts: the M2 header's selection-ring claim, the texture-transform and MCNK impassable-flag notes, a DBC reader note that said short records zero-pad (they do not, #17), and about twenty more found on the way. The SRP handshake's redraw and redial are marked as a deviation, with the measured reason. - Verified: every file strips to identical code, and `cargo fmt --check` is clean. A second reader compared every file old against new and restored the facts the rewrite had dropped. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
137 lines
4.9 KiB
Rust
137 lines
4.9 KiB
Rust
//! 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<u8>;
|
|
fn u16_at(&self, o: usize) -> Option<u16>;
|
|
fn u32_at(&self, o: usize) -> Option<u32>;
|
|
fn i32_at(&self, o: usize) -> Option<i32>;
|
|
fn f32_at(&self, o: usize) -> Option<f32>;
|
|
/// 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<u8> {
|
|
self.get(o).copied()
|
|
}
|
|
#[inline]
|
|
fn u16_at(&self, o: usize) -> Option<u16> {
|
|
Some(u16::from_le_bytes(
|
|
self.get(o..o.checked_add(2)?)?.try_into().ok()?,
|
|
))
|
|
}
|
|
#[inline]
|
|
fn u32_at(&self, o: usize) -> Option<u32> {
|
|
Some(u32::from_le_bytes(
|
|
self.get(o..o.checked_add(4)?)?.try_into().ok()?,
|
|
))
|
|
}
|
|
#[inline]
|
|
fn i32_at(&self, o: usize) -> Option<i32> {
|
|
self.u32_at(o).map(|v| v as i32)
|
|
}
|
|
#[inline]
|
|
fn f32_at(&self, o: usize) -> Option<f32> {
|
|
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<Item = ([u8; 4], &[u8])> {
|
|
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
|
|
}
|
|
}
|