mirror of
https://github.com/xavo95/repak.git
synced 2025-04-12 11:09:48 +00:00
236 lines
7.8 KiB
Rust
236 lines
7.8 KiB
Rust
use super::{Version, VersionMajor};
|
|
use hashbrown::HashMap;
|
|
use std::io::{self, Seek};
|
|
|
|
#[derive(Debug)]
|
|
pub struct PakReader<R: io::Read + io::Seek> {
|
|
pak: Pak,
|
|
reader: R,
|
|
}
|
|
#[derive(Debug)]
|
|
pub struct Pak {
|
|
version: Version,
|
|
mount_point: String,
|
|
key: Option<aes::Aes256Dec>,
|
|
index: Index,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Index {
|
|
V1(IndexV1),
|
|
V2(IndexV2),
|
|
}
|
|
|
|
impl Index {
|
|
fn entries(&self) -> &HashMap<String, super::entry::Entry> {
|
|
match self {
|
|
Index::V1(index) => &index.entries,
|
|
Index::V2(index) => &index.entries_by_path,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct IndexV1 {
|
|
entries: HashMap<String, super::entry::Entry>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct IndexV2 {
|
|
path_hash_seed: u64,
|
|
path_hash_index: Option<Vec<u8>>,
|
|
full_directory_index: Option<HashMap<String, HashMap<String, u32>>>,
|
|
encoded_entries: Vec<u8>,
|
|
entries_by_path: HashMap<String, super::entry::Entry>,
|
|
}
|
|
|
|
fn decrypt(key: &Option<aes::Aes256Dec>, bytes: &mut [u8]) -> Result<(), super::Error> {
|
|
if let Some(key) = &key {
|
|
use aes::cipher::BlockDecrypt;
|
|
for chunk in bytes.chunks_mut(16) {
|
|
key.decrypt_block(aes::Block::from_mut_slice(chunk))
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(super::Error::Encrypted)
|
|
}
|
|
}
|
|
|
|
impl<R: io::Read + io::Seek> PakReader<R> {
|
|
pub fn new_any(mut reader: R, key: Option<aes::Aes256Dec>) -> Result<Self, super::Error> {
|
|
for ver in Version::iter() {
|
|
match PakReader::new(&mut reader, ver, key.clone()) {
|
|
Ok(pak) => {
|
|
return Ok(PakReader { pak, reader });
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
Err(super::Error::Other("version unsupported"))
|
|
}
|
|
|
|
pub fn new(
|
|
mut reader: R,
|
|
version: super::Version,
|
|
key: Option<aes::Aes256Dec>,
|
|
) -> Result<Pak, super::Error> {
|
|
use super::ext::ReadExt;
|
|
use byteorder::{ReadBytesExt, LE};
|
|
// read footer to get index, encryption & compression info
|
|
reader.seek(io::SeekFrom::End(-version.size()))?;
|
|
let footer = super::footer::Footer::new(&mut reader, version)?;
|
|
// read index to get all the entry info
|
|
reader.seek(io::SeekFrom::Start(footer.index_offset))?;
|
|
let mut index = reader.read_len(footer.index_size as usize)?;
|
|
|
|
// decrypt index if needed
|
|
if footer.encrypted {
|
|
decrypt(&key, &mut index)?;
|
|
}
|
|
|
|
let mut index = io::Cursor::new(index);
|
|
let mount_point = index.read_string()?;
|
|
let len = index.read_u32::<LE>()? as usize;
|
|
|
|
let index = if version.version_major() >= VersionMajor::PathHashIndex {
|
|
let path_hash_seed = index.read_u64::<LE>()?;
|
|
|
|
let path_hash_index = if index.read_u32::<LE>()? != 0 {
|
|
let path_hash_index_offset = index.read_u64::<LE>()?;
|
|
let path_hash_index_size = index.read_u64::<LE>()?;
|
|
let _path_hash_index_hash = index.read_len(20)?;
|
|
|
|
reader.seek(io::SeekFrom::Start(path_hash_index_offset))?;
|
|
let mut path_hash_index = reader.read_len(path_hash_index_size as usize)?;
|
|
// TODO verify hash
|
|
|
|
if footer.encrypted {
|
|
decrypt(&key, &mut path_hash_index)?;
|
|
}
|
|
Some(path_hash_index)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let full_directory_index = if index.read_u32::<LE>()? != 0 {
|
|
let full_directory_index_offset = index.read_u64::<LE>()?;
|
|
let full_directory_index_size = index.read_u64::<LE>()?;
|
|
let _full_directory_index_hash = index.read_len(20)?;
|
|
|
|
reader.seek(io::SeekFrom::Start(full_directory_index_offset))?;
|
|
let mut full_directory_index =
|
|
reader.read_len(full_directory_index_size as usize)?;
|
|
// TODO verify hash
|
|
|
|
if footer.encrypted {
|
|
decrypt(&key, &mut full_directory_index)?;
|
|
}
|
|
let mut fdi = io::Cursor::new(full_directory_index);
|
|
|
|
let dir_count = fdi.read_u32::<LE>()? as usize;
|
|
let mut directories = HashMap::with_capacity(dir_count);
|
|
for _ in 0..dir_count {
|
|
let dir_name = fdi.read_string()?;
|
|
let file_count = fdi.read_u32::<LE>()? as usize;
|
|
let mut files = HashMap::with_capacity(file_count);
|
|
for _ in 0..file_count {
|
|
let file_name = fdi.read_string()?;
|
|
files.insert(file_name, fdi.read_u32::<LE>()?);
|
|
}
|
|
directories.insert(dir_name, files);
|
|
}
|
|
Some(directories)
|
|
} else {
|
|
None
|
|
};
|
|
let size = index.read_u32::<LE>()? as usize;
|
|
let encoded_entries = index.read_len(size)?;
|
|
|
|
let mut entries_by_path = HashMap::new();
|
|
if let Some(fdi) = &full_directory_index {
|
|
let mut encoded_entries = io::Cursor::new(&encoded_entries);
|
|
for (dir_name, dir) in fdi {
|
|
for (file_name, encoded_offset) in dir {
|
|
encoded_entries.seek(io::SeekFrom::Start(*encoded_offset as u64))?;
|
|
let entry =
|
|
super::entry::Entry::new_encoded(&mut encoded_entries, version)?;
|
|
|
|
// entry next to file contains full metadata
|
|
//reader.seek(io::SeekFrom::Start(entry.offset))?;
|
|
//let _ = super::entry::Entry::new(&mut reader, version)?;
|
|
|
|
// concat directory with file name to match IndexV1 but should provide a more direct access method
|
|
let path = format!(
|
|
"{}{}",
|
|
dir_name.strip_prefix('/').unwrap_or(dir_name),
|
|
file_name
|
|
);
|
|
entries_by_path.insert(path, entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
assert_eq!(index.read_u32::<LE>()?, 0, "remaining index bytes are 0"); // TODO possibly remaining unencoded entries?
|
|
|
|
Index::V2(IndexV2 {
|
|
path_hash_seed,
|
|
path_hash_index,
|
|
full_directory_index,
|
|
encoded_entries,
|
|
entries_by_path,
|
|
})
|
|
} else {
|
|
let mut entries = HashMap::with_capacity(len);
|
|
for _ in 0..len {
|
|
entries.insert(
|
|
index.read_string()?,
|
|
super::entry::Entry::new(&mut index, version)?,
|
|
);
|
|
}
|
|
Index::V1(IndexV1 { entries })
|
|
};
|
|
|
|
Ok(Pak {
|
|
version,
|
|
mount_point,
|
|
key,
|
|
index,
|
|
})
|
|
}
|
|
|
|
pub fn version(&self) -> super::Version {
|
|
self.pak.version
|
|
}
|
|
|
|
pub fn mount_point(&self) -> &str {
|
|
&self.pak.mount_point
|
|
}
|
|
|
|
pub fn get(&mut self, path: &str) -> Result<Vec<u8>, super::Error> {
|
|
let mut data = Vec::new();
|
|
self.read(path, &mut data)?;
|
|
Ok(data)
|
|
}
|
|
|
|
pub fn read<W: io::Write>(&mut self, path: &str, writer: &mut W) -> Result<(), super::Error> {
|
|
match self.pak.index.entries().get(path) {
|
|
Some(entry) => entry.read(
|
|
&mut self.reader,
|
|
self.pak.version,
|
|
self.pak.key.as_ref(),
|
|
writer,
|
|
),
|
|
None => Err(super::Error::Other("no file found at given path")),
|
|
}
|
|
}
|
|
|
|
pub fn files(&self) -> std::vec::IntoIter<String> {
|
|
self.pak
|
|
.index
|
|
.entries()
|
|
.keys()
|
|
.cloned()
|
|
.collect::<Vec<String>>()
|
|
.into_iter()
|
|
}
|
|
}
|