1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use handlebars::Handlebars;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11#[derive(Debug, Clone)]
13pub struct VirtualFileSystem {
14 #[allow(dead_code)]
15 root: PathBuf,
16 files: Arc<RwLock<HashMap<PathBuf, VirtualFile>>>,
17 fixtures: Arc<RwLock<HashMap<PathBuf, FileFixture>>>,
18 directories: Arc<RwLock<HashSet<PathBuf>>>,
19}
20
21impl VirtualFileSystem {
22 pub fn new(root: PathBuf) -> Self {
23 let mut dirs = HashSet::new();
24 dirs.insert(PathBuf::from("/"));
25 Self {
26 root,
27 files: Arc::new(RwLock::new(HashMap::new())),
28 fixtures: Arc::new(RwLock::new(HashMap::new())),
29 directories: Arc::new(RwLock::new(dirs)),
30 }
31 }
32
33 pub fn add_file(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
34 let mut files = self.files.blocking_write();
35 files.insert(path, file);
36 Ok(())
37 }
38
39 pub fn get_file(&self, path: &Path) -> Option<VirtualFile> {
40 let files = self.files.blocking_read();
41 if let Some(file) = files.get(path) {
42 return Some(file.clone());
43 }
44
45 let fixtures = self.fixtures.blocking_read();
47 if let Some(fixture) = fixtures.get(path) {
48 return Some(fixture.clone().to_virtual_file());
49 }
50
51 None
52 }
53
54 pub fn remove_file(&self, path: &Path) -> Result<()> {
55 let mut files = self.files.blocking_write();
56 files.remove(path);
57 Ok(())
58 }
59
60 pub fn list_files(&self, path: &Path) -> Vec<VirtualFile> {
61 let files = self.files.blocking_read();
62 files
63 .iter()
64 .filter(|(file_path, _)| file_path.starts_with(path))
65 .map(|(_, file)| file.clone())
66 .collect()
67 }
68
69 pub fn clear(&self) -> Result<()> {
70 let mut files = self.files.blocking_write();
71 files.clear();
72 Ok(())
73 }
74
75 pub fn add_fixture(&self, fixture: FileFixture) -> Result<()> {
76 let mut fixtures = self.fixtures.blocking_write();
77 fixtures.insert(fixture.path.clone(), fixture);
78 Ok(())
79 }
80
81 pub fn load_fixtures(&self, fixtures: Vec<FileFixture>) -> Result<()> {
82 for fixture in fixtures {
83 self.add_fixture(fixture)?;
84 }
85 Ok(())
86 }
87
88 pub fn create_directory(&self, path: PathBuf) -> Result<()> {
90 let mut dirs = self.directories.blocking_write();
91 dirs.insert(path);
92 Ok(())
93 }
94
95 pub fn remove_directory(&self, path: &Path) -> Result<()> {
97 if self.is_directory_empty(path) {
98 let mut dirs = self.directories.blocking_write();
99 dirs.remove(path);
100 Ok(())
101 } else {
102 Err(anyhow::anyhow!("Directory is not empty"))
103 }
104 }
105
106 pub fn directory_exists(&self, path: &Path) -> bool {
108 let dirs = self.directories.blocking_read();
109 dirs.contains(path)
110 }
111
112 pub fn is_directory_empty(&self, path: &Path) -> bool {
114 let files = self.files.blocking_read();
115 let has_files =
116 files.keys().any(|file_path| file_path != path && file_path.starts_with(path));
117 if has_files {
118 return false;
119 }
120
121 let dirs = self.directories.blocking_read();
122 let has_subdirs =
123 dirs.iter().any(|dir_path| dir_path != path && dir_path.starts_with(path));
124 !has_subdirs
125 }
126
127 pub async fn add_file_async(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
129 let mut files = self.files.write().await;
130 files.insert(path, file);
131 Ok(())
132 }
133
134 pub async fn get_file_async(&self, path: &Path) -> Option<VirtualFile> {
136 let files = self.files.read().await;
137 if let Some(file) = files.get(path) {
138 return Some(file.clone());
139 }
140
141 let fixtures = self.fixtures.read().await;
143 if let Some(fixture) = fixtures.get(path) {
144 return Some(fixture.clone().to_virtual_file());
145 }
146
147 None
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct VirtualFile {
154 pub path: PathBuf,
155 pub content: FileContent,
156 pub metadata: FileMetadata,
157 pub created_at: DateTime<Utc>,
158 pub modified_at: DateTime<Utc>,
159}
160
161impl VirtualFile {
162 pub fn new(path: PathBuf, content: FileContent, metadata: FileMetadata) -> Self {
163 let now = Utc::now();
164 Self {
165 path,
166 content,
167 metadata,
168 created_at: now,
169 modified_at: now,
170 }
171 }
172
173 pub fn render_content(&self) -> Result<Vec<u8>> {
174 match &self.content {
175 FileContent::Static(data) => Ok(data.clone()),
176 FileContent::Template(template) => {
177 let handlebars = Handlebars::new();
179 let context = create_template_context();
180 let rendered = handlebars.render_template(template, &context)?;
181 Ok(rendered.into_bytes())
182 }
183 FileContent::Generated { size, pattern } => match pattern {
184 GenerationPattern::Random => Ok((0..*size).map(|_| rand::random::<u8>()).collect()),
185 GenerationPattern::Zeros => Ok(vec![0; *size]),
186 GenerationPattern::Ones => Ok(vec![1; *size]),
187 GenerationPattern::Incremental => Ok((0..*size).map(|i| (i % 256) as u8).collect()),
188 },
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub enum FileContent {
196 Static(Vec<u8>),
197 Template(String),
198 Generated {
199 size: usize,
200 pattern: GenerationPattern,
201 },
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct FileMetadata {
207 pub permissions: String,
208 pub owner: String,
209 pub group: String,
210 pub size: u64,
211}
212
213impl Default for FileMetadata {
214 fn default() -> Self {
215 Self {
216 permissions: "644".to_string(),
217 owner: "mockforge".to_string(),
218 group: "users".to_string(),
219 size: 0,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub enum GenerationPattern {
227 Random,
228 Zeros,
229 Ones,
230 Incremental,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct FileFixture {
236 pub path: PathBuf,
237 pub content: FileContent,
238 pub metadata: FileMetadata,
239}
240
241impl FileFixture {
242 pub fn to_virtual_file(self) -> VirtualFile {
243 VirtualFile::new(self.path, self.content, self.metadata)
244 }
245}
246
247fn create_template_context() -> Value {
249 let mut context = serde_json::Map::new();
250
251 let now = Utc::now();
253 context.insert("now".to_string(), Value::String(now.to_rfc3339()));
254 context.insert("timestamp".to_string(), Value::Number(now.timestamp().into()));
255 context.insert("date".to_string(), Value::String(now.format("%Y-%m-%d").to_string()));
256 context.insert("time".to_string(), Value::String(now.format("%H:%M:%S").to_string()));
257
258 context.insert("random_int".to_string(), Value::Number(rand::random::<i64>().into()));
260 context.insert(
261 "random_float".to_string(),
262 Value::String(format!("{:.6}", rand::random::<f64>())),
263 );
264
265 context.insert("uuid".to_string(), Value::String(uuid::Uuid::new_v4().to_string()));
267
268 let mut faker = serde_json::Map::new();
270 faker.insert("name".to_string(), Value::String("John Doe".to_string()));
271 faker.insert("email".to_string(), Value::String("john.doe@example.com".to_string()));
272 faker.insert("age".to_string(), Value::Number(30.into()));
273 context.insert("faker".to_string(), Value::Object(faker));
274
275 Value::Object(context)
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_file_metadata_default() {
284 let metadata = FileMetadata::default();
285 assert_eq!(metadata.permissions, "644");
286 assert_eq!(metadata.owner, "mockforge");
287 assert_eq!(metadata.group, "users");
288 assert_eq!(metadata.size, 0);
289 }
290
291 #[test]
292 fn test_file_metadata_clone() {
293 let metadata = FileMetadata {
294 permissions: "755".to_string(),
295 owner: "root".to_string(),
296 group: "root".to_string(),
297 size: 1024,
298 };
299
300 let cloned = metadata.clone();
301 assert_eq!(metadata.permissions, cloned.permissions);
302 assert_eq!(metadata.owner, cloned.owner);
303 assert_eq!(metadata.size, cloned.size);
304 }
305
306 #[test]
307 fn test_generation_pattern_clone() {
308 let pattern = GenerationPattern::Random;
309 let _cloned = pattern.clone();
310 }
312
313 #[test]
314 fn test_generation_pattern_debug() {
315 let pattern = GenerationPattern::Zeros;
316 let debug = format!("{:?}", pattern);
317 assert!(debug.contains("Zeros"));
318 }
319
320 #[test]
321 fn test_file_content_static() {
322 let content = FileContent::Static(b"hello world".to_vec());
323 let debug = format!("{:?}", content);
324 assert!(debug.contains("Static"));
325 }
326
327 #[test]
328 fn test_file_content_template() {
329 let content = FileContent::Template("Hello {{name}}".to_string());
330 let debug = format!("{:?}", content);
331 assert!(debug.contains("Template"));
332 }
333
334 #[test]
335 fn test_file_content_generated() {
336 let content = FileContent::Generated {
337 size: 100,
338 pattern: GenerationPattern::Ones,
339 };
340 let debug = format!("{:?}", content);
341 assert!(debug.contains("Generated"));
342 }
343
344 #[test]
345 fn test_virtual_file_new() {
346 let file = VirtualFile::new(
347 PathBuf::from("/test.txt"),
348 FileContent::Static(b"content".to_vec()),
349 FileMetadata::default(),
350 );
351
352 assert_eq!(file.path, PathBuf::from("/test.txt"));
353 }
354
355 #[test]
356 fn test_virtual_file_render_static() {
357 let file = VirtualFile::new(
358 PathBuf::from("/test.txt"),
359 FileContent::Static(b"hello".to_vec()),
360 FileMetadata::default(),
361 );
362
363 let content = file.render_content().unwrap();
364 assert_eq!(content, b"hello".to_vec());
365 }
366
367 #[test]
368 fn test_virtual_file_render_generated_zeros() {
369 let file = VirtualFile::new(
370 PathBuf::from("/zeros.bin"),
371 FileContent::Generated {
372 size: 10,
373 pattern: GenerationPattern::Zeros,
374 },
375 FileMetadata::default(),
376 );
377
378 let content = file.render_content().unwrap();
379 assert_eq!(content.len(), 10);
380 assert!(content.iter().all(|&b| b == 0));
381 }
382
383 #[test]
384 fn test_virtual_file_render_generated_ones() {
385 let file = VirtualFile::new(
386 PathBuf::from("/ones.bin"),
387 FileContent::Generated {
388 size: 10,
389 pattern: GenerationPattern::Ones,
390 },
391 FileMetadata::default(),
392 );
393
394 let content = file.render_content().unwrap();
395 assert_eq!(content.len(), 10);
396 assert!(content.iter().all(|&b| b == 1));
397 }
398
399 #[test]
400 fn test_virtual_file_render_generated_incremental() {
401 let file = VirtualFile::new(
402 PathBuf::from("/inc.bin"),
403 FileContent::Generated {
404 size: 256,
405 pattern: GenerationPattern::Incremental,
406 },
407 FileMetadata::default(),
408 );
409
410 let content = file.render_content().unwrap();
411 assert_eq!(content.len(), 256);
412 for (i, &b) in content.iter().enumerate() {
413 assert_eq!(b, i as u8);
414 }
415 }
416
417 #[test]
418 fn test_virtual_file_render_generated_random() {
419 let file = VirtualFile::new(
420 PathBuf::from("/random.bin"),
421 FileContent::Generated {
422 size: 100,
423 pattern: GenerationPattern::Random,
424 },
425 FileMetadata::default(),
426 );
427
428 let content = file.render_content().unwrap();
429 assert_eq!(content.len(), 100);
430 }
431
432 #[test]
433 fn test_virtual_file_clone() {
434 let file = VirtualFile::new(
435 PathBuf::from("/test.txt"),
436 FileContent::Static(b"test".to_vec()),
437 FileMetadata::default(),
438 );
439
440 let cloned = file.clone();
441 assert_eq!(file.path, cloned.path);
442 }
443
444 #[test]
445 fn test_virtual_file_debug() {
446 let file = VirtualFile::new(
447 PathBuf::from("/test.txt"),
448 FileContent::Static(vec![]),
449 FileMetadata::default(),
450 );
451
452 let debug = format!("{:?}", file);
453 assert!(debug.contains("VirtualFile"));
454 }
455
456 #[test]
457 fn test_file_fixture_to_virtual_file() {
458 let fixture = FileFixture {
459 path: PathBuf::from("/fixture.txt"),
460 content: FileContent::Static(b"fixture content".to_vec()),
461 metadata: FileMetadata::default(),
462 };
463
464 let file = fixture.to_virtual_file();
465 assert_eq!(file.path, PathBuf::from("/fixture.txt"));
466 }
467
468 #[test]
469 fn test_file_fixture_clone() {
470 let fixture = FileFixture {
471 path: PathBuf::from("/test.txt"),
472 content: FileContent::Static(vec![]),
473 metadata: FileMetadata::default(),
474 };
475
476 let cloned = fixture.clone();
477 assert_eq!(fixture.path, cloned.path);
478 }
479
480 #[test]
481 fn test_vfs_new() {
482 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
483 let files = vfs.list_files(&PathBuf::from("/"));
484 assert!(files.is_empty());
485 }
486
487 #[test]
488 fn test_vfs_add_file() {
489 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
490 let file = VirtualFile::new(
491 PathBuf::from("/test.txt"),
492 FileContent::Static(b"hello".to_vec()),
493 FileMetadata::default(),
494 );
495
496 vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
497
498 let retrieved = vfs.get_file(&PathBuf::from("/test.txt"));
499 assert!(retrieved.is_some());
500 }
501
502 #[test]
503 fn test_vfs_get_file_not_found() {
504 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
505 let retrieved = vfs.get_file(&PathBuf::from("/nonexistent.txt"));
506 assert!(retrieved.is_none());
507 }
508
509 #[test]
510 fn test_vfs_remove_file() {
511 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
512 let file = VirtualFile::new(
513 PathBuf::from("/test.txt"),
514 FileContent::Static(vec![]),
515 FileMetadata::default(),
516 );
517
518 vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
519 vfs.remove_file(&PathBuf::from("/test.txt")).unwrap();
520
521 let retrieved = vfs.get_file(&PathBuf::from("/test.txt"));
522 assert!(retrieved.is_none());
523 }
524
525 #[test]
526 fn test_vfs_list_files() {
527 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
528
529 let file1 = VirtualFile::new(
530 PathBuf::from("/dir/file1.txt"),
531 FileContent::Static(vec![]),
532 FileMetadata::default(),
533 );
534 let file2 = VirtualFile::new(
535 PathBuf::from("/dir/file2.txt"),
536 FileContent::Static(vec![]),
537 FileMetadata::default(),
538 );
539
540 vfs.add_file(PathBuf::from("/dir/file1.txt"), file1).unwrap();
541 vfs.add_file(PathBuf::from("/dir/file2.txt"), file2).unwrap();
542
543 let files = vfs.list_files(&PathBuf::from("/dir"));
544 assert_eq!(files.len(), 2);
545 }
546
547 #[test]
548 fn test_vfs_clear() {
549 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
550
551 let file = VirtualFile::new(
552 PathBuf::from("/test.txt"),
553 FileContent::Static(vec![]),
554 FileMetadata::default(),
555 );
556 vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
557
558 vfs.clear().unwrap();
559
560 let files = vfs.list_files(&PathBuf::from("/"));
561 assert!(files.is_empty());
562 }
563
564 #[test]
565 fn test_vfs_add_fixture() {
566 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
567 let fixture = FileFixture {
568 path: PathBuf::from("/fixture.txt"),
569 content: FileContent::Static(b"fixture".to_vec()),
570 metadata: FileMetadata::default(),
571 };
572
573 vfs.add_fixture(fixture).unwrap();
574
575 let file = vfs.get_file(&PathBuf::from("/fixture.txt"));
576 assert!(file.is_some());
577 }
578
579 #[test]
580 fn test_vfs_load_fixtures() {
581 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
582 let fixtures = vec![
583 FileFixture {
584 path: PathBuf::from("/f1.txt"),
585 content: FileContent::Static(vec![]),
586 metadata: FileMetadata::default(),
587 },
588 FileFixture {
589 path: PathBuf::from("/f2.txt"),
590 content: FileContent::Static(vec![]),
591 metadata: FileMetadata::default(),
592 },
593 ];
594
595 vfs.load_fixtures(fixtures).unwrap();
596
597 assert!(vfs.get_file(&PathBuf::from("/f1.txt")).is_some());
598 assert!(vfs.get_file(&PathBuf::from("/f2.txt")).is_some());
599 }
600
601 #[test]
602 fn test_vfs_clone() {
603 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
604 let _cloned = vfs.clone();
605 }
607
608 #[test]
609 fn test_vfs_debug() {
610 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
611 let debug = format!("{:?}", vfs);
612 assert!(debug.contains("VirtualFileSystem"));
613 }
614
615 #[test]
616 fn test_template_context_has_expected_fields() {
617 let context = create_template_context();
618 assert!(context.get("now").is_some());
619 assert!(context.get("timestamp").is_some());
620 assert!(context.get("date").is_some());
621 assert!(context.get("uuid").is_some());
622 assert!(context.get("faker").is_some());
623 }
624
625 #[test]
626 fn test_virtual_file_render_template() {
627 let file = VirtualFile::new(
628 PathBuf::from("/template.txt"),
629 FileContent::Template("Hello {{faker.name}}!".to_string()),
630 FileMetadata::default(),
631 );
632
633 let content = file.render_content().unwrap();
634 let text = String::from_utf8(content).unwrap();
635 assert!(text.contains("Hello"));
636 assert!(text.contains("John Doe")); }
638
639 #[test]
640 fn test_virtual_file_render_template_with_timestamp() {
641 let file = VirtualFile::new(
642 PathBuf::from("/timestamp.txt"),
643 FileContent::Template("Current timestamp: {{timestamp}}".to_string()),
644 FileMetadata::default(),
645 );
646
647 let content = file.render_content().unwrap();
648 let text = String::from_utf8(content).unwrap();
649 assert!(text.contains("Current timestamp:"));
650 }
651
652 #[test]
653 fn test_virtual_file_render_template_with_uuid() {
654 let file = VirtualFile::new(
655 PathBuf::from("/uuid.txt"),
656 FileContent::Template("ID: {{uuid}}".to_string()),
657 FileMetadata::default(),
658 );
659
660 let content = file.render_content().unwrap();
661 let text = String::from_utf8(content).unwrap();
662 assert!(text.starts_with("ID: "));
663 let uuid_part = text.trim_start_matches("ID: ");
665 assert!(!uuid_part.is_empty());
666 }
667
668 #[test]
669 fn test_vfs_get_file_from_fixtures() {
670 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
671 let fixture = FileFixture {
672 path: PathBuf::from("/fixture.txt"),
673 content: FileContent::Static(b"fixture content".to_vec()),
674 metadata: FileMetadata::default(),
675 };
676
677 vfs.add_fixture(fixture).unwrap();
678
679 let file = vfs.get_file(&PathBuf::from("/fixture.txt"));
680 assert!(file.is_some());
681 let content = file.unwrap().render_content().unwrap();
682 assert_eq!(content, b"fixture content");
683 }
684
685 #[test]
686 fn test_vfs_files_priority_over_fixtures() {
687 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
688
689 let fixture = FileFixture {
691 path: PathBuf::from("/test.txt"),
692 content: FileContent::Static(b"fixture".to_vec()),
693 metadata: FileMetadata::default(),
694 };
695 vfs.add_fixture(fixture).unwrap();
696
697 let file = VirtualFile::new(
699 PathBuf::from("/test.txt"),
700 FileContent::Static(b"file".to_vec()),
701 FileMetadata::default(),
702 );
703 vfs.add_file(PathBuf::from("/test.txt"), file).unwrap();
704
705 let retrieved = vfs.get_file(&PathBuf::from("/test.txt")).unwrap();
707 let content = retrieved.render_content().unwrap();
708 assert_eq!(content, b"file");
709 }
710
711 #[test]
712 fn test_vfs_list_files_empty_path() {
713 let vfs = VirtualFileSystem::new(PathBuf::from("/"));
714
715 let file1 = VirtualFile::new(
716 PathBuf::from("/file1.txt"),
717 FileContent::Static(vec![]),
718 FileMetadata::default(),
719 );
720 let file2 = VirtualFile::new(
721 PathBuf::from("/subdir/file2.txt"),
722 FileContent::Static(vec![]),
723 FileMetadata::default(),
724 );
725
726 vfs.add_file(PathBuf::from("/file1.txt"), file1).unwrap();
727 vfs.add_file(PathBuf::from("/subdir/file2.txt"), file2).unwrap();
728
729 let files = vfs.list_files(&PathBuf::from("/"));
731 assert_eq!(files.len(), 2);
732 }
733
734 #[test]
735 fn test_virtual_file_serialization() {
736 let file = VirtualFile::new(
737 PathBuf::from("/test.txt"),
738 FileContent::Static(b"test".to_vec()),
739 FileMetadata::default(),
740 );
741
742 let serialized = serde_json::to_string(&file);
744 assert!(serialized.is_ok());
745
746 let deserialized: Result<VirtualFile, _> = serde_json::from_str(&serialized.unwrap());
748 assert!(deserialized.is_ok());
749 }
750
751 #[test]
752 fn test_file_metadata_serialization() {
753 let metadata = FileMetadata {
754 permissions: "755".to_string(),
755 owner: "root".to_string(),
756 group: "admin".to_string(),
757 size: 2048,
758 };
759
760 let serialized = serde_json::to_string(&metadata);
761 assert!(serialized.is_ok());
762
763 let deserialized: Result<FileMetadata, _> = serde_json::from_str(&serialized.unwrap());
764 assert!(deserialized.is_ok());
765 }
766
767 #[test]
768 fn test_file_content_serialization() {
769 let content = FileContent::Static(b"test content".to_vec());
770 let serialized = serde_json::to_string(&content);
771 assert!(serialized.is_ok());
772 }
773
774 #[test]
775 fn test_generation_pattern_serialization() {
776 let pattern = GenerationPattern::Random;
777 let serialized = serde_json::to_string(&pattern);
778 assert!(serialized.is_ok());
779 }
780}