#![warn(
missing_docs,
rust_2018_idioms,
missing_debug_implementations,
rustdoc::broken_intra_doc_links
)]
pub mod error;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::str;
use error::{BsaError, Result};
const MAGIC_HEADER: &[u8] = &[0x0, 0x1, 0x0, 0x0];
pub type FileList = Vec<FileStruct>;
fn calculate_hash(name: &str) -> u64 {
let lower_name = name.to_ascii_lowercase();
let characters: Vec<char> = lower_name.chars().collect();
let l = lower_name.chars().count() as u32 >> 1;
let (mut sum, mut off): (u32, u32) = (0, 0);
for (i, c) in characters.iter().enumerate() {
if i as u32 >= l {
break;
}
sum ^= (*c as u32) << (off & 0x1F);
off += 8;
}
let low = sum;
let mut sum: u64 = 0;
off = 0;
let (mut temp, mut n);
for c in characters.iter() {
temp = (*c as u32) << (off & 0x1F);
sum ^= temp as u64;
n = temp & 0x1F;
sum = (sum << (32 - n)) | (sum >> n); off += 8;
}
let high = sum;
(low as u64) | (high << 32)
}
fn check_bytes_written(expected: u32, actual: usize) -> Result<()> {
if expected != actual as u32 {
return Err(BsaError::BytesWritten { expected, actual });
}
Ok(())
}
#[derive(Debug)]
pub struct FileStruct {
pub file_size: usize,
pub offset: u32,
pub name: String,
}
#[derive(Debug, Default)]
pub struct BSAFile<'a> {
files: FileList,
is_loaded: bool,
filename: &'a str,
lookup: HashMap<String, u32>,
}
impl<'a> BSAFile<'a> {
pub fn new(file: &'a str) -> Result<Self> {
let mut obj = Self::default();
obj.open(file)?;
Ok(obj)
}
pub fn open(&mut self, file: &'a str) -> Result<()> {
self.filename = file;
self.files.clear();
self.is_loaded = false;
self.lookup.clear();
self.read_header()
}
pub fn exists(&self, file: &str) -> bool {
self.ensure_loaded().unwrap();
self.get_index(file).is_ok()
}
pub fn get_file(&self, file: &str) -> Result<Vec<u8>> {
self.ensure_loaded()?;
let i = self.get_index(file)?;
let fs = &self.files[i as usize];
let mut file = File::open(self.filename)?;
file.seek(SeekFrom::Start(fs.offset as u64))?;
let mut buf = vec![0u8; fs.file_size];
file.read_exact(&mut buf)?;
Ok(buf)
}
pub fn get_list(&self) -> &FileList {
self.ensure_loaded().unwrap();
&self.files
}
fn ensure_loaded(&self) -> Result<()> {
if !self.is_loaded {
return Err(BsaError::NotOpen);
}
Ok(())
}
fn ensure_not_loaded(&self) -> Result<()> {
if self.is_loaded {
return Err(BsaError::AlreadyOpen);
}
Ok(())
}
fn read_header(&mut self) -> Result<()> {
self.ensure_not_loaded()?;
let mut file = File::open(self.filename)?;
let fsize = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(0))?;
if fsize < 12 {
return Err(BsaError::TooSmall(fsize));
}
let dirsize: u32;
let filenum: u32;
{
let mut buff = [0u8; 4];
file.read_exact(&mut buff)?;
if buff[..4] != *MAGIC_HEADER {
return Err(BsaError::BadHeader);
}
file.read_exact(&mut buff)?;
dirsize = u32::from_le_bytes(buff);
file.read_exact(&mut buff)?;
filenum = u32::from_le_bytes(buff);
}
if (filenum as u64 * 21 > (fsize - 12))
|| (dirsize as u64 + 8 * filenum as u64 > (fsize - 12))
{
return Err(BsaError::DirSize);
}
let mut offsets: Vec<u32> = Vec::with_capacity(filenum as usize);
let mut offsets_handle = file.take(12 * filenum as u64);
let mut offsets_buffer = Vec::new();
offsets_handle.read_to_end(&mut offsets_buffer)?;
let mut buff: [u8; 4];
for b in (0..12 * filenum as usize).step_by(4) {
buff = offsets_buffer[b..b + 4].try_into().unwrap();
offsets.push(u32::from_le_bytes(buff));
}
let mut file = offsets_handle.get_ref();
let mut buff: Vec<u8> = vec![0; dirsize as usize - 12 * filenum as usize]; file.read_exact(&mut buff)?;
let string_buf = String::from_utf8(buff).unwrap();
let string_vec = string_buf.split('\0').collect::<Vec<&str>>();
if file.stream_position()? != 12 + dirsize as u64 {
return Err(BsaError::Position {
expected: 12 + dirsize,
actual: file.stream_position()?,
});
}
let file_data_offset = 12 + dirsize + 8 * filenum;
for i in 0..filenum {
let fs = FileStruct {
file_size: offsets[i as usize * 2] as usize,
offset: offsets[i as usize * 2 + 1] + file_data_offset,
name: string_vec[i as usize].to_string(),
};
if fs.offset as u64 + fs.file_size as u64 > fsize {
return Err(BsaError::OffsetOutside);
}
self.lookup.insert(fs.name.to_string(), i);
self.files.push(fs);
}
self.is_loaded = true;
Ok(())
}
pub fn create(&mut self, file: &'a str, filenames: &[String]) -> Result<()> {
self.ensure_not_loaded()?;
self.filename = file;
let mut bytes_written: usize = 0;
let filenum = filenames.len() as u32;
let mut total_files_size: u32 = 0;
for (i, filename) in filenames.iter().enumerate() {
let archive_path = filename.to_ascii_lowercase().replace('/', "\\");
let metadata = fs::metadata(filename)?;
let fsize = metadata.len();
let fs = FileStruct {
file_size: fsize as usize,
name: archive_path,
offset: total_files_size,
};
total_files_size += fsize as u32;
self.lookup.insert(fs.name.to_string(), i as u32);
self.files.push(fs);
}
let f = File::create(file).expect("Unable to create file");
let mut f = BufWriter::new(f);
bytes_written += f.write(MAGIC_HEADER)?;
let mut hash_offset: u32 = 12 * filenum;
for file in &self.files {
hash_offset += file.name.chars().count() as u32 + 1;
}
bytes_written += f.write(&hash_offset.to_le_bytes())?;
bytes_written += f.write(&filenum.to_le_bytes())?;
check_bytes_written(12, bytes_written)?;
for file in &self.files {
bytes_written += f.write(&file.file_size.to_le_bytes())?;
bytes_written += f.write(&file.offset.to_le_bytes())?;
}
let mut starting_offset: u32 = 0;
for file in &self.files {
bytes_written += f.write(&starting_offset.to_le_bytes())?;
let mut filename_length = file.name.chars().count() as u32;
filename_length += 1; starting_offset += filename_length;
}
check_bytes_written(12 + 12 * filenum, bytes_written)?;
let null_term = [b'\0'];
for file in &self.files {
bytes_written += f.write(file.name.as_bytes())?;
bytes_written += f.write(&null_term)?;
}
for file in &self.files {
let terminated = file.name.to_string() + "\0";
let hash = calculate_hash(&terminated);
bytes_written += f.write(&hash.to_le_bytes())?;
}
for (i, filename) in filenames.iter().enumerate() {
let mut read_buf: Vec<u8> = Vec::new();
let rfile = File::open(filename)?;
let mut reader = BufReader::new(rfile);
reader.read_to_end(&mut read_buf)?;
check_bytes_written(read_buf.len() as u32, self.files.get(i).unwrap().file_size)?;
f.write_all(&read_buf)?;
}
f.flush()?;
Ok(())
}
fn get_index(&self, file: &str) -> Result<u32> {
match self.lookup.get(file) {
Some(&index) => Ok(index),
None => Err(BsaError::FileNotFound(file.to_string())),
}
}
}