Parse the collision container framing
Collision sits inside core_data at the offset the core index gives, behind a 0x40-byte header that self-describes: it carries its own size and the payload size. That pairing is the invariant -- the declared header size is exactly 0x40 on all 27 levels and header + payload stays inside the lump. 41,481,520 payload bytes across the 19 levels that have any; 8 declare zero length, which is present-but-empty rather than absent, so the two are counted separately. The cells inside the payload are not decoded. They do not walk as fixed-stride cells, and treating faces as variable-length records stops partway into the first cell on level 0 rather than tiling the payload. The first cell reads as 11 verts and 33 faces under every hypothesis tried, which is exactly the kind of agreement that looks like a result and is not one -- three different strides all produce the same first cell and all of them are wrong. Left open rather than committed as a guess.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
//! Collision: container framing.
|
||||
//!
|
||||
//! Spec: `docs/formats/collision.md`.
|
||||
//!
|
||||
//! Collision lives inside the level's `core_data`, at the offset the core
|
||||
//! index gives. Every level begins it with a 0x40-byte header holding two
|
||||
//! `u32` -- the header's own size and the payload's -- and the rest of the
|
||||
//! header is zero. That pairing is the invariant: **the declared header size is
|
||||
//! exactly 0x40 and the payload stays inside the lump on all 27 levels**.
|
||||
//!
|
||||
//! The cells inside the payload are *not* decoded here. That layout is open:
|
||||
//! the payload does not walk as fixed-stride cells, and treating faces as
|
||||
//! variable-length records stops partway into the first cell on level 0 rather
|
||||
//! than tiling the payload. Getting that wrong in a way that still "looks
|
||||
//! plausible" is the specific failure this crate is written to avoid, so the
|
||||
//! framing is what gets committed and the cell layout stays marked open.
|
||||
|
||||
use crate::buf::{Buf, Error, ErrorKind, Result};
|
||||
|
||||
/// The measured size of a collision header.
|
||||
pub const COLLISION_HEADER_SIZE: u32 = 0x40;
|
||||
|
||||
/// The head of a collision region.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CollisionHeader {
|
||||
/// Size of this header; 0x40 on every level.
|
||||
pub header_size: u32,
|
||||
/// Size of the payload that follows.
|
||||
pub payload_size: u32,
|
||||
}
|
||||
|
||||
impl CollisionHeader {
|
||||
/// Parse the 8 meaningful bytes at the head.
|
||||
///
|
||||
/// The remaining 0x38 bytes are zero on all 27 levels, but they are not
|
||||
/// required to be: only the two fields below are load-bearing.
|
||||
pub fn parse(bytes: &[u8]) -> Result<CollisionHeader> {
|
||||
let mut b = Buf::new(bytes);
|
||||
Ok(CollisionHeader {
|
||||
header_size: b.u32("collision header size")?,
|
||||
payload_size: b.u32("collision payload size")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A collision region, sliced out of `core_data`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Collision<'a> {
|
||||
pub header: CollisionHeader,
|
||||
/// The whole header, 0x40 bytes.
|
||||
pub raw_header: &'a [u8],
|
||||
/// The cell data. Its internal layout is not yet decoded.
|
||||
pub payload: &'a [u8],
|
||||
}
|
||||
|
||||
impl CollisionHeader {
|
||||
/// Slice a collision region starting at `offset` within `core_data`.
|
||||
///
|
||||
/// Returns `Ok(None)` when the level has no collision data, which is
|
||||
/// distinct from a zero-length one.
|
||||
pub fn region<'a>(core: &'a [u8], offset: i32) -> Result<Option<Collision<'a>>> {
|
||||
if offset <= 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let start = offset as usize;
|
||||
let head = core.get(start..).ok_or(Error {
|
||||
offset: start,
|
||||
context: "collision offset runs past the lump",
|
||||
kind: ErrorKind::OutOfBounds {
|
||||
needed: 8,
|
||||
available: core.len().saturating_sub(start),
|
||||
},
|
||||
})?;
|
||||
let header = CollisionHeader::parse(head)?;
|
||||
let hsize = header.header_size as usize;
|
||||
let raw_header = core.get(start..start + hsize).ok_or(Error {
|
||||
offset: start,
|
||||
context: "collision header runs past the lump",
|
||||
kind: ErrorKind::OutOfBounds {
|
||||
needed: hsize,
|
||||
available: core.len().saturating_sub(start),
|
||||
},
|
||||
})?;
|
||||
let pstart = start + hsize;
|
||||
let pend = pstart + header.payload_size as usize;
|
||||
let payload = core.get(pstart..pend).ok_or(Error {
|
||||
offset: pstart,
|
||||
context: "collision payload runs past the lump",
|
||||
kind: ErrorKind::OutOfBounds {
|
||||
needed: header.payload_size as usize,
|
||||
available: core.len().saturating_sub(pstart),
|
||||
},
|
||||
})?;
|
||||
Ok(Some(Collision {
|
||||
header,
|
||||
raw_header,
|
||||
payload,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 16 bytes of leading lump, then the header, then the payload. Offset 0
|
||||
/// is reserved to mean "no collision", so the fixture cannot start there.
|
||||
const BASE: usize = 16;
|
||||
|
||||
fn core(header_size: u32, payload_size: u32) -> Vec<u8> {
|
||||
let mut v = vec![0u8; BASE + 64];
|
||||
v[BASE..BASE + 4].copy_from_slice(&header_size.to_le_bytes());
|
||||
v[BASE + 4..BASE + 8].copy_from_slice(&payload_size.to_le_bytes());
|
||||
v.extend(std::iter::repeat_n(0xabu8, payload_size as usize));
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slices_the_payload_out() {
|
||||
let c = core(0x40, 8);
|
||||
let r = CollisionHeader::region(&c, BASE as i32)
|
||||
.unwrap()
|
||||
.expect("present");
|
||||
assert_eq!(r.raw_header.len(), 0x40);
|
||||
assert_eq!(r.payload.len(), 8);
|
||||
assert!(r.payload.iter().all(|&b| b == 0xab));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_offset_means_absent_not_present() {
|
||||
let c = core(0x40, 8);
|
||||
assert!(CollisionHeader::region(&c, 0).unwrap().is_none());
|
||||
assert!(CollisionHeader::region(&c, -1).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_payload_past_the_lump_is_an_error_not_a_panic() {
|
||||
// A header that starts inside the lump but runs off the end of it.
|
||||
let mut c = core(0x40, 8);
|
||||
c[BASE + 8..BASE + 12].copy_from_slice(&9_000u32.to_le_bytes());
|
||||
assert!(CollisionHeader::region(&c, (BASE + 8) as i32).is_err());
|
||||
let mut c = core(0x40, 8);
|
||||
c[BASE + 4..BASE + 8].copy_from_slice(&9_000u32.to_le_bytes());
|
||||
assert!(CollisionHeader::region(&c, BASE as i32).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod buf;
|
||||
pub mod collision;
|
||||
pub mod global_wad;
|
||||
pub mod iso9660;
|
||||
pub mod level_core;
|
||||
|
||||
Reference in New Issue
Block a user