use crate::{decompress_raw, WadChunk, WadChunkCompression, WadChunks, WadError};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WadSubchunk {
pub compressed_size: u32,
pub uncompressed_size: u32,
pub checksum: u64,
}
impl WadSubchunk {
pub fn is_raw(&self) -> bool {
self.compressed_size == self.uncompressed_size
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SubchunkToc {
records: Box<[WadSubchunk]>,
}
const RECORD_SIZE: usize = 16;
impl SubchunkToc {
pub fn from_bytes(data: &[u8]) -> Result<Self, WadError> {
if !data.len().is_multiple_of(RECORD_SIZE) {
return Err(WadError::Other(format!(
"subchunk toc length {} is not a multiple of {RECORD_SIZE}",
data.len()
)));
}
let records = data
.chunks_exact(RECORD_SIZE)
.map(|record| WadSubchunk {
compressed_size: u32::from_le_bytes(record[0..4].try_into().unwrap()),
uncompressed_size: u32::from_le_bytes(record[4..8].try_into().unwrap()),
checksum: u64::from_le_bytes(record[8..16].try_into().unwrap()),
})
.collect();
Ok(Self { records })
}
pub fn records(&self) -> &[WadSubchunk] {
&self.records
}
pub fn subchunks_of(&self, chunk: &WadChunk) -> Option<&[WadSubchunk]> {
if chunk.compression_type != WadChunkCompression::ZstdMulti {
return None;
}
let start = chunk.start_frame as usize;
self.records.get(start..start + chunk.frame_count as usize)
}
pub fn covers(&self, chunks: &WadChunks) -> bool {
chunks
.iter()
.filter(|chunk| chunk.compression_type == WadChunkCompression::ZstdMulti)
.all(|chunk| {
self.subchunks_of(chunk).is_some_and(|subchunks| {
let compressed: u64 =
subchunks.iter().map(|sub| sub.compressed_size as u64).sum();
let uncompressed: u64 = subchunks
.iter()
.map(|sub| sub.uncompressed_size as u64)
.sum();
compressed == chunk.compressed_size as u64
&& uncompressed == chunk.uncompressed_size as u64
})
})
}
}
pub(crate) fn detect_subchunk_toc(
chunks: &WadChunks,
mut load: impl FnMut(&WadChunk) -> Result<Box<[u8]>, WadError>,
) -> Option<SubchunkToc> {
let frames = chunks
.iter()
.filter(|chunk| chunk.compression_type == WadChunkCompression::ZstdMulti)
.map(|chunk| chunk.start_frame as usize + chunk.frame_count as usize)
.max()?;
let candidates = chunks
.iter()
.filter(|chunk| chunk.uncompressed_size == frames * RECORD_SIZE);
for candidate in candidates {
let Ok(raw) = load(candidate) else { continue };
let Ok(data) = decompress_raw(
&raw,
candidate.compression_type,
candidate.uncompressed_size,
) else {
continue;
};
let Ok(toc) = SubchunkToc::from_bytes(&data) else {
continue;
};
if toc.covers(chunks) {
return Some(toc);
}
}
None
}
#[cfg(test)]
mod tests;