casc_storage/storage/
loose_files.rs

1//! Support for loose files (individual files not in archives)
2
3use crate::error::{CascError, Result};
4use crate::types::EKey;
5use std::collections::HashMap;
6use std::io::{Read, Seek};
7use std::path::PathBuf;
8use tracing::{debug, info};
9
10/// Storage for loose files (not in archives)
11pub struct LooseFileStorage {
12    /// Mapping from EKey to file path
13    files: HashMap<EKey, PathBuf>,
14    /// Base directory for loose files
15    base_path: PathBuf,
16}
17
18impl LooseFileStorage {
19    /// Parse a hex string filename into an EKey
20    fn parse_hex_filename(filename: &str) -> Option<EKey> {
21        if filename.len() != 32 {
22            return None;
23        }
24
25        let mut bytes = [0u8; 16];
26        for i in 0..16 {
27            let hex_pair = &filename[i * 2..i * 2 + 2];
28            bytes[i] = u8::from_str_radix(hex_pair, 16).ok()?;
29        }
30
31        EKey::from_slice(&bytes)
32    }
33
34    /// Create a new loose file storage
35    pub fn new(base_path: PathBuf) -> Result<Self> {
36        std::fs::create_dir_all(&base_path)?;
37        Ok(Self {
38            files: HashMap::new(),
39            base_path,
40        })
41    }
42
43    /// Scan directory for loose files
44    pub fn scan(&mut self) -> Result<()> {
45        info!("Scanning for loose files in {:?}", self.base_path);
46
47        self.files.clear();
48
49        // Look for files with EKey-based names
50        for entry in std::fs::read_dir(&self.base_path)? {
51            let entry = entry?;
52            let path = entry.path();
53
54            if path.is_file() {
55                if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
56                    // Try to parse filename as hex EKey
57                    if filename.len() == 32 {
58                        if let Some(ekey) = Self::parse_hex_filename(filename) {
59                            debug!("Found loose file: {}", ekey);
60                            self.files.insert(ekey, path);
61                        }
62                    }
63                }
64            }
65        }
66
67        info!("Found {} loose files", self.files.len());
68        Ok(())
69    }
70
71    /// Read a loose file
72    pub fn read(&self, ekey: &EKey) -> Result<Vec<u8>> {
73        let path = self
74            .files
75            .get(ekey)
76            .ok_or_else(|| CascError::EntryNotFound(ekey.to_string()))?;
77
78        debug!("Reading loose file {} from {:?}", ekey, path);
79
80        // Use streaming approach to avoid loading file twice
81        let mut file = std::fs::File::open(path)?;
82
83        // Check if file is BLTE compressed by reading magic
84        let mut magic = [0u8; 4];
85        file.read_exact(&mut magic)?;
86
87        if magic == blte::BLTE_MAGIC {
88            debug!(
89                "Loose file {} is BLTE compressed, using streaming decompression",
90                ekey
91            );
92            // Seek back to beginning for BLTE parser
93            file.seek(std::io::SeekFrom::Start(0))?;
94
95            // Create streaming BLTE reader
96            let mut stream = blte::create_streaming_reader(file, None)
97                .map_err(|e| CascError::DecompressionError(e.to_string()))?;
98
99            let mut result = Vec::new();
100            stream
101                .read_to_end(&mut result)
102                .map_err(|e| CascError::DecompressionError(e.to_string()))?;
103            Ok(result)
104        } else {
105            debug!("Loose file {} is uncompressed", ekey);
106            // Seek back to beginning and read entire file
107            file.seek(std::io::SeekFrom::Start(0))?;
108            let mut data = Vec::new();
109            file.read_to_end(&mut data)?;
110            Ok(data)
111        }
112    }
113
114    /// Write a loose file
115    pub fn write(&mut self, ekey: &EKey, data: &[u8], compress: bool) -> Result<()> {
116        let filename = format!("{ekey}");
117        let path = self.base_path.join(&filename);
118
119        debug!("Writing loose file {} to {:?}", ekey, path);
120
121        // Optionally compress
122        let output = if compress {
123            blte::compress_data_single(data.to_vec(), blte::CompressionMode::ZLib, None)?
124        } else {
125            data.to_vec()
126        };
127
128        std::fs::write(&path, output)?;
129        self.files.insert(*ekey, path);
130
131        Ok(())
132    }
133
134    /// Remove a loose file
135    pub fn remove(&mut self, ekey: &EKey) -> Result<()> {
136        if let Some(path) = self.files.remove(ekey) {
137            debug!("Removing loose file {} at {:?}", ekey, path);
138            std::fs::remove_file(path)?;
139        }
140        Ok(())
141    }
142
143    /// Check if a loose file exists
144    pub fn contains(&self, ekey: &EKey) -> bool {
145        self.files.contains_key(ekey)
146    }
147
148    /// Get the number of loose files
149    pub fn len(&self) -> usize {
150        self.files.len()
151    }
152
153    /// Check if empty
154    pub fn is_empty(&self) -> bool {
155        self.files.is_empty()
156    }
157
158    /// Iterate over all loose files
159    pub fn iter(&self) -> impl Iterator<Item = (&EKey, &PathBuf)> {
160        self.files.iter()
161    }
162
163    /// Get total size of all loose files
164    pub fn total_size(&self) -> Result<u64> {
165        let mut total = 0u64;
166        for path in self.files.values() {
167            total += std::fs::metadata(path)?.len();
168        }
169        Ok(total)
170    }
171}