1use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9use std::sync::RwLock;
10
11use super::runtime_policy::{FileMetadata, FileSystemProvider, PathEntry};
12
13#[derive(Debug, Clone)]
19struct VfsEntry {
20 content: Vec<u8>,
21 is_dir: bool,
22}
23
24pub struct VirtualFilesystem {
29 files: RwLock<HashMap<PathBuf, VfsEntry>>,
30 read_only: RwLock<HashSet<PathBuf>>,
31 total_written: RwLock<usize>,
32 max_size: usize,
33}
34
35impl VirtualFilesystem {
36 pub fn new(max_size: usize) -> Self {
40 Self {
41 files: RwLock::new(HashMap::new()),
42 read_only: RwLock::new(HashSet::new()),
43 total_written: RwLock::new(0),
44 max_size,
45 }
46 }
47
48 pub fn seed_file(&self, path: impl Into<PathBuf>, content: Vec<u8>) {
51 let path = path.into();
52 self.ensure_parents(&path);
54 let mut files = self.files.write().unwrap();
55 files.insert(
56 path.clone(),
57 VfsEntry {
58 content,
59 is_dir: false,
60 },
61 );
62 self.read_only.write().unwrap().insert(path);
63 }
64
65 pub fn seed_dir(&self, path: impl Into<PathBuf>) {
67 let path = path.into();
68 self.ensure_parents(&path);
69 let mut files = self.files.write().unwrap();
70 files.insert(
71 path.clone(),
72 VfsEntry {
73 content: Vec::new(),
74 is_dir: true,
75 },
76 );
77 self.read_only.write().unwrap().insert(path);
78 }
79
80 pub fn extract_written_files(&self) -> HashMap<PathBuf, Vec<u8>> {
84 let files = self.files.read().unwrap();
85 let ro = self.read_only.read().unwrap();
86 files
87 .iter()
88 .filter(|(p, e)| !e.is_dir && !ro.contains(*p))
89 .map(|(p, e)| (p.clone(), e.content.clone()))
90 .collect()
91 }
92
93 pub fn total_bytes_written(&self) -> usize {
95 *self.total_written.read().unwrap()
96 }
97
98 fn ensure_parents(&self, path: &Path) {
100 let mut files = self.files.write().unwrap();
101 for ancestor in path.ancestors().skip(1) {
102 if ancestor == Path::new("") || ancestor == Path::new("/") {
103 continue;
105 }
106 files
107 .entry(ancestor.to_path_buf())
108 .or_insert_with(|| VfsEntry {
109 content: Vec::new(),
110 is_dir: true,
111 });
112 }
113 }
114
115 fn check_size_limit(&self, additional: usize) -> std::io::Result<()> {
117 if self.max_size == 0 {
118 return Ok(());
119 }
120 let current = *self.total_written.read().unwrap();
121 if current + additional > self.max_size {
122 return Err(std::io::Error::new(
123 std::io::ErrorKind::Other,
124 format!(
125 "VFS size limit exceeded: {} + {} > {}",
126 current, additional, self.max_size
127 ),
128 ));
129 }
130 Ok(())
131 }
132}
133
134impl FileSystemProvider for VirtualFilesystem {
135 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
136 let files = self.files.read().unwrap();
137 match files.get(path) {
138 Some(entry) if !entry.is_dir => Ok(entry.content.clone()),
139 Some(_) => Err(std::io::Error::new(
140 std::io::ErrorKind::Other,
141 format!("{} is a directory", path.display()),
142 )),
143 None => Err(std::io::Error::new(
144 std::io::ErrorKind::NotFound,
145 format!("{} not found in VFS", path.display()),
146 )),
147 }
148 }
149
150 fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
151 if self.read_only.read().unwrap().contains(path) {
152 return Err(std::io::Error::new(
153 std::io::ErrorKind::PermissionDenied,
154 format!("{} is read-only (seed file)", path.display()),
155 ));
156 }
157 self.check_size_limit(data.len())?;
158 self.ensure_parents(path);
159 let mut files = self.files.write().unwrap();
160
161 let old_size = files
163 .get(path)
164 .filter(|e| !e.is_dir)
165 .map(|e| e.content.len())
166 .unwrap_or(0);
167
168 files.insert(
169 path.to_path_buf(),
170 VfsEntry {
171 content: data.to_vec(),
172 is_dir: false,
173 },
174 );
175
176 let mut total = self.total_written.write().unwrap();
177 *total = total.saturating_sub(old_size) + data.len();
178 Ok(())
179 }
180
181 fn append(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
182 if self.read_only.read().unwrap().contains(path) {
183 return Err(std::io::Error::new(
184 std::io::ErrorKind::PermissionDenied,
185 format!("{} is read-only (seed file)", path.display()),
186 ));
187 }
188 self.check_size_limit(data.len())?;
189 self.ensure_parents(path);
190 let mut files = self.files.write().unwrap();
191 let entry = files.entry(path.to_path_buf()).or_insert_with(|| VfsEntry {
192 content: Vec::new(),
193 is_dir: false,
194 });
195 if entry.is_dir {
196 return Err(std::io::Error::new(
197 std::io::ErrorKind::Other,
198 format!("{} is a directory", path.display()),
199 ));
200 }
201 entry.content.extend_from_slice(data);
202 *self.total_written.write().unwrap() += data.len();
203 Ok(())
204 }
205
206 fn exists(&self, path: &Path) -> bool {
207 if path == Path::new("/") || path == Path::new("") {
209 return true;
210 }
211 self.files.read().unwrap().contains_key(path)
212 }
213
214 fn remove(&self, path: &Path) -> std::io::Result<()> {
215 if self.read_only.read().unwrap().contains(path) {
216 return Err(std::io::Error::new(
217 std::io::ErrorKind::PermissionDenied,
218 format!("{} is read-only (seed file)", path.display()),
219 ));
220 }
221 let mut files = self.files.write().unwrap();
222 match files.remove(path) {
223 Some(entry) if !entry.is_dir => {
224 let mut total = self.total_written.write().unwrap();
226 *total = total.saturating_sub(entry.content.len());
227 Ok(())
228 }
229 Some(entry) => {
230 files.insert(path.to_path_buf(), entry);
232 Err(std::io::Error::new(
233 std::io::ErrorKind::Other,
234 format!("{} is a directory", path.display()),
235 ))
236 }
237 None => Err(std::io::Error::new(
238 std::io::ErrorKind::NotFound,
239 format!("{} not found in VFS", path.display()),
240 )),
241 }
242 }
243
244 fn list_dir(&self, path: &Path) -> std::io::Result<Vec<PathEntry>> {
245 let files = self.files.read().unwrap();
246 let is_root = path == Path::new("/") || path == Path::new("");
248 if !is_root {
249 match files.get(path) {
250 Some(e) if e.is_dir => {}
251 Some(_) => {
252 return Err(std::io::Error::new(
253 std::io::ErrorKind::Other,
254 format!("{} is not a directory", path.display()),
255 ));
256 }
257 None => {
258 return Err(std::io::Error::new(
259 std::io::ErrorKind::NotFound,
260 format!("{} not found in VFS", path.display()),
261 ));
262 }
263 }
264 }
265
266 let mut entries = Vec::new();
267 for (p, e) in files.iter() {
268 if let Some(parent) = p.parent() {
269 if parent == path && p != path {
270 entries.push(PathEntry {
271 path: p.clone(),
272 is_dir: e.is_dir,
273 });
274 }
275 }
276 }
277 entries.sort_by(|a, b| a.path.cmp(&b.path));
278 Ok(entries)
279 }
280
281 fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
282 let files = self.files.read().unwrap();
283 match files.get(path) {
284 Some(entry) => Ok(FileMetadata {
285 size: entry.content.len() as u64,
286 is_dir: entry.is_dir,
287 is_file: !entry.is_dir,
288 readonly: self.read_only.read().unwrap().contains(path),
289 }),
290 None => Err(std::io::Error::new(
291 std::io::ErrorKind::NotFound,
292 format!("{} not found in VFS", path.display()),
293 )),
294 }
295 }
296
297 fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
298 let mut files = self.files.write().unwrap();
299 for ancestor in path.ancestors() {
300 if ancestor == Path::new("") || ancestor == Path::new("/") {
301 continue;
302 }
303 files
304 .entry(ancestor.to_path_buf())
305 .or_insert_with(|| VfsEntry {
306 content: Vec::new(),
307 is_dir: true,
308 });
309 }
310 files.entry(path.to_path_buf()).or_insert_with(|| VfsEntry {
312 content: Vec::new(),
313 is_dir: true,
314 });
315 Ok(())
316 }
317}
318
319#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn vfs() -> VirtualFilesystem {
328 VirtualFilesystem::new(0) }
330
331 #[test]
334 fn write_then_read() {
335 let fs = vfs();
336 fs.write(Path::new("/hello.txt"), b"world").unwrap();
337 let data = fs.read(Path::new("/hello.txt")).unwrap();
338 assert_eq!(data, b"world");
339 }
340
341 #[test]
342 fn read_nonexistent() {
343 let fs = vfs();
344 let err = fs.read(Path::new("/nope")).unwrap_err();
345 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
346 }
347
348 #[test]
349 fn overwrite_file() {
350 let fs = vfs();
351 fs.write(Path::new("/f"), b"old").unwrap();
352 fs.write(Path::new("/f"), b"new").unwrap();
353 assert_eq!(fs.read(Path::new("/f")).unwrap(), b"new");
354 }
355
356 #[test]
359 fn append_creates_and_extends() {
360 let fs = vfs();
361 fs.append(Path::new("/log.txt"), b"line1\n").unwrap();
362 fs.append(Path::new("/log.txt"), b"line2\n").unwrap();
363 assert_eq!(fs.read(Path::new("/log.txt")).unwrap(), b"line1\nline2\n");
364 }
365
366 #[test]
369 fn seed_file_is_readable() {
370 let fs = vfs();
371 fs.seed_file(PathBuf::from("/config.toml"), b"key = true".to_vec());
372 assert_eq!(fs.read(Path::new("/config.toml")).unwrap(), b"key = true");
373 }
374
375 #[test]
376 fn seed_file_is_not_writable() {
377 let fs = vfs();
378 fs.seed_file(PathBuf::from("/config.toml"), b"data".to_vec());
379 let err = fs.write(Path::new("/config.toml"), b"hacked").unwrap_err();
380 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
381 }
382
383 #[test]
384 fn seed_file_is_not_appendable() {
385 let fs = vfs();
386 fs.seed_file(PathBuf::from("/config.toml"), b"data".to_vec());
387 let err = fs.append(Path::new("/config.toml"), b"extra").unwrap_err();
388 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
389 }
390
391 #[test]
392 fn seed_file_is_not_removable() {
393 let fs = vfs();
394 fs.seed_file(PathBuf::from("/config.toml"), b"data".to_vec());
395 let err = fs.remove(Path::new("/config.toml")).unwrap_err();
396 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
397 }
398
399 #[test]
400 fn seed_file_not_in_extract() {
401 let fs = vfs();
402 fs.seed_file(PathBuf::from("/seed.txt"), b"seed".to_vec());
403 fs.write(Path::new("/output.txt"), b"output").unwrap();
404 let written = fs.extract_written_files();
405 assert!(written.contains_key(Path::new("/output.txt")));
406 assert!(!written.contains_key(Path::new("/seed.txt")));
407 }
408
409 #[test]
412 fn create_dir_and_list() {
413 let fs = vfs();
414 fs.create_dir_all(Path::new("/data/subdir")).unwrap();
415 fs.write(Path::new("/data/subdir/file.txt"), b"hi").unwrap();
416 let entries = fs.list_dir(Path::new("/data/subdir")).unwrap();
417 assert_eq!(entries.len(), 1);
418 assert_eq!(entries[0].path, PathBuf::from("/data/subdir/file.txt"));
419 assert!(!entries[0].is_dir);
420 }
421
422 #[test]
423 fn list_root() {
424 let fs = vfs();
425 fs.write(Path::new("/a.txt"), b"a").unwrap();
426 fs.create_dir_all(Path::new("/dir")).unwrap();
427 let entries = fs.list_dir(Path::new("/")).unwrap();
428 assert!(entries.len() >= 2);
429 }
430
431 #[test]
432 fn read_directory_fails() {
433 let fs = vfs();
434 fs.create_dir_all(Path::new("/mydir")).unwrap();
435 let err = fs.read(Path::new("/mydir")).unwrap_err();
436 assert_eq!(err.kind(), std::io::ErrorKind::Other);
437 }
438
439 #[test]
442 fn size_limit_enforced() {
443 let fs = VirtualFilesystem::new(10);
444 fs.write(Path::new("/a"), b"12345").unwrap();
445 assert_eq!(fs.total_bytes_written(), 5);
446 let err = fs.write(Path::new("/b"), b"123456").unwrap_err();
448 assert_eq!(err.kind(), std::io::ErrorKind::Other);
449 }
450
451 #[test]
452 fn overwrite_reclaims_space() {
453 let fs = VirtualFilesystem::new(20);
454 fs.write(Path::new("/f"), b"1234567890").unwrap(); assert_eq!(fs.total_bytes_written(), 10);
456 fs.write(Path::new("/f"), b"ab").unwrap(); assert_eq!(fs.total_bytes_written(), 2);
458 }
459
460 #[test]
463 fn exists_for_files_and_dirs() {
464 let fs = vfs();
465 assert!(!fs.exists(Path::new("/x")));
466 fs.write(Path::new("/x"), b"data").unwrap();
467 assert!(fs.exists(Path::new("/x")));
468 fs.create_dir_all(Path::new("/d")).unwrap();
469 assert!(fs.exists(Path::new("/d")));
470 }
471
472 #[test]
473 fn root_always_exists() {
474 let fs = vfs();
475 assert!(fs.exists(Path::new("/")));
476 }
477
478 #[test]
481 fn remove_file() {
482 let fs = vfs();
483 fs.write(Path::new("/f"), b"data").unwrap();
484 fs.remove(Path::new("/f")).unwrap();
485 assert!(!fs.exists(Path::new("/f")));
486 }
487
488 #[test]
489 fn remove_nonexistent_errors() {
490 let fs = vfs();
491 let err = fs.remove(Path::new("/nope")).unwrap_err();
492 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
493 }
494
495 #[test]
496 fn remove_reclaims_bytes() {
497 let fs = VirtualFilesystem::new(100);
498 fs.write(Path::new("/f"), b"12345").unwrap();
499 assert_eq!(fs.total_bytes_written(), 5);
500 fs.remove(Path::new("/f")).unwrap();
501 assert_eq!(fs.total_bytes_written(), 0);
502 }
503
504 #[test]
507 fn metadata_file() {
508 let fs = vfs();
509 fs.write(Path::new("/f"), b"hello").unwrap();
510 let m = fs.metadata(Path::new("/f")).unwrap();
511 assert!(m.is_file);
512 assert!(!m.is_dir);
513 assert_eq!(m.size, 5);
514 assert!(!m.readonly);
515 }
516
517 #[test]
518 fn metadata_seed_file_is_readonly() {
519 let fs = vfs();
520 fs.seed_file(PathBuf::from("/s"), b"data".to_vec());
521 let m = fs.metadata(Path::new("/s")).unwrap();
522 assert!(m.readonly);
523 }
524
525 #[test]
526 fn metadata_dir() {
527 let fs = vfs();
528 fs.create_dir_all(Path::new("/d")).unwrap();
529 let m = fs.metadata(Path::new("/d")).unwrap();
530 assert!(m.is_dir);
531 assert!(!m.is_file);
532 }
533
534 #[test]
537 fn writing_deep_path_creates_parents() {
538 let fs = vfs();
539 fs.write(Path::new("/a/b/c/file.txt"), b"deep").unwrap();
540 assert!(fs.exists(Path::new("/a")));
541 assert!(fs.exists(Path::new("/a/b")));
542 assert!(fs.exists(Path::new("/a/b/c")));
543 let m = fs.metadata(Path::new("/a/b")).unwrap();
544 assert!(m.is_dir);
545 }
546}