mirror of
https://github.com/xavo95/repak.git
synced 2025-01-18 19:04:07 +00:00
initial commit
This commit is contained in:
commit
b2188fc6f8
10 changed files with 258 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*target/
|
||||||
|
*.lock
|
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
BIN
examples/rando_p.pak
Normal file
Binary file not shown.
14
examples/read.rs
Normal file
14
examples/read.rs
Normal 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
9
src/error.rs
Normal 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
25
src/ext.rs
Normal 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
66
src/footer.rs
Normal 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
52
src/lib.rs
Normal 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
17
src/pakentry.rs
Normal 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
52
src/pakfile.rs
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue