use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
use memmap2::Mmap;
use crate::error::{CascError, Result};
use crate::util::io::read_le_u32;
pub const DATA_HEADER_SIZE: usize = 30;
#[derive(Debug, Clone)]
pub struct DataHeader {
pub ekey_hash: [u8; 16],
pub size: u32,
pub flags: [u8; 2],
}
pub struct DataStore {
mmaps: HashMap<u32, Mmap>,
}
pub fn parse_data_header(data: &[u8]) -> Result<DataHeader> {
if data.len() < DATA_HEADER_SIZE {
return Err(CascError::InvalidFormat(format!(
"data header too short: {} bytes (need {})",
data.len(),
DATA_HEADER_SIZE
)));
}
let mut ekey_hash = [0u8; 16];
for i in 0..16 {
ekey_hash[i] = data[15 - i];
}
let size = read_le_u32(&data[0x10..0x14]);
let flags = [data[0x14], data[0x15]];
Ok(DataHeader {
ekey_hash,
size,
flags,
})
}
fn parse_data_filename(name: &str) -> Option<u32> {
let suffix = name.strip_prefix("data.")?;
suffix.parse::<u32>().ok()
}
impl DataStore {
pub fn open(data_dir: &Path) -> Result<Self> {
let pattern = data_dir.join("data.*");
let pattern_str = pattern.to_string_lossy().to_string();
let mut mmaps = HashMap::new();
for path in glob::glob(&pattern_str)
.map_err(|e| CascError::InvalidFormat(format!("glob error: {e}")))?
{
let path = path.map_err(|e| CascError::Io(e.into_error()))?;
let fname = match path.file_name().and_then(|f| f.to_str()) {
Some(f) => f.to_owned(),
None => continue,
};
if let Some(archive_num) = parse_data_filename(&fname) {
let file = File::open(&path)?;
let mmap = unsafe { Mmap::map(&file)? };
mmaps.insert(archive_num, mmap);
}
}
Ok(Self { mmaps })
}
pub fn read_entry(&self, archive_number: u32, offset: u64, size: u32) -> Result<&[u8]> {
let raw = self.read_raw(archive_number, offset, size)?;
if raw.len() < DATA_HEADER_SIZE {
return Err(CascError::InvalidFormat(
"data entry too small to contain header".into(),
));
}
Ok(&raw[DATA_HEADER_SIZE..])
}
pub fn read_raw(&self, archive_number: u32, offset: u64, size: u32) -> Result<&[u8]> {
let mmap = self.mmaps.get(&archive_number).ok_or_else(|| {
CascError::InvalidFormat(format!("data.{:03} not found", archive_number))
})?;
let start = offset as usize;
let end = start + size as usize;
if end > mmap.len() {
return Err(CascError::InvalidFormat(format!(
"data.{:03} read out of bounds: offset={}, size={}, file_len={}",
archive_number,
offset,
size,
mmap.len()
)));
}
Ok(&mmap[start..end])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_data_header_valid() {
let mut header = [0u8; 30];
for (i, slot) in header.iter_mut().enumerate().take(16) {
*slot = (16 - i) as u8;
}
header[0x10..0x14].copy_from_slice(&1000u32.to_le_bytes());
header[0x14] = 0;
header[0x15] = 0;
let dh = parse_data_header(&header).unwrap();
assert_eq!(dh.ekey_hash[0], 1);
assert_eq!(dh.ekey_hash[15], 16);
assert_eq!(dh.size, 1000);
assert_eq!(dh.flags, [0, 0]);
}
#[test]
fn data_header_size_includes_header() {
let mut header = [0u8; 30];
header[0x10..0x14].copy_from_slice(&30u32.to_le_bytes());
let dh = parse_data_header(&header).unwrap();
assert_eq!(dh.size, 30);
}
#[test]
fn data_header_too_short() {
let header = [0u8; 10];
assert!(parse_data_header(&header).is_err());
}
}