Skip to main content

casc_lib/storage/
data.rs

1//! Reader for CASC `data.NNN` archive files.
2//!
3//! Each archive file contains a sequence of entries, where every entry starts with
4//! a 30-byte header (EKey hash in reversed byte order, size, and flags) followed
5//! by the BLTE-encoded payload. Files are memory-mapped for efficient random access
6//! via [`DataStore`](crate::storage::data::DataStore).
7
8use std::collections::HashMap;
9use std::fs::File;
10use std::path::Path;
11
12use memmap2::Mmap;
13
14use crate::error::{CascError, Result};
15use crate::util::io::read_le_u32;
16
17/// Size of the per-entry header in a data.NNN file.
18pub const DATA_HEADER_SIZE: usize = 30;
19
20/// Parsed data header from a data.NNN file.
21#[derive(Debug, Clone)]
22pub struct DataHeader {
23    /// EKey hash (reversed from on-disk format, restored to normal order).
24    pub ekey_hash: [u8; 16],
25    /// Total size: header (30) + BLTE data.
26    pub size: u32,
27    /// Flags.
28    pub flags: [u8; 2],
29}
30
31/// Manages memory-mapped data.NNN files.
32pub struct DataStore {
33    /// Memory-mapped data files, keyed by archive number.
34    mmaps: HashMap<u32, Mmap>,
35}
36
37// ---------------------------------------------------------------------------
38// Public helpers
39// ---------------------------------------------------------------------------
40
41/// Parse the 30-byte data header. The on-disk EKey is stored in reversed byte
42/// order; this function restores it to the normal order.
43pub fn parse_data_header(data: &[u8]) -> Result<DataHeader> {
44    if data.len() < DATA_HEADER_SIZE {
45        return Err(CascError::InvalidFormat(format!(
46            "data header too short: {} bytes (need {})",
47            data.len(),
48            DATA_HEADER_SIZE
49        )));
50    }
51
52    // EKey hash - stored reversed on disk, restore to normal order
53    let mut ekey_hash = [0u8; 16];
54    for i in 0..16 {
55        ekey_hash[i] = data[15 - i];
56    }
57
58    let size = read_le_u32(&data[0x10..0x14]);
59    let flags = [data[0x14], data[0x15]];
60
61    Ok(DataHeader {
62        ekey_hash,
63        size,
64        flags,
65    })
66}
67
68/// Parse a data.NNN filename and return the archive number.
69/// e.g. `"data.042"` -> `Some(42)`
70fn parse_data_filename(name: &str) -> Option<u32> {
71    let suffix = name.strip_prefix("data.")?;
72    suffix.parse::<u32>().ok()
73}
74
75impl DataStore {
76    /// Open and memory-map all `data.NNN` files found in `data_dir`.
77    pub fn open(data_dir: &Path) -> Result<Self> {
78        let pattern = data_dir.join("data.*");
79        let pattern_str = pattern.to_string_lossy().to_string();
80
81        let mut mmaps = HashMap::new();
82
83        for path in glob::glob(&pattern_str)
84            .map_err(|e| CascError::InvalidFormat(format!("glob error: {e}")))?
85        {
86            let path = path.map_err(|e| CascError::Io(e.into_error()))?;
87            let fname = match path.file_name().and_then(|f| f.to_str()) {
88                Some(f) => f.to_owned(),
89                None => continue,
90            };
91
92            if let Some(archive_num) = parse_data_filename(&fname) {
93                let file = File::open(&path)?;
94                let mmap = unsafe { Mmap::map(&file)? };
95                mmaps.insert(archive_num, mmap);
96            }
97        }
98
99        Ok(Self { mmaps })
100    }
101
102    /// Read the BLTE payload for an entry (skips the 30-byte data header).
103    pub fn read_entry(&self, archive_number: u32, offset: u64, size: u32) -> Result<&[u8]> {
104        let raw = self.read_raw(archive_number, offset, size)?;
105        if raw.len() < DATA_HEADER_SIZE {
106            return Err(CascError::InvalidFormat(
107                "data entry too small to contain header".into(),
108            ));
109        }
110        Ok(&raw[DATA_HEADER_SIZE..])
111    }
112
113    /// Read the raw bytes for an entry (including the 30-byte header).
114    pub fn read_raw(&self, archive_number: u32, offset: u64, size: u32) -> Result<&[u8]> {
115        let mmap = self.mmaps.get(&archive_number).ok_or_else(|| {
116            CascError::InvalidFormat(format!("data.{:03} not found", archive_number))
117        })?;
118
119        let start = offset as usize;
120        let end = start + size as usize;
121
122        if end > mmap.len() {
123            return Err(CascError::InvalidFormat(format!(
124                "data.{:03} read out of bounds: offset={}, size={}, file_len={}",
125                archive_number,
126                offset,
127                size,
128                mmap.len()
129            )));
130        }
131
132        Ok(&mmap[start..end])
133    }
134}
135
136// ---------------------------------------------------------------------------
137// Tests
138// ---------------------------------------------------------------------------
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn parse_data_header_valid() {
146        let mut header = [0u8; 30];
147        // EKey hash (reversed on disk): put 0x01..0x10 reversed
148        for (i, slot) in header.iter_mut().enumerate().take(16) {
149            *slot = (16 - i) as u8;
150        }
151        // Size = 1000 (LE)
152        header[0x10..0x14].copy_from_slice(&1000u32.to_le_bytes());
153        // Flags = [0, 0]
154        header[0x14] = 0;
155        header[0x15] = 0;
156
157        let dh = parse_data_header(&header).unwrap();
158        // After reversing, should be 0x01..0x10
159        assert_eq!(dh.ekey_hash[0], 1);
160        assert_eq!(dh.ekey_hash[15], 16);
161        assert_eq!(dh.size, 1000);
162        assert_eq!(dh.flags, [0, 0]);
163    }
164
165    #[test]
166    fn data_header_size_includes_header() {
167        let mut header = [0u8; 30];
168        header[0x10..0x14].copy_from_slice(&30u32.to_le_bytes());
169        let dh = parse_data_header(&header).unwrap();
170        assert_eq!(dh.size, 30);
171    }
172
173    #[test]
174    fn data_header_too_short() {
175        let header = [0u8; 10];
176        assert!(parse_data_header(&header).is_err());
177    }
178}