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