commit b2188fc6f823ff9659c2ec39c653c52dd84014fe Author: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Tue Jan 3 10:33:42 2023 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0595486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*target/ +*.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a775799 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "un-pak" +authors = ["spuds"] +repository = "https://github.com/bananaturtlesandwich/un-pak" +description = "a no-nonsense unreal pak parsing crate" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +byteorder = "*" +strum = { version = "*", features = ["derive"] } +hashbrown = "*" +thiserror = "*" + +[profile.release] +strip = true +lto = true +codegen-units = 1 +panic = "abort" diff --git a/examples/rando_p.pak b/examples/rando_p.pak new file mode 100644 index 0000000..e63d229 Binary files /dev/null and b/examples/rando_p.pak differ diff --git a/examples/read.rs b/examples/read.rs new file mode 100644 index 0000000..4069000 --- /dev/null +++ b/examples/read.rs @@ -0,0 +1,14 @@ +fn main() -> Result<(), un_pak::Error> { + for version in un_pak::Version::iter().rev() { + match un_pak::PakFile::new(version, std::io::Cursor::new(include_bytes!("rando_p.pak"))) { + Ok(_) => { + println!("parsed successfully!"); + return Ok(()); + } + Err(e) => println!("{e}"), + } + } + Err(un_pak::Error::PakInvalid( + "no version can parse".to_string(), + )) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e1e7991 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,9 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("error parsing pak: {0}")] + PakInvalid(String), + #[error("error reading file: {0}")] + IoError(#[from] std::io::Error), + #[error("error converting to enum: {0}")] + StrumError(#[from] strum::ParseError), +} diff --git a/src/ext.rs b/src/ext.rs new file mode 100644 index 0000000..4b88be6 --- /dev/null +++ b/src/ext.rs @@ -0,0 +1,25 @@ +use byteorder::ReadBytesExt; + +pub trait ReadExt { + fn read_bool(&mut self) -> Result; + fn read_guid(&mut self) -> Result<[u8; 20], super::Error>; + fn read_len(&mut self, len: usize) -> Result, super::Error>; +} + +impl ReadExt for R { + fn read_bool(&mut self) -> Result { + Ok(self.read_u8()? != 0) + } + + fn read_guid(&mut self) -> Result<[u8; 20], super::Error> { + let mut guid = [0; 20]; + self.read_exact(&mut guid)?; + Ok(guid) + } + + fn read_len(&mut self, len: usize) -> Result, super::Error> { + let mut buf = Vec::with_capacity(len); + self.read_exact(&mut buf)?; + Ok(buf) + } +} diff --git a/src/footer.rs b/src/footer.rs new file mode 100644 index 0000000..9dffd7f --- /dev/null +++ b/src/footer.rs @@ -0,0 +1,66 @@ +use std::{io, str::FromStr}; + +use byteorder::{ReadBytesExt, LE}; + +use super::{Compression, ReadExt, Version}; + +pub struct Footer { + pub encryption_guid: Option<[u8; 20]>, + pub encrypted: Option, + pub magic: u32, + pub version: Version, + pub offset: u64, + pub size: u64, + pub hash: [u8; 20], + pub frozen: Option, + pub compression: Option>, +} + +impl Footer { + pub fn new(reader: &mut R, version: &Version) -> Result { + let footer = Footer { + encryption_guid: (version >= &Version::EncryptionKeyGuid) + .then_some(reader.read_guid()?), + encrypted: (version >= &Version::CompressionEncryption).then_some(reader.read_bool()?), + magic: reader.read_u32::()?, + version: Version::from_repr(reader.read_u32::()?).unwrap_or_default(), + offset: reader.read_u64::()?, + size: reader.read_u64::()?, + hash: reader.read_guid()?, + frozen: (version == &Version::FrozenIndex).then_some(reader.read_bool()?), + compression: (version >= &Version::FNameBasedCompression).then_some({ + let mut compression = + Vec::with_capacity(if version == &Version::FNameBasedCompression { + 4 + } else { + 5 + }); + for _ in 0..compression.capacity() { + compression.push(Compression::from_str( + &reader + .read_len(32)? + .iter() + // filter out whitespace and convert to char + .filter_map(|&ch| (ch != 0).then_some(ch as char)) + .collect::(), + )?) + } + compression + }), + }; + if super::MAGIC != footer.magic { + return Err(super::Error::PakInvalid(format!( + "incorrect magic - expected {} but got {}", + super::MAGIC, + footer.magic + ))); + } + if version != &footer.version { + return Err(super::Error::PakInvalid(format!( + "incorrect version - parsed with {} but is {}", + version, footer.version + ))); + } + Ok(footer) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8ef5ded --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,52 @@ +#![allow(dead_code)] +mod error; +mod ext; +mod footer; +mod pakentry; +mod pakfile; + +pub use {error::*, ext::*, footer::*, pakentry::*, pakfile::*}; + +pub const MAGIC: u32 = 0x5A6F12E1; + +#[repr(u32)] +#[derive( + Default, + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Debug, + strum::Display, + strum::FromRepr, + strum::EnumIter, +)] +pub enum Version { + Unknown, // unknown (mostly just for padding :p) + Initial, // initial specification + NoTimestamps, // timestamps removed + CompressionEncryption, // compression and encryption support + IndexEncryption, // index encryption support + RelativeChunkOffsets, // offsets are relative to header + DeleteRecords, // record deletion support + EncryptionKeyGuid, // include key GUID + FNameBasedCompression, // compression names included + FrozenIndex, // frozen index byte included + #[default] + PathHashIndex, // more compression methods +} + +// i don't want people to need to install strum +impl Version { + pub fn iter() -> VersionIter { + ::iter() + } +} + +#[derive(Copy, Clone, Debug, strum::Display, strum::EnumString)] +pub enum Compression { + Zlib, + Gzip, + Oodle, +} diff --git a/src/pakentry.rs b/src/pakentry.rs new file mode 100644 index 0000000..604182d --- /dev/null +++ b/src/pakentry.rs @@ -0,0 +1,17 @@ +pub struct PakEntry { + pub offset: u64, + pub compressed: u64, + pub decompressed: u64, + pub compression_method: super::Compression, + pub hash: [u8; 20], + pub compression_blocks: Vec, + pub flags: Vec, + pub block_size: u32, +} + +pub struct Block { + /// start offset relative to the start of the entry header + pub offset: u64, + /// size of the compressed block + pub size: u64, +} diff --git a/src/pakfile.rs b/src/pakfile.rs new file mode 100644 index 0000000..1add19d --- /dev/null +++ b/src/pakfile.rs @@ -0,0 +1,52 @@ +use std::io; + +use super::Version; + +pub struct PakFile { + pub version: Version, + pub footer: super::Footer, + pub entries: hashbrown::HashMap, +} + +impl PakFile { + pub fn new( + version: super::Version, + mut reader: R, + ) -> Result { + reader.seek(io::SeekFrom::End(-(footer_size(&version) as i64)))?; + // parse footer info to get index offset + let footer = super::Footer::new(&mut reader, &version)?; + reader.seek(io::SeekFrom::Start(footer.offset))?; + Ok(Self { + version, + footer, + entries: hashbrown::HashMap::new(), + }) + } +} + +fn footer_size(version: &Version) -> u32 { + // (magic + version): u32 + (offset + size): u64 + hash: [u8; 20] + let mut size = 4 * 2 + 8 * 2 + 20; + if version >= &Version::IndexEncryption { + // encrypted: bool + size += 1; + } + if version >= &Version::EncryptionKeyGuid { + // encryption guid: [u8; 20] + size += 10; + } + if version >= &Version::FNameBasedCompression { + // compression names: [[u8; 32]; 4] + size += 32 * 4; + } + if version >= &Version::FrozenIndex { + // extra compression name: [u8; 32] + size += 32 + } + if version == &Version::FrozenIndex { + // frozen index: bool + size += 1; + } + size +}