//! The asset containers under `G/` on the disc. //! //! Each `G/*.WAD` file begins with a typed header whose only universally //! applicable fields are: //! //! ```text //! offset size field //! 0x00 4 header size in bytes //! 0x04 4 sector count of this container //! 0x08.. ... a table of {offset, size} pairs, both in SECTORS //! ``` //! //! The pair table is the important part. Sectors chain: entry `n`'s offset plus //! its size is entry `n+1`'s offset, which is how the extractor recovers the //! layout without a per-container parser. The pairs begin at `0x08` and run to //! `header_size`, giving `(header_size - 8) / 8` entries. //! //! The payload behind each pair is **not** WAD-compressed. Insomniac's WAD LZ77 //! container is used elsewhere on the disc (inside level cores and MPEG //! streams); these global containers store their sections raw and let the //! table do the addressing. //! //! Which pair means what depends on the container — `ARMOR.WAD` is nine armor //! entries then six wrench entries, each with a mesh and a texture range — but //! the geometry above holds for every global on this disc, so the extractor //! stays generic and per-container meaning is recovered later. //! //! Spec: `docs/formats/global-wad.md`. use crate::buf::{Buf, Error, ErrorKind, Result}; /// Disc logical block size. All container offsets are in these units. pub const SECTOR_SIZE: u32 = 2048; /// A `{offset, size}` pair, both in sectors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SectorRange { pub offset: i32, pub size: i32, } impl SectorRange { /// Byte offset of the extent. `None` when the range is absent. pub fn byte_offset(&self) -> Option { if self.size <= 0 || self.offset < 0 { return None; } Some(self.offset as u64 * SECTOR_SIZE as u64) } /// Byte length of the extent. pub fn byte_len(&self) -> u64 { if self.size <= 0 { 0 } else { self.size as u64 * SECTOR_SIZE as u64 } } pub fn is_present(&self) -> bool { self.size > 0 } } /// A `{offset, size}` pair where the offset is in sectors and the size is in /// **bytes**. /// /// The MPEG containers use this form: their entries are video and subtitle /// streams whose length is not sector-aligned, so a sector count would round /// and lose the tail. See `docs/formats/global-wad.md`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SectorByteRange { pub offset: i32, pub size: i32, } impl SectorByteRange { pub fn byte_offset(&self) -> Option { if self.size <= 0 || self.offset < 0 { return None; } Some(self.offset as u64 * SECTOR_SIZE as u64) } pub fn byte_len(&self) -> u64 { if self.size <= 0 { 0 } else { self.size as u64 } } pub fn is_present(&self) -> bool { self.size > 0 } } /// How a container's table should be read. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RangeKind { /// Both fields are sector counts. Sectors, /// Offset in sectors, size in bytes. Used by every indexed container on /// this disc except the scene containers. SectorBytes, /// A flat array of sector numbers, not pairs. The scene containers are /// this: each value points at a sector, and the length of each extent comes /// from the difference to the next entry. The last entry is bounded by the /// end of the container. SectorList, } impl RangeKind { /// Is this container's table a flat list rather than pairs? pub fn is_list(&self) -> bool { matches!(self, RangeKind::SectorList) } } impl RangeKind { /// Classify a container from its header size. /// /// On this disc **every** indexed container uses the sector/byte form: a /// table of `{offset in sectors, size in bytes}` pairs. This was measured /// rather than assumed. For all 96 containers that carry a pair table, the /// sector/byte reading places every entry inside the file; the /// sector/sector reading leaves entries running past the end of the file, /// and on the 27 level containers and the 27 audio containers it places /// *none* of them correctly. /// /// The header size is the reliable discriminator because it is the field /// every container agrees on, and it distinguishes the level containers /// (0x60) from audio (0x1018), scene (0x137c) and the rest. pub fn for_header(header_size: u32) -> RangeKind { match header_size { // The per-level scene containers hold a flat sector list rather // than pairs: 0x137c is the header size the 27 SCENE.WAD files // share, and (0x137c - 8) is not a multiple of 8, so the bytes // cannot be read as pairs at all. 0x137c => RangeKind::SectorList, // No container on this disc uses the sector/sector form. Kept so a // future disc revision can be checked rather than assumed. _ => RangeKind::SectorBytes, } } /// Classify by ISO file name, for callers that only have the path. pub fn for_container(name: &str) -> RangeKind { let stem = name.rsplit('/').next().unwrap_or(name); let stem = stem.trim_end_matches(".WAD").to_ascii_uppercase(); if stem.starts_with("SCENE") && stem.len() > 5 && stem[5..].chars().all(|c| c.is_ascii_digit()) { // SCENE0..SCENE26 are the per-level containers. SCENE.WAD (no // digits) is the global one and does use pairs. return RangeKind::SectorList; } RangeKind::SectorBytes } } /// A parsed global container header. #[derive(Debug, Clone)] pub struct GlobalWad { pub header_size: u32, /// Declared sector count of the whole container. pub sector_count: u32, /// The pair table, in declaration order. pub entries: Vec, /// How the pair table was read. pub kind: RangeKind, } impl GlobalWad { /// Parse a container header, reading the table as sector-count pairs. pub fn parse(bytes: &[u8]) -> Result { GlobalWad::parse_with(bytes, RangeKind::SectorBytes) } /// Parse a container header with an explicit range interpretation. /// /// Both interpretations read the same 8 bytes per entry; they differ only in /// how the size field is scaled, which is a property of the container /// rather than of the bytes. pub fn parse_with(bytes: &[u8], kind: RangeKind) -> Result { let mut b = Buf::new(bytes); let header_size = b.u32("container header size")? as usize; let sector_count = b.u32("container sector count")?; if header_size < 8 || header_size > bytes.len() { return Err(Error { offset: 0, context: "container header", kind: ErrorKind::InvalidLength("container header size"), }); } // A pair table must divide evenly into 8-byte entries; a remainder // means we have guessed the layout wrong and should not read garbage. // A sector list is a flat run of 4-byte values and needs no such check. let table_bytes = header_size - 8; if !kind.is_list() && table_bytes % 8 != 0 { return Err(Error { offset: 0, context: "container header", kind: ErrorKind::Invalid("pair table is not a whole number of entries"), }); } let mut entries = Vec::with_capacity(table_bytes / 8); if kind.is_list() { // Each value is the first sector of one extent; the size runs to the // next value, and the last runs to the end of the container. let mut sectors = Vec::with_capacity(table_bytes / 4); while b.pos() + 4 <= header_size { sectors.push(b.i32("extent sector")?); } let total_sectors = (bytes.len() as u32).div_ceil(SECTOR_SIZE); for (i, &start) in sectors.iter().enumerate() { let end = sectors.get(i + 1).copied().unwrap_or(total_sectors as i32); entries.push(SectorRange { offset: start, size: end - start, }); } } else { while b.pos() + 8 <= header_size { let offset = b.i32("entry offset")?; let size = b.i32("entry size")?; entries.push(SectorRange { offset, size }); } } Ok(GlobalWad { header_size: header_size as u32, sector_count, entries, kind, }) } /// Entries that point at real data, as `(index, byte offset, byte length)`. pub fn present(&self) -> impl Iterator + '_ { self.entries .iter() .copied() .enumerate() .filter_map(|(i, r)| { let len = match self.kind { RangeKind::Sectors => r.byte_len(), RangeKind::SectorList => r.byte_len(), RangeKind::SectorBytes => SectorByteRange { offset: r.offset, size: r.size, } .byte_len(), }; let off = r.byte_offset()?; if len == 0 { return None; } Some((i, off, len)) }) } } #[cfg(test)] mod tests { use super::*; /// Build a synthetic container: header_size, sector_count, then pairs. fn synthetic(entries: &[(i32, i32)]) -> Vec { let header_size = (8 + entries.len() * 8) as u32; let mut v = Vec::new(); v.extend_from_slice(&header_size.to_le_bytes()); v.extend_from_slice(&100u32.to_le_bytes()); for (o, s) in entries { v.extend_from_slice(&o.to_le_bytes()); v.extend_from_slice(&s.to_le_bytes()); } // Pad the body so byte_offset() reads land inside the buffer. v.resize(header_size as usize + 4096, 0); v } #[test] fn parses_header_and_pairs() { let wad = GlobalWad::parse(&synthetic(&[(1, 72), (73, 51), (124, 80)])).unwrap(); assert_eq!(wad.header_size, 32); assert_eq!(wad.sector_count, 100); assert_eq!(wad.entries.len(), 3); assert_eq!( wad.entries[0], SectorRange { offset: 1, size: 72 } ); assert_eq!(wad.entries[2].offset, 124); } #[test] fn sector_ranges_convert_to_bytes() { let r = SectorRange { offset: 3, size: 2 }; assert_eq!(r.byte_offset(), Some(3 * 2048)); assert_eq!(r.byte_len(), 2 * 2048); assert!(r.is_present()); } #[test] fn absent_ranges_are_not_present() { assert!(!SectorRange { offset: 0, size: 0 }.is_present()); assert!(!SectorRange { offset: 5, size: -1 } .is_present()); assert_eq!(SectorRange { offset: 5, size: 0 }.byte_offset(), None); assert_eq!( SectorRange { offset: -5, size: 10 } .byte_offset(), None ); } #[test] fn present_filters_zero_sizes() { let wad = GlobalWad::parse(&synthetic(&[(1, 4), (5, 0), (5, 4)])).unwrap(); let present: Vec = wad.present().map(|(i, _, _)| i).collect(); assert_eq!(present, vec![0, 2]); } #[test] fn present_scales_sizes_by_the_range_kind() { let bytes = synthetic(&[(1, 100)]); // The default for this disc treats the second field as a byte count. let raw = GlobalWad::parse(&bytes).unwrap(); assert_eq!(raw.present().next().unwrap().2, 100); // The sector-count form scales it up. let sectors = GlobalWad::parse_with(&bytes, RangeKind::Sectors).unwrap(); assert_eq!(sectors.present().next().unwrap().2, 100 * 2048); } #[test] fn rejects_a_header_size_beyond_the_buffer() { let mut v = 9999u32.to_le_bytes().to_vec(); v.resize(16, 0); assert!(GlobalWad::parse(&v).is_err()); } #[test] fn rejects_a_header_size_below_the_minimum() { let v = 4u32.to_le_bytes().to_vec(); assert!(GlobalWad::parse(&v).is_err()); } #[test] fn rejects_a_table_that_does_not_divide_evenly() { // header_size 13 -> table of 5 bytes, not a multiple of 8. let mut v = 13u32.to_le_bytes().to_vec(); v.extend_from_slice(&0u32.to_le_bytes()); v.extend_from_slice(&[0u8; 5]); let err = GlobalWad::parse(&v).unwrap_err(); assert!(matches!(err.kind, ErrorKind::Invalid(_)), "got {err:?}"); } #[test] fn empty_table_is_allowed() { let mut v = 8u32.to_le_bytes().to_vec(); v.extend_from_slice(&10u32.to_le_bytes()); v.resize(4096, 0); let wad = GlobalWad::parse(&v).unwrap(); assert!(wad.entries.is_empty()); } }