use std::io::{Read, Seek, SeekFrom};
use crate::error::{Error, Result};
use crate::format::read_vec;
pub const PFS0_MAGIC: &[u8; 4] = b"PFS0";
#[derive(Debug, Clone)]
pub struct Pfs0Entry {
pub name: String,
pub offset: u64,
pub size: u64,
pub flag: u32,
}
pub struct Pfs0Reader<R: Read + Seek> {
reader: R,
pub entries: Vec<Pfs0Entry>,
pub header_size: u64,
}
impl<R: Read + Seek> Pfs0Reader<R> {
pub fn new(mut reader: R) -> Result<Pfs0Reader<R>> {
let mut head = [0u8; 16];
reader.read_exact(&mut head)?;
if &head[0..4] != PFS0_MAGIC {
return Err(Error::BadMagic {
expected: "PFS0".into(),
found: hex::encode(&head[0..4]),
});
}
let file_count = u32::from_le_bytes(head[4..8].try_into().unwrap()) as usize;
let string_table_size = u32::from_le_bytes(head[8..12].try_into().unwrap()) as usize;
let header_size = 16 + file_count * 24 + string_table_size;
let table = read_vec(&mut reader, (file_count * 24) as u64)?;
let string_table = read_vec(&mut reader, string_table_size as u64)?;
let mut entries = Vec::with_capacity(file_count);
for i in 0..file_count {
let base = i * 24;
let rel_offset = u64::from_le_bytes(table[base..base + 8].try_into().unwrap());
let size = u64::from_le_bytes(table[base + 8..base + 16].try_into().unwrap());
let string_offset =
u32::from_le_bytes(table[base + 16..base + 20].try_into().unwrap()) as usize;
let flag = u32::from_le_bytes(table[base + 20..base + 24].try_into().unwrap());
let name = read_cstr(&string_table, string_offset)?;
let offset = (header_size as u64)
.checked_add(rel_offset)
.filter(|off| off.checked_add(size).is_some())
.ok_or_else(|| Error::Corrupt(format!("{name}: offset out of range")))?;
entries.push(Pfs0Entry {
name,
offset,
size,
flag,
});
}
Ok(Pfs0Reader {
reader,
entries,
header_size: header_size as u64,
})
}
pub fn read_file(&mut self, entry: &Pfs0Entry) -> Result<Vec<u8>> {
self.reader.seek(SeekFrom::Start(entry.offset))?;
Ok(read_vec(&mut self.reader, entry.size)?)
}
pub fn into_inner(self) -> R {
self.reader
}
}
fn read_cstr(table: &[u8], at: usize) -> Result<String> {
if at >= table.len() {
return Err(Error::Corrupt("string offset out of range".into()));
}
let end = table[at..]
.iter()
.position(|&b| b == 0)
.map(|p| at + p)
.unwrap_or(table.len());
String::from_utf8(table[at..end].to_vec())
.map_err(|_| Error::Corrupt("invalid utf-8 in string table".into()))
}
pub fn padded_string_table_size(names: &[&str]) -> usize {
let non_padded: usize = names.iter().map(|n| n.len() + 1).sum();
let header_non_padded = 0x10 + names.len() * 0x18 + non_padded;
non_padded + align0x20(header_non_padded)
}
pub fn align0x20(n: usize) -> usize {
0x20 - (n % 0x20)
}
pub fn build_header(entries: &[(String, u64, u64)], string_table_size: usize) -> Vec<u8> {
let names: Vec<&str> = entries.iter().map(|(n, _, _)| n.as_str()).collect();
let non_padded: usize = names.iter().map(|n| n.len() + 1).sum();
let mut string_table = String::new();
for (i, n) in names.iter().enumerate() {
if i > 0 {
string_table.push('\0');
}
string_table.push_str(n);
}
string_table.push('\0');
string_table.push_str(&"\0".repeat(string_table_size - non_padded));
let header_size = 0x10 + entries.len() * 0x18 + string_table_size;
let mut h = Vec::with_capacity(header_size);
h.extend_from_slice(PFS0_MAGIC);
h.extend_from_slice(&(entries.len() as u32).to_le_bytes());
h.extend_from_slice(&(string_table_size as u32).to_le_bytes());
h.extend_from_slice(&[0, 0, 0, 0]);
let mut string_offset = 0u32;
for (name, offset, size) in entries {
h.extend_from_slice(&(offset - header_size as u64).to_le_bytes());
h.extend_from_slice(&size.to_le_bytes());
h.extend_from_slice(&string_offset.to_le_bytes());
h.extend_from_slice(&[0, 0, 0, 0]);
string_offset += name.len() as u32 + 1;
}
h.extend_from_slice(string_table.as_bytes());
h
}