Skip to content

Commit

Permalink
add bsa writer
Browse files Browse the repository at this point in the history
  • Loading branch information
lxndr committed Feb 23, 2023
1 parent 4d4046e commit 1b457ce
Show file tree
Hide file tree
Showing 33 changed files with 1,055 additions and 440 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "flpak"
version = "0.4.0"
edition = "2021"
rust-version = "1.67.1"

[lib]
name = "flpak"
Expand All @@ -12,6 +13,7 @@ name = "flpak"
path = "src/bin/cli.rs"

[dependencies]
bitflags = "1.3.2"
clap = { version = "4.1.4", features = ["derive"] }
crc32fast = "1.3.2"
glob = "0.3.1"
Expand Down
Binary file added samples/bsa/correct_v105.bsa
Binary file not shown.
5 changes: 2 additions & 3 deletions src/bin/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use clap::{Parser, Subcommand};
use std::{fs, io, path::PathBuf};

use flpak::{reader, registry::Registry, FileType, InputFileListBuilder};
use std::{collections::HashMap, fs, io, path::PathBuf};

#[derive(Parser)]
#[command(about = "An archive utility", long_about = None)]
Expand Down Expand Up @@ -290,7 +289,7 @@ fn main() -> std::io::Result<()> {
)
})?;

writer_fn(input_files, &output_file).map_err(|err| {
writer_fn(input_files, &output_file, HashMap::new()).map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to create archive: {}", err),
Expand Down
1 change: 0 additions & 1 deletion src/lib/bsa/common.rs

This file was deleted.

24 changes: 0 additions & 24 deletions src/lib/bsa/decompressor.rs

This file was deleted.

173 changes: 92 additions & 81 deletions src/lib/bsa/hash.rs
Original file line number Diff line number Diff line change
@@ -1,106 +1,117 @@
pub fn calc_file_name_hash(name: &str) -> u64 {
let filename = normalize_path(name);
use std::{fmt, io};

let (name, ext) = match filename.rfind('.') {
Some(pos) => (&filename[..pos], &filename[pos..]),
None => (filename.as_str(), ""),
};
use crate::{ReadEx, WriteEx};

let name_bytes = name.as_bytes();
let name_len = name_bytes.len();
let ext_bytes = ext.as_bytes();
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Hash(u64);

let mut hash1 = calc_name_hash(name);
hash1 |= calc_ext_hash(ext);
impl Hash {
pub fn from_file_name(fname: &str) -> Hash {
let (name, ext) = match fname.rfind('.') {
Some(pos) => (&fname[..pos], &fname[pos..]),
None => (fname, ""),
};

let hash2: u64 = if name_len > 3 {
calc_slice_hash(&name_bytes[1..name_len - 2]).wrapping_add(calc_slice_hash(ext_bytes))
} else {
calc_slice_hash(ext_bytes)
};
let name_bytes = name.as_bytes();
let name_len = name_bytes.len();
let ext_bytes = ext.as_bytes();

(hash2 << 32) + (hash1 as u64)
}
let mut hash1 = Self::calc_name_hash(name);
hash1 |= Self::calc_ext_hash(ext);

pub fn calc_folder_name_hash(name: &str) -> u64 {
let name = normalize_path(name);
let name_bytes = name.as_bytes();
let name_len = name_bytes.len();
let hash2: u64 = if name_len > 3 {
Self::calc_slice_hash(&name_bytes[1..name_len - 2])
.wrapping_add(Self::calc_slice_hash(ext_bytes))
} else {
Self::calc_slice_hash(ext_bytes)
};

let hash1 = calc_name_hash(&name);
let hash2: u64 = if name_len > 3 {
calc_slice_hash(&name_bytes[1..name_len - 2])
} else {
0
};
Hash((hash2 << 32) + u64::from(hash1))
}

(hash2 << 32) + u64::from(hash1)
}
pub fn from_folder_path(name: &str) -> Hash {
let name = Self::normalize_path(name);
let name_bytes = name.as_bytes();
let name_len = name_bytes.len();

fn normalize_path(path: &str) -> String {
assert!(!path.is_empty(), "Path cannot be empty");
path.to_ascii_lowercase().replace('/', "\\")
}
let hash1 = Self::calc_name_hash(&name);
let hash2: u64 = if name_len > 3 {
Self::calc_slice_hash(&name_bytes[1..name_len - 2])
} else {
0
};

fn calc_name_hash(name: &str) -> u32 {
let bytes = name.as_bytes();
let len = bytes.len();
Hash((hash2 << 32) + u64::from(hash1))
}

let mut hash: u32 = u32::from(bytes[len - 1]);
hash |= u32::from(if len < 3 { 0 } else { bytes[len - 2] }) << 8;
hash |= (len as u32) << 16;
hash |= u32::from(bytes[0]) << 24;
fn normalize_path(path: &str) -> String {
assert!(!path.is_empty(), "Path cannot be empty");
path.to_ascii_lowercase().replace('/', "\\")
}

hash
}
fn calc_name_hash(name: &str) -> u32 {
let bytes = name.as_bytes();
let len = bytes.len();

let mut hash: u32 = u32::from(bytes[len - 1]);
hash |= u32::from(if len < 3 { 0 } else { bytes[len - 2] }) << 8;
hash |= (len as u32) << 16;
hash |= u32::from(bytes[0]) << 24;

fn calc_ext_hash(ext: &str) -> u32 {
match ext {
".kf" => 0x80,
".nif" => 0x8000,
".dds" => 0x8080,
".wav" => 0x80000000,
_ => 0,
hash
}

fn calc_ext_hash(ext: &str) -> u32 {
match ext {
".kf" => 0x80,
".nif" => 0x8000,
".dds" => 0x8080,
".wav" => 0x80000000,
_ => 0,
}
}

fn calc_slice_hash(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0;

for &byte in bytes {
hash = hash.wrapping_mul(0x1003f).wrapping_add(u64::from(byte));
}

hash
}
}

fn calc_slice_hash(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0;
impl From<u64> for Hash {
fn from(val: u64) -> Self {
Self(val)
}
}

for &byte in bytes {
hash = hash.wrapping_mul(0x1003f).wrapping_add(u64::from(byte));
impl From<Hash> for u64 {
fn from(val: Hash) -> Self {
val.0
}
}

hash
impl fmt::LowerHex for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}

#[cfg(test)]
mod tests {
#[test]
fn calc_file_name_hash() {
assert_eq!(
super::calc_file_name_hash("dlcanchfrelevmachplatf01.nif"),
3351549684976496689,
);
assert_eq!(super::calc_file_name_hash("go.nif"), 10578188054420422767,);
pub trait ReadHash: io::BufRead {
fn read_hash(&mut self, big_endian: bool) -> io::Result<Hash> {
Ok(Hash(self.read_u64(big_endian)?))
}
}

#[test]
fn calc_folder_name_hash() {
assert_eq!(
super::calc_folder_name_hash(&String::from(
"sound\\voice\\hearthfires.esm\\femaleelfhaughty"
)),
18022389080945785,
);
assert_eq!(super::calc_folder_name_hash(&String::from("x")), 2013331576,);
assert_eq!(
super::calc_folder_name_hash(&String::from("xx")),
2013397112,
);
assert_eq!(
super::calc_folder_name_hash(&String::from("xxx")),
2013493368,
);
impl<R: io::BufRead + ?Sized> ReadHash for R {}

pub trait WriteHash: io::Write {
fn write_hash(&mut self, hash: &Hash, big_endian: bool) -> io::Result<()> {
self.write_u64(hash.0, big_endian)
}
}

impl<R: io::Write + ?Sized> WriteHash for R {}
35 changes: 35 additions & 0 deletions src/lib/bsa/hash_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use super::Hash;

#[test]
fn calc_file_name_hash() {
assert_eq!(
Hash::from_file_name("dlcanchfrelevmachplatf01.nif"),
Hash::from(0x2E8318AE6418B031),
);
assert_eq!(
Hash::from_file_name("go.nif"),
Hash::from(0x92CD45FD6702806F),
);
}

#[test]
fn calc_folder_name_hash() {
assert_eq!(
Hash::from_folder_path(&String::from(
"sound\\voice\\hearthfires.esm\\femaleelfhaughty"
)),
Hash::from(0x00400744732C7479),
);
assert_eq!(
Hash::from_folder_path(&String::from("x")),
Hash::from(0x0000000078010078),
);
assert_eq!(
Hash::from_folder_path(&String::from("xx")),
Hash::from(0x0000000078020078),
);
assert_eq!(
Hash::from_folder_path(&String::from("xxx")),
Hash::from(0x0000000078037878),
);
}
Loading

0 comments on commit 1b457ce

Please sign in to comment.