initial commit

This commit is contained in:
spuds 2023-01-03 10:33:42 +00:00
commit b2188fc6f8
No known key found for this signature in database
GPG key ID: 0B6CA6068E827C8F
10 changed files with 258 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*target/
*.lock

21
Cargo.toml Normal file
View file

@ -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"

BIN
examples/rando_p.pak Normal file

Binary file not shown.

14
examples/read.rs Normal file
View file

@ -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(),
))
}

9
src/error.rs Normal file
View file

@ -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),
}

25
src/ext.rs Normal file
View file

@ -0,0 +1,25 @@
use byteorder::ReadBytesExt;
pub trait ReadExt {
fn read_bool(&mut self) -> Result<bool, super::Error>;
fn read_guid(&mut self) -> Result<[u8; 20], super::Error>;
fn read_len(&mut self, len: usize) -> Result<Vec<u8>, super::Error>;
}
impl<R: std::io::Read> ReadExt for R {
fn read_bool(&mut self) -> Result<bool, super::Error> {
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<Vec<u8>, super::Error> {
let mut buf = Vec::with_capacity(len);
self.read_exact(&mut buf)?;
Ok(buf)
}
}

66
src/footer.rs Normal file
View file

@ -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<bool>,
pub magic: u32,
pub version: Version,
pub offset: u64,
pub size: u64,
pub hash: [u8; 20],
pub frozen: Option<bool>,
pub compression: Option<Vec<Compression>>,
}
impl Footer {
pub fn new<R: io::Read>(reader: &mut R, version: &Version) -> Result<Self, super::Error> {
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::<LE>()?,
version: Version::from_repr(reader.read_u32::<LE>()?).unwrap_or_default(),
offset: reader.read_u64::<LE>()?,
size: reader.read_u64::<LE>()?,
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::<String>(),
)?)
}
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)
}
}

52
src/lib.rs Normal file
View file

@ -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 {
<Version as strum::IntoEnumIterator>::iter()
}
}
#[derive(Copy, Clone, Debug, strum::Display, strum::EnumString)]
pub enum Compression {
Zlib,
Gzip,
Oodle,
}

17
src/pakentry.rs Normal file
View file

@ -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<Block>,
pub flags: Vec<u8>,
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,
}

52
src/pakfile.rs Normal file
View file

@ -0,0 +1,52 @@
use std::io;
use super::Version;
pub struct PakFile {
pub version: Version,
pub footer: super::Footer,
pub entries: hashbrown::HashMap<String, super::PakEntry>,
}
impl PakFile {
pub fn new<R: io::Read + io::Seek>(
version: super::Version,
mut reader: R,
) -> Result<Self, super::Error> {
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
}