use super::*;
use crate::{Wad, WadBuilder, WadChunkBuilder, WadChunkCompression};
use ltk_hash::Hash as _;
use std::io::{Cursor, Write};
const SKIN: &str = "assets/characters/test/skins/skin0.dds";
const VFX: &str = "assets/characters/test/particles.bin";
fn hash(path: &str) -> WadHash {
WadHash::hash_str(path)
}
fn build_wad(chunks: &[(&str, &[u8])]) -> Vec<u8> {
let mut builder = WadBuilder::default();
for (path, _) in chunks {
builder = builder.with_chunk(
WadChunkBuilder::default()
.with_path(*path)
.with_force_compression(WadChunkCompression::None),
);
}
let by_hash: BTreeMap<WadHash, Vec<u8>> = chunks
.iter()
.map(|(path, bytes)| (hash(path), bytes.to_vec()))
.collect();
let mut cursor = Cursor::new(Vec::new());
builder
.build_to_writer(&mut cursor, move |chunk_hash, writer| {
writer.write_all(&by_hash[&chunk_hash])?;
Ok(())
})
.expect("fixture WAD builds");
cursor.into_inner()
}
fn layout_of(wad_bytes: &[u8]) -> (WadTailLayout, BTreeMap<WadHash, WadChunk>) {
let wad = Wad::mount(Cursor::new(wad_bytes)).expect("the fixture mounts");
let chunks = wad.chunks();
let toc_capacity = u32::try_from(chunks.len()).expect("the fixture is small");
let data_region_offset = chunks
.iter()
.map(|chunk| chunk.data_offset as u64)
.min()
.expect("the fixture holds chunks");
let tail_offset = chunks
.iter()
.map(|chunk| (chunk.data_offset + chunk.compressed_size) as u64)
.max()
.expect("the fixture holds chunks");
let layout = WadTailLayout {
data_region_offset,
offset_delta: 0,
tail_offset,
toc_capacity,
};
let entries = chunks
.iter()
.map(|chunk| (chunk.path_hash, layout.shifted(chunk).expect("in range")))
.collect();
(layout, entries)
}
fn stored(data: &[u8]) -> EncodedChunk {
EncodedChunk::new(
data,
u32::try_from(data.len()).expect("the fixture is small"),
WadChunkCompression::None,
)
}
#[test]
fn a_rejected_entry_count_leaves_the_file_untouched() {
let tmp = tempfile::tempdir().expect("a temp directory");
let wad_path = tmp.path().join("Test.wad.client");
let built = build_wad(&[(SKIN, b"the original skin"), (VFX, b"the original vfx")]);
std::fs::write(&wad_path, &built).expect("the fixture writes");
let (layout, base_entries) = layout_of(&built);
let before = std::fs::read(&wad_path).expect("the fixture reads back");
let new_entry = hash("assets/characters/test/brand_new.bin");
let tail = [(new_entry, stored(b"a chunk the TOC has no room for"))];
let refused = WadRebasePlan::tail(&layout, base_entries, &tail);
assert!(
refused.is_err(),
"an over-capacity entry set must be refused"
);
assert_eq!(
std::fs::read(&wad_path).expect("the file reads back"),
before,
"a refused rewrite must not have touched the file"
);
}
#[test]
fn a_tail_written_at_a_base_offset_reads_back_as_a_wad() {
const BASE: u64 = 4096;
let built = build_wad(&[(SKIN, b"the original skin"), (VFX, b"the original vfx")]);
let (layout, base_entries) = layout_of(&built);
let mut container = vec![0xAAu8; BASE as usize];
container.extend_from_slice(&built[..layout.tail_offset as usize]);
let tail = [(hash(SKIN), stored(b"a differently modded skin"))];
let plan = WadRebasePlan::tail(&layout, base_entries, &tail).expect("the plan is admissible");
let tail_len = plan.tail_len();
let mut cursor = Cursor::new(container);
let report = plan.write(&mut cursor, BASE).expect("the tail writes");
let container = cursor.into_inner();
assert_eq!(
report.tail_len, tail_len,
"the plan predicted the tail length"
);
assert_eq!(report.entry_count, 2);
assert!(
container[..BASE as usize].iter().all(|byte| *byte == 0xAA),
"the write ran back over the container's own bytes"
);
let mut wad = Wad::mount(Cursor::new(&container[BASE as usize..])).expect("the region mounts");
let chunk = *wad.chunks().get(hash(SKIN)).expect("the rebased chunk");
assert_eq!(
&*wad
.load_chunk_decompressed(&chunk)
.expect("it decompresses"),
b"a differently modded skin"
);
let untouched = *wad
.chunks()
.get(hash(VFX))
.expect("the chunk the region still holds");
assert_eq!(
&*wad
.load_chunk_decompressed(&untouched)
.expect("it decompresses"),
b"the original vfx"
);
}
#[test]
fn a_layout_whose_toc_would_land_in_the_header_is_refused() {
let smallest = WadTailLayout {
data_region_offset: 268 + 4 + 32,
offset_delta: 0,
tail_offset: 4096,
toc_capacity: 1,
};
assert!(
smallest.validate().is_ok(),
"the tightest legal layout must still be usable"
);
let in_the_header = WadTailLayout {
data_region_offset: 4 + 32,
..smallest
};
assert!(
in_the_header.validate().is_err(),
"a region offset that leaves no room for the header must be refused"
);
}
#[test]
fn a_built_wad_puts_its_toc_exactly_past_the_header() {
let built = build_wad(&[(SKIN, b"the original skin")]);
let (layout, _) = layout_of(&built);
assert_eq!(
layout.chunk_count_offset().expect("the layout is coherent"),
268,
"the chunk count follows the header"
);
assert_eq!(
layout.toc_offset().expect("the layout is coherent"),
272,
"the TOC follows the chunk count"
);
}
#[test]
fn a_tail_hash_the_base_already_holds_adds_no_entry() {
let built = build_wad(&[(SKIN, b"the original skin"), (VFX, b"the original vfx")]);
let (_, base_entries) = layout_of(&built);
assert_eq!(
WadRebasePlan::merged_entry_count(&base_entries, [hash(SKIN)]),
2,
"overriding a chunk the region holds reuses its entry"
);
assert_eq!(
WadRebasePlan::merged_entry_count(&base_entries, [hash("assets/new.bin")]),
3,
"a hash the region does not hold needs an entry of its own"
);
}
#[test]
fn a_layout_shifts_a_chunk_into_its_region() {
let source = WadChunk {
path_hash: hash(SKIN),
data_offset: 8192,
compressed_size: 512,
uncompressed_size: 1024,
compression_type: WadChunkCompression::Zstd,
is_duplicated: false,
frame_count: 3,
start_frame: 7,
checksum: 0xDEAD_BEEF_CAFE_BABE,
};
let layout = WadTailLayout {
data_region_offset: 4096,
offset_delta: -4096,
tail_offset: 65536,
toc_capacity: 1,
};
let shifted = layout.shifted(&source).expect("the shift is addressable");
assert_eq!(shifted.data_offset, 4096, "the offset moved by the delta");
assert_eq!(
WadChunk {
data_offset: source.data_offset,
..shifted
},
source,
"the offset is the only field a shift touches"
);
let past_the_limit = WadTailLayout {
offset_delta: i64::from(u32::MAX),
..layout
};
assert!(
past_the_limit.shifted(&source).is_err(),
"a chunk shifted past the addressable file must be refused"
);
}