Skip to main content

cdx_core/archive/
storage.rs

1//! Archive storage trait for abstraction over storage backends.
2//!
3//! This module provides the [`ArchiveStorage`] trait which enables testing
4//! archive operations without actual ZIP file I/O, and supports future
5//! alternative backends.
6
7use std::collections::HashMap;
8
9use crate::Result;
10
11/// Trait for archive storage backends.
12///
13/// This trait abstracts over the underlying storage mechanism (ZIP files,
14/// in-memory storage, etc.) to enable unit testing and support alternative
15/// backends.
16pub trait ArchiveStorage {
17    /// Read a file from the archive.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error if the file doesn't exist or cannot be read.
22    fn read_file(&mut self, path: &str) -> Result<Vec<u8>>;
23
24    /// Write a file to the archive.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the file cannot be written.
29    fn write_file(&mut self, path: &str, data: &[u8]) -> Result<()>;
30
31    /// Check if a file exists in the archive.
32    fn file_exists(&self, path: &str) -> bool;
33
34    /// Get all file names in the archive.
35    fn file_names(&self) -> Vec<String>;
36}
37
38/// In-memory storage for testing purposes.
39///
40/// This implementation stores all files in memory, enabling unit tests
41/// to run without filesystem access or ZIP operations.
42///
43/// # Example
44///
45/// ```
46/// use cdx_core::archive::MemoryStorage;
47/// use cdx_core::archive::ArchiveStorage;
48///
49/// let mut storage = MemoryStorage::new();
50/// storage.write_file("test.txt", b"Hello, world!").unwrap();
51///
52/// assert!(storage.file_exists("test.txt"));
53/// assert_eq!(storage.read_file("test.txt").unwrap(), b"Hello, world!");
54/// ```
55#[derive(Debug, Clone, Default)]
56pub struct MemoryStorage {
57    files: HashMap<String, Vec<u8>>,
58}
59
60impl MemoryStorage {
61    /// Create a new empty in-memory storage.
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            files: HashMap::new(),
66        }
67    }
68
69    /// Create a storage with pre-populated files.
70    #[must_use]
71    pub fn with_files(files: HashMap<String, Vec<u8>>) -> Self {
72        Self { files }
73    }
74
75    /// Get the number of files stored.
76    #[must_use]
77    pub fn len(&self) -> usize {
78        self.files.len()
79    }
80
81    /// Check if the storage is empty.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.files.is_empty()
85    }
86
87    /// Remove a file from storage.
88    ///
89    /// Returns the file contents if it existed.
90    pub fn remove(&mut self, path: &str) -> Option<Vec<u8>> {
91        self.files.remove(path)
92    }
93
94    /// Clear all files from storage.
95    pub fn clear(&mut self) {
96        self.files.clear();
97    }
98}
99
100impl ArchiveStorage for MemoryStorage {
101    fn read_file(&mut self, path: &str) -> Result<Vec<u8>> {
102        self.files
103            .get(path)
104            .cloned()
105            .ok_or_else(|| crate::Error::MissingFile {
106                path: path.to_string(),
107            })
108    }
109
110    fn write_file(&mut self, path: &str, data: &[u8]) -> Result<()> {
111        self.files.insert(path.to_string(), data.to_vec());
112        Ok(())
113    }
114
115    fn file_exists(&self, path: &str) -> bool {
116        self.files.contains_key(path)
117    }
118
119    fn file_names(&self) -> Vec<String> {
120        self.files.keys().cloned().collect()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_memory_storage_new() {
130        let storage = MemoryStorage::new();
131        assert!(storage.is_empty());
132        assert_eq!(storage.len(), 0);
133    }
134
135    #[test]
136    fn test_memory_storage_write_read() {
137        let mut storage = MemoryStorage::new();
138
139        storage.write_file("test.txt", b"Hello").unwrap();
140        assert_eq!(storage.read_file("test.txt").unwrap(), b"Hello");
141    }
142
143    #[test]
144    fn test_memory_storage_file_exists() {
145        let mut storage = MemoryStorage::new();
146
147        assert!(!storage.file_exists("test.txt"));
148        storage.write_file("test.txt", b"data").unwrap();
149        assert!(storage.file_exists("test.txt"));
150    }
151
152    #[test]
153    fn test_memory_storage_file_names() {
154        let mut storage = MemoryStorage::new();
155
156        storage.write_file("a.txt", b"a").unwrap();
157        storage.write_file("b.txt", b"b").unwrap();
158
159        let names = storage.file_names();
160        assert_eq!(names.len(), 2);
161        assert!(names.contains(&"a.txt".to_string()));
162        assert!(names.contains(&"b.txt".to_string()));
163    }
164
165    #[test]
166    fn test_memory_storage_read_missing() {
167        let mut storage = MemoryStorage::new();
168        let result = storage.read_file("nonexistent");
169        assert!(result.is_err());
170    }
171
172    #[test]
173    fn test_memory_storage_overwrite() {
174        let mut storage = MemoryStorage::new();
175
176        storage.write_file("test.txt", b"first").unwrap();
177        storage.write_file("test.txt", b"second").unwrap();
178
179        assert_eq!(storage.read_file("test.txt").unwrap(), b"second");
180        assert_eq!(storage.len(), 1);
181    }
182
183    #[test]
184    fn test_memory_storage_remove() {
185        let mut storage = MemoryStorage::new();
186
187        storage.write_file("test.txt", b"data").unwrap();
188        let removed = storage.remove("test.txt");
189
190        assert_eq!(removed, Some(b"data".to_vec()));
191        assert!(!storage.file_exists("test.txt"));
192    }
193
194    #[test]
195    fn test_memory_storage_remove_nonexistent() {
196        let mut storage = MemoryStorage::new();
197        assert!(storage.remove("nonexistent").is_none());
198    }
199
200    #[test]
201    fn test_memory_storage_clear() {
202        let mut storage = MemoryStorage::new();
203
204        storage.write_file("a.txt", b"a").unwrap();
205        storage.write_file("b.txt", b"b").unwrap();
206        storage.clear();
207
208        assert!(storage.is_empty());
209    }
210
211    #[test]
212    fn test_memory_storage_with_files() {
213        let mut files = HashMap::new();
214        files.insert("existing.txt".to_string(), b"content".to_vec());
215
216        let mut storage = MemoryStorage::with_files(files);
217
218        assert!(storage.file_exists("existing.txt"));
219        assert_eq!(storage.read_file("existing.txt").unwrap(), b"content");
220    }
221
222    #[test]
223    fn test_memory_storage_binary_data() {
224        let mut storage = MemoryStorage::new();
225        let binary = vec![0x00, 0xFF, 0x7F, 0x80, 0x01];
226
227        storage.write_file("binary.dat", &binary).unwrap();
228        assert_eq!(storage.read_file("binary.dat").unwrap(), binary);
229    }
230
231    #[test]
232    fn test_memory_storage_empty_file() {
233        let mut storage = MemoryStorage::new();
234
235        storage.write_file("empty.txt", b"").unwrap();
236
237        assert!(storage.file_exists("empty.txt"));
238        assert!(storage.read_file("empty.txt").unwrap().is_empty());
239    }
240
241    #[test]
242    fn test_memory_storage_nested_paths() {
243        let mut storage = MemoryStorage::new();
244
245        storage
246            .write_file("path/to/nested/file.txt", b"nested")
247            .unwrap();
248
249        assert!(storage.file_exists("path/to/nested/file.txt"));
250        assert_eq!(
251            storage.read_file("path/to/nested/file.txt").unwrap(),
252            b"nested"
253        );
254    }
255}