use binrw::BinRead;
use byteorder::{ReadBytesExt, LE};
use std::{
collections::HashMap,
io::{BufReader, Read, Seek, SeekFrom},
};
use ltk_io_ext::ReaderExt;
use crate::{
chunk::ModpkgChunk, error::ModpkgError, ChunkKey, ChunkPath, LayerHash, LayerIndex, Modpkg,
ModpkgLayer, PathHash, WadIndex, WadNameHash,
};
impl<TSource: Read + Seek> Modpkg<TSource> {
const MAGIC: [u8; 8] = *b"_modpkg_";
pub fn mount_from_reader(mut source: TSource) -> Result<Self, ModpkgError> {
let mut reader = BufReader::with_capacity(64 * 1024, &mut source);
let magic = reader.read_u64::<LE>()?;
if magic != u64::from_le_bytes(Self::MAGIC) {
return Err(ModpkgError::InvalidMagic(magic));
}
let version = reader.read_u32::<LE>()?;
if version != 1 {
return Err(ModpkgError::UnsupportedFormatVersion(version));
}
let signature_size = reader.read_u32::<LE>()?;
let chunk_count = reader.read_u32::<LE>()?;
let mut signature = vec![0; signature_size as usize];
reader.read_exact(&mut signature)?;
let (layer_indices, layers) = read_layers(&mut reader)?;
let (chunk_path_indices, chunk_paths) = read_chunk_paths(&mut reader)?;
let (wad_indices, wads) = read_wads(&mut reader)?;
let position = reader.stream_position()?;
reader.seek(SeekFrom::Current(((8 - (position % 8)) % 8) as i64))?;
let mut chunks = HashMap::new();
let mut chunks_by_wad_layer: HashMap<(WadIndex, LayerIndex), Vec<ChunkKey>> =
HashMap::new();
for _ in 0..chunk_count {
let chunk = ModpkgChunk::read(&mut reader)?;
let layer_hash = if chunk.layer_index == LayerIndex::NONE {
LayerHash::NONE
} else {
layer_indices[chunk.layer_index.value() as usize]
};
if chunk.path_index as usize >= chunk_path_indices.len() {
return Err(ModpkgError::MissingChunk(chunk.path_hash));
}
let key = ChunkKey::new(chunk.path_hash, layer_hash);
chunks_by_wad_layer
.entry((chunk.wad_index, chunk.layer_index))
.or_default()
.push(key);
if let Some(existing) = chunks.insert(key, chunk) {
if (existing.uncompressed_checksum, existing.uncompressed_size)
!= (chunk.uncompressed_checksum, chunk.uncompressed_size)
{
return Err(ModpkgError::ChunksInconsistent(chunk.path_hash));
}
}
}
drop(reader);
Ok(Self {
signature,
layer_indices,
layers,
chunk_path_indices,
chunk_paths,
wad_indices,
wads,
chunks,
chunks_by_wad_layer,
source,
})
}
}
fn read_layers<R: Read + Seek>(
reader: &mut R,
) -> Result<(Vec<LayerHash>, HashMap<LayerHash, ModpkgLayer>), ModpkgError> {
let layer_count = reader.read_u32::<LE>()?;
let mut layer_indices = Vec::with_capacity(layer_count as usize);
let mut layers = HashMap::with_capacity(layer_count as usize);
for _ in 0..layer_count {
let layer = ModpkgLayer::read(reader)?;
check_contained(&layer.name)?;
let layer_hash = LayerHash::from_name(&layer.name);
layers.insert(layer_hash, layer);
layer_indices.push(layer_hash);
}
Ok((layer_indices, layers))
}
fn read_chunk_paths<R: Read + Seek>(
reader: &mut R,
) -> Result<(Vec<PathHash>, HashMap<PathHash, String>), ModpkgError> {
let chunk_paths_count = reader.read_u32::<LE>()?;
let mut chunk_path_indices = Vec::with_capacity(chunk_paths_count as usize);
let mut chunk_paths = HashMap::with_capacity(chunk_paths_count as usize);
for _ in 0..chunk_paths_count {
let chunk_path = ChunkPath::new(reader.read_str_until_nul()?);
check_contained(chunk_path.as_str())?;
let chunk_path_hash = chunk_path.hash();
chunk_path_indices.push(chunk_path_hash);
chunk_paths.insert(chunk_path_hash, chunk_path.into_string());
}
Ok((chunk_path_indices, chunk_paths))
}
fn read_wads<R: Read + Seek>(
reader: &mut R,
) -> Result<(Vec<WadNameHash>, HashMap<WadNameHash, String>), ModpkgError> {
let wads_count = reader.read_u32::<LE>()?;
let mut wads_indices = Vec::with_capacity(wads_count as usize);
let mut wads = HashMap::with_capacity(wads_count as usize);
for _ in 0..wads_count {
let wad = reader.read_str_until_nul()?;
check_contained(&wad)?;
let wad_hash = WadNameHash::from_name(&wad);
wads.insert(wad_hash, wad);
wads_indices.push(wad_hash);
}
Ok((wads_indices, wads))
}
fn is_contained(name: &str) -> bool {
!name.starts_with(['/', '\\'])
&& !name.contains(':')
&& name.split(['/', '\\']).all(|component| component != "..")
}
fn check_contained(name: &str) -> Result<(), ModpkgError> {
match is_contained(name) {
true => Ok(()),
false => Err(ModpkgError::EscapingPath(name.to_owned())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
builder::{ModpkgBuilder, ModpkgChunkBuilder, ModpkgLayerBuilder},
ModpkgCompression,
};
use std::io::Cursor;
fn package_with(path: &str, wad: &str) -> Cursor<Vec<u8>> {
let mut cursor = Cursor::new(Vec::new());
ModpkgBuilder::default()
.with_layer(ModpkgLayerBuilder::base())
.with_chunk(
ModpkgChunkBuilder::new()
.with_path(path)
.with_wad(wad)
.with_compression(ModpkgCompression::None),
)
.build_to_writer(&mut cursor, |_| Ok(vec![0xAA; 4]))
.expect("Failed to build Modpkg");
cursor.set_position(0);
cursor
}
#[test]
fn a_chunk_path_that_escapes_refuses_the_package() {
let Err(error) = Modpkg::mount_from_reader(package_with("../../pwned.bin", "")) else {
panic!("a package with an escaping chunk path mounted");
};
assert!(
matches!(&error, ModpkgError::EscapingPath(path) if path == "../../pwned.bin"),
"{error:?}"
);
}
#[test]
fn a_wad_name_that_escapes_refuses_the_package() {
let Err(error) = Modpkg::mount_from_reader(package_with("data.bin", "../../pwned")) else {
panic!("a package with an escaping WAD name mounted");
};
assert!(
matches!(&error, ModpkgError::EscapingPath(name) if name == "../../pwned"),
"{error:?}"
);
}
#[test]
fn containment_refuses_escapes_and_keeps_ordinary_names() {
for name in [
"../x",
"a/../../x",
r"a\..\..\x",
"/etc/x",
r"\etc\x",
"C:/x",
] {
assert!(!is_contained(name), "{name} was accepted");
}
for name in ["a/b/c.bin", "..bin", "a..b/c", "./a/b", "a/./b"] {
assert!(is_contained(name), "{name} was refused");
}
}
}