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