use std::collections::BTreeMap;
use std::io::{BufWriter, Seek, SeekFrom, Write};
use std::sync::Arc;
use byteorder::{WriteBytesExt as _, LE};
use xxhash_rust::xxh3::xxh3_64;
use crate::{WadChunk, WadChunkCompression, WadError, WadHash};
#[cfg(test)]
mod tests;
const TOC_ENTRY_SIZE: usize = 32;
const WAD_HEADER_SIZE: u64 = 268;
const WRITE_BUFFER_SIZE: usize = 1 << 20;
const TOC_SLACK_ENTRIES: u32 = 0;
const MAX_WAD_OFFSET: u64 = u32::MAX as u64;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WadRebaseError {
#[error(
"a layout reserving {toc_capacity} TOC entries cannot put its data region at \
{data_region_offset} and its tail at {tail_offset}"
)]
IncoherentLayout {
toc_capacity: u32,
data_region_offset: u64,
tail_offset: u64,
},
#[error("the WAD reserved {reserved} TOC entries, not the {needed} this rebase needs")]
TocCapacity {
needed: usize,
reserved: u32,
},
#[error(
"the tail would reach offset {offset}, past the 4 GiB the WAD v3.4 format \
can address"
)]
TailTooLarge {
offset: u64,
},
#[error("chunk {path_hash:016x} cannot be addressed at offset {offset}")]
ChunkUnaddressable {
path_hash: WadHash,
offset: i64,
},
#[error(transparent)]
Wad(#[from] WadError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WadRebaseReport {
pub entry_count: usize,
pub tail_len: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedChunk {
compressed: Arc<[u8]>,
uncompressed_size: u32,
compression: WadChunkCompression,
checksum: u64,
}
impl EncodedChunk {
pub fn new(
compressed: impl Into<Arc<[u8]>>,
uncompressed_size: u32,
compression: WadChunkCompression,
) -> Self {
let compressed = compressed.into();
Self {
checksum: xxh3_64(&compressed),
compressed,
uncompressed_size,
compression,
}
}
#[must_use]
pub fn compressed(&self) -> &[u8] {
&self.compressed
}
#[must_use]
pub fn uncompressed_size(&self) -> u32 {
self.uncompressed_size
}
#[must_use]
pub fn compression(&self) -> WadChunkCompression {
self.compression
}
#[must_use]
pub fn checksum(&self) -> u64 {
self.checksum
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WadTailLayout {
pub data_region_offset: u64,
pub offset_delta: i64,
pub tail_offset: u64,
pub toc_capacity: u32,
}
impl WadTailLayout {
pub fn toc_offset(&self) -> Result<u64, WadRebaseError> {
self.chunk_count_offset()
.map(|offset| offset + size_of::<u32>() as u64)
}
pub fn chunk_count_offset(&self) -> Result<u64, WadRebaseError> {
let below_region =
u64::from(self.toc_capacity) * TOC_ENTRY_SIZE as u64 + size_of::<u32>() as u64;
self.data_region_offset
.checked_sub(below_region)
.filter(|&offset| offset >= WAD_HEADER_SIZE)
.ok_or(WadRebaseError::IncoherentLayout {
toc_capacity: self.toc_capacity,
data_region_offset: self.data_region_offset,
tail_offset: self.tail_offset,
})
}
pub fn shifted(&self, orig: &WadChunk) -> Result<WadChunk, WadRebaseError> {
let shifted = orig.data_offset as i64 + self.offset_delta;
let end = shifted + orig.compressed_size as i64;
if shifted < 0 || end > MAX_WAD_OFFSET as i64 {
return Err(WadRebaseError::ChunkUnaddressable {
path_hash: orig.path_hash,
offset: shifted,
});
}
Ok(WadChunk {
data_offset: shifted as usize,
..*orig
})
}
#[must_use]
pub fn admits_entry_count(&self, entry_count: usize) -> bool {
let fewest = self.toc_capacity.saturating_sub(TOC_SLACK_ENTRIES);
u32::try_from(entry_count).is_ok_and(|count| (fewest..=self.toc_capacity).contains(&count))
}
pub fn validate(&self) -> Result<(), WadRebaseError> {
self.chunk_count_offset()?;
if self.tail_offset < self.data_region_offset {
return Err(WadRebaseError::IncoherentLayout {
toc_capacity: self.toc_capacity,
data_region_offset: self.data_region_offset,
tail_offset: self.tail_offset,
});
}
Ok(())
}
}
#[derive(Debug)]
pub struct WadRebasePlan<'a> {
layout: WadTailLayout,
entries: BTreeMap<WadHash, WadChunk>,
tail: &'a [(WadHash, EncodedChunk)],
entry_count: usize,
tail_end: u64,
}
impl<'a> WadRebasePlan<'a> {
pub fn tail(
layout: &WadTailLayout,
base_entries: BTreeMap<WadHash, WadChunk>,
tail: &'a [(WadHash, EncodedChunk)],
) -> Result<Self, WadRebaseError> {
layout.validate()?;
let entry_count =
Self::merged_entry_count(&base_entries, tail.iter().map(|(hash, _)| *hash));
if !layout.admits_entry_count(entry_count) {
return Err(WadRebaseError::TocCapacity {
needed: entry_count,
reserved: layout.toc_capacity,
});
}
let tail_end = tail.iter().fold(layout.tail_offset, |end, (_, chunk)| {
end.saturating_add(chunk.compressed().len() as u64)
});
if tail_end > MAX_WAD_OFFSET {
return Err(WadRebaseError::TailTooLarge { offset: tail_end });
}
Ok(Self {
layout: *layout,
entries: base_entries,
tail,
entry_count,
tail_end,
})
}
#[must_use]
pub fn merged_entry_count(
base_entries: &BTreeMap<WadHash, WadChunk>,
tail_hashes: impl IntoIterator<Item = WadHash>,
) -> usize {
base_entries.len()
+ tail_hashes
.into_iter()
.filter(|hash| !base_entries.contains_key(hash))
.count()
}
#[must_use]
pub fn tail_len(&self) -> u64 {
self.tail_end - self.layout.tail_offset
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.entry_count
}
pub fn write<W: Write + Seek>(
mut self,
wad: &mut W,
base: u64,
) -> Result<WadRebaseReport, WadRebaseError> {
let mut writer = BufWriter::with_capacity(WRITE_BUFFER_SIZE, wad);
writer.seek(SeekFrom::Start(base + self.layout.tail_offset))?;
let mut cursor = self.layout.tail_offset;
for (path_hash, encoded) in self.tail {
let chunk = Self::write_tail_chunk(&mut writer, *path_hash, encoded, &mut cursor)?;
self.entries.insert(*path_hash, chunk);
}
let tail_len = cursor - self.layout.tail_offset;
writer.seek(SeekFrom::Start(base + self.layout.chunk_count_offset()?))?;
writer.write_u32::<LE>(self.entries.len() as u32)?;
for chunk in self.entries.values() {
chunk.write_v3_4(&mut writer)?;
}
for _ in self.entries.len()..self.layout.toc_capacity as usize {
writer.write_all(&[0u8; TOC_ENTRY_SIZE])?;
}
writer.flush()?;
Ok(WadRebaseReport {
entry_count: self.entries.len(),
tail_len,
})
}
fn write_tail_chunk<W: Write>(
writer: &mut W,
path_hash: WadHash,
encoded: &EncodedChunk,
cursor: &mut u64,
) -> Result<WadChunk, WadRebaseError> {
let compressed_size = encoded.compressed().len();
let end = *cursor + compressed_size as u64;
if end > MAX_WAD_OFFSET {
return Err(WadRebaseError::TailTooLarge { offset: end });
}
let chunk = WadChunk {
path_hash,
data_offset: *cursor as usize,
compressed_size,
uncompressed_size: encoded.uncompressed_size() as usize,
compression_type: encoded.compression(),
is_duplicated: false,
frame_count: 0,
start_frame: 0,
checksum: encoded.checksum(),
};
writer.write_all(encoded.compressed())?;
*cursor = end;
Ok(chunk)
}
}