1use std::collections::BTreeMap;
8
9use vortex_core::{DetRng, FsError, FsFaultConfig, FsFaultRule, FsOp};
10
11use crate::traits::{FileMetadata, FileType, VortexFs, VortexFsError, VortexFsResult};
12
13#[derive(Debug, Clone)]
15enum Entry {
16 File(Vec<u8>),
17 Dir,
18}
19
20pub struct SimFs {
35 rng: DetRng,
36 entries: BTreeMap<String, Entry>,
37 fault_config: FsFaultConfig,
38 total_bytes_written: u64,
40}
41
42impl SimFs {
43 pub fn new(seed: u64) -> Self {
45 let mut entries = BTreeMap::new();
46 entries.insert("/".to_string(), Entry::Dir);
47 Self {
48 rng: DetRng::new(seed),
49 entries,
50 fault_config: FsFaultConfig::default(),
51 total_bytes_written: 0,
52 }
53 }
54
55 pub fn with_faults(seed: u64, config: FsFaultConfig) -> Self {
57 let mut fs = Self::new(seed);
58 fs.fault_config = config;
59 fs
60 }
61
62 pub fn set_fault_config(&mut self, config: FsFaultConfig) {
64 self.fault_config = config;
65 }
66
67 pub fn total_bytes_written(&self) -> u64 {
69 self.total_bytes_written
70 }
71
72 pub fn entry_count(&self) -> usize {
74 self.entries.len()
75 }
76
77 fn check_fault(&mut self, path: &str, op: FsOp, pending_bytes: u64) -> Option<VortexFsError> {
82 let effective_bytes = self.total_bytes_written + pending_bytes;
83 for rule in &self.fault_config.rules {
84 if rule.error == FsError::TornWrite
87 || rule.error == FsError::Corrupt
88 || (rule.error == FsError::DelayedFsync && op != FsOp::Fsync)
89 {
90 continue;
91 }
92 if !matches_glob(&rule.path, path) {
93 continue;
94 }
95 if rule.op != FsOp::Any && rule.op != op {
96 continue;
97 }
98 if rule.after_bytes > 0 && effective_bytes < rule.after_bytes {
99 continue;
100 }
101 if self.rng.chance(rule.probability) {
102 return Some(fault_rule_to_error(rule, path));
103 }
104 }
105 None
106 }
107
108 fn check_torn_write(&mut self, path: &str, data: &[u8]) -> Option<(usize, VortexFsError)> {
110 for rule in &self.fault_config.rules {
111 if rule.error != FsError::TornWrite {
112 continue;
113 }
114 if !matches_glob(&rule.path, path) {
115 continue;
116 }
117 if rule.op != FsOp::Any && rule.op != FsOp::Write {
118 continue;
119 }
120 if rule.after_bytes > 0 && self.total_bytes_written < rule.after_bytes {
121 continue;
122 }
123 if self.rng.chance(rule.probability) {
124 let partial = if data.is_empty() {
125 0
126 } else {
127 self.rng.next_u64_below(data.len() as u64) as usize
128 };
129 return Some((
130 partial,
131 VortexFsError::TornWrite {
132 path: path.to_string(),
133 bytes_written: partial as u64,
134 intended: data.len() as u64,
135 },
136 ));
137 }
138 }
139 None
140 }
141
142 fn maybe_corrupt(&mut self, path: &str, data: &mut [u8]) {
144 for rule in &self.fault_config.rules {
145 if rule.error != FsError::Corrupt {
146 continue;
147 }
148 if !matches_glob(&rule.path, path) {
149 continue;
150 }
151 if self.rng.chance(rule.probability) && !data.is_empty() {
152 let pos = self.rng.next_u64_below(data.len() as u64) as usize;
153 data[pos] ^= (self.rng.next_u64_below(255) + 1) as u8;
154 }
155 }
156 }
157}
158
159impl VortexFs for SimFs {
160 fn read_file(&self, path: &str) -> VortexFsResult<Vec<u8>> {
161 let norm = normalise(path);
162 match self.entries.get(&norm) {
166 Some(Entry::File(data)) => Ok(data.clone()),
167 Some(Entry::Dir) => Err(VortexFsError::IsADirectory(norm)),
168 None => Err(VortexFsError::NotFound(norm)),
169 }
170 }
171
172 fn write_file(&mut self, path: &str, data: &[u8]) -> VortexFsResult<()> {
173 let norm = normalise(path);
174
175 if let Some(err) = self.check_fault(&norm, FsOp::Write, data.len() as u64) {
177 return Err(err);
178 }
179
180 if let Some((partial_bytes, err)) = self.check_torn_write(&norm, data) {
182 self.ensure_parent_dirs(&norm)?;
184 self.entries
185 .insert(norm, Entry::File(data[..partial_bytes].to_vec()));
186 self.total_bytes_written += partial_bytes as u64;
187 return Err(err);
188 }
189
190 self.ensure_parent_dirs(&norm)?;
191 let mut file_data = data.to_vec();
192 self.maybe_corrupt(&norm, &mut file_data);
193 self.total_bytes_written += file_data.len() as u64;
194 self.entries.insert(norm, Entry::File(file_data));
195 Ok(())
196 }
197
198 fn append_file(&mut self, path: &str, data: &[u8]) -> VortexFsResult<()> {
199 let norm = normalise(path);
200 if let Some(err) = self.check_fault(&norm, FsOp::Write, data.len() as u64) {
201 return Err(err);
202 }
203 self.ensure_parent_dirs(&norm)?;
204 let entry = self
205 .entries
206 .entry(norm.clone())
207 .or_insert_with(|| Entry::File(Vec::new()));
208 match entry {
209 Entry::File(existing) => {
210 existing.extend_from_slice(data);
211 self.total_bytes_written += data.len() as u64;
212 Ok(())
213 }
214 Entry::Dir => Err(VortexFsError::IsADirectory(norm)),
215 }
216 }
217
218 fn remove_file(&mut self, path: &str) -> VortexFsResult<()> {
219 let norm = normalise(path);
220 if let Some(err) = self.check_fault(&norm, FsOp::Delete, 0) {
221 return Err(err);
222 }
223 match self.entries.get(&norm) {
224 Some(Entry::File(_)) => {
225 self.entries.remove(&norm);
226 Ok(())
227 }
228 Some(Entry::Dir) => Err(VortexFsError::IsADirectory(norm)),
229 None => Err(VortexFsError::NotFound(norm)),
230 }
231 }
232
233 fn rename(&mut self, from: &str, to: &str) -> VortexFsResult<()> {
234 let from_norm = normalise(from);
235 let to_norm = normalise(to);
236 if let Some(err) = self.check_fault(&from_norm, FsOp::Rename, 0) {
237 return Err(err);
238 }
239 match self.entries.remove(&from_norm) {
240 Some(entry) => {
241 self.entries.insert(to_norm, entry);
242 Ok(())
243 }
244 None => Err(VortexFsError::NotFound(from_norm)),
245 }
246 }
247
248 fn create_dir_all(&mut self, path: &str) -> VortexFsResult<()> {
249 let norm = normalise(path);
250 let parts: Vec<&str> = norm.split('/').filter(|s| !s.is_empty()).collect();
252 let mut current = String::from("/");
253 for part in parts {
254 if !current.ends_with('/') {
255 current.push('/');
256 }
257 current.push_str(part);
258 let norm_current = normalise(¤t);
259 if let Some(entry) = self.entries.get(&norm_current) {
260 match entry {
261 Entry::Dir => continue,
262 Entry::File(_) => {
263 return Err(VortexFsError::NotADirectory(norm_current));
264 }
265 }
266 }
267 self.entries.insert(norm_current, Entry::Dir);
268 }
269 Ok(())
270 }
271
272 fn remove_dir(&mut self, path: &str) -> VortexFsResult<()> {
273 let norm = normalise(path);
274 match self.entries.get(&norm) {
275 Some(Entry::Dir) => {
276 let prefix = if norm.ends_with('/') {
278 norm.clone()
279 } else {
280 format!("{norm}/")
281 };
282 let has_children = self
283 .entries
284 .keys()
285 .any(|k| k != &norm && k.starts_with(&prefix));
286 if has_children {
287 return Err(VortexFsError::NotEmpty(norm));
288 }
289 self.entries.remove(&norm);
290 Ok(())
291 }
292 Some(Entry::File(_)) => Err(VortexFsError::NotADirectory(norm)),
293 None => Err(VortexFsError::NotFound(norm)),
294 }
295 }
296
297 fn read_dir(&self, path: &str) -> VortexFsResult<Vec<String>> {
298 let norm = normalise(path);
299 match self.entries.get(&norm) {
300 Some(Entry::Dir) => {}
301 Some(Entry::File(_)) => return Err(VortexFsError::NotADirectory(norm.clone())),
302 None => return Err(VortexFsError::NotFound(norm.clone())),
303 }
304 let prefix = if norm == "/" {
305 "/".to_string()
306 } else {
307 format!("{norm}/")
308 };
309 let mut names: Vec<String> = Vec::new();
310 for key in self.entries.keys() {
311 if key == &norm {
312 continue;
313 }
314 if let Some(rest) = key.strip_prefix(&prefix) {
315 if !rest.contains('/') && !rest.is_empty() {
317 names.push(rest.to_string());
318 }
319 }
320 }
321 names.sort();
322 Ok(names)
323 }
324
325 fn metadata(&self, path: &str) -> VortexFsResult<FileMetadata> {
326 let norm = normalise(path);
327 match self.entries.get(&norm) {
328 Some(Entry::File(data)) => Ok(FileMetadata {
329 file_type: FileType::File,
330 size: data.len() as u64,
331 }),
332 Some(Entry::Dir) => Ok(FileMetadata {
333 file_type: FileType::Directory,
334 size: 0,
335 }),
336 None => Err(VortexFsError::NotFound(norm)),
337 }
338 }
339
340 fn exists(&self, path: &str) -> bool {
341 let norm = normalise(path);
342 self.entries.contains_key(&norm)
343 }
344
345 fn fsync(&mut self, path: &str) -> VortexFsResult<()> {
346 let norm = normalise(path);
347 if let Some(err) = self.check_fault(&norm, FsOp::Fsync, 0) {
348 return Err(err);
349 }
350 if !self.entries.contains_key(&norm) {
351 return Err(VortexFsError::NotFound(norm));
352 }
353 Ok(()) }
355}
356
357impl SimFs {
358 fn ensure_parent_dirs(&mut self, path: &str) -> VortexFsResult<()> {
359 if let Some(parent) = parent_path(path)
360 && !self.entries.contains_key(&parent)
361 {
362 self.create_dir_all(&parent)?;
363 }
364 Ok(())
365 }
366}
367
368fn normalise(path: &str) -> String {
372 let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
373 if parts.is_empty() {
374 return "/".to_string();
375 }
376 format!("/{}", parts.join("/"))
377}
378
379fn parent_path(path: &str) -> Option<String> {
381 let norm = normalise(path);
382 if norm == "/" {
383 return None;
384 }
385 match norm.rfind('/') {
386 Some(0) => Some("/".to_string()),
387 Some(pos) => Some(norm[..pos].to_string()),
388 None => None,
389 }
390}
391
392fn matches_glob(pattern: &str, path: &str) -> bool {
394 if pattern == "*" || pattern == "**" {
396 return true;
397 }
398
399 if let Some(suffix) = pattern.strip_prefix('*')
401 && !suffix.contains('*')
402 {
403 return path.ends_with(suffix)
404 || path
405 .rsplit('/')
406 .next()
407 .is_some_and(|name| name.ends_with(suffix));
408 }
409
410 if let Some(prefix) = pattern.strip_suffix("/**") {
412 return path.starts_with(prefix);
413 }
414 if let Some(prefix) = pattern.strip_suffix("/*") {
415 let rest = path.strip_prefix(prefix).unwrap_or("");
416 return rest.starts_with('/') && rest.matches('/').count() <= 1;
417 }
418
419 pattern == path
421}
422
423fn fault_rule_to_error(rule: &FsFaultRule, path: &str) -> VortexFsError {
425 match rule.error {
426 FsError::Enospc => VortexFsError::DiskFull(path.to_string()),
427 FsError::Eio => VortexFsError::IoError(path.to_string()),
428 FsError::Eacces => VortexFsError::PermissionDenied(path.to_string()),
429 FsError::TornWrite => VortexFsError::IoError(format!("torn write on {path}")),
430 FsError::Corrupt => VortexFsError::Corrupted(path.to_string()),
431 FsError::DelayedFsync => VortexFsError::IoError(format!("delayed fsync on {path}")),
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use vortex_core::{FsError, FsFaultConfig, FsFaultRule, FsOp};
439
440 #[test]
441 fn test_basic_read_write() {
442 let mut fs = SimFs::new(42);
443 fs.write_file("/hello.txt", b"world").unwrap();
444 assert_eq!(fs.read_file("/hello.txt").unwrap(), b"world");
445 }
446
447 #[test]
448 fn test_auto_create_parent_dirs() {
449 let mut fs = SimFs::new(42);
450 fs.write_file("/a/b/c/file.txt", b"data").unwrap();
451 assert!(fs.exists("/a"));
452 assert!(fs.exists("/a/b"));
453 assert!(fs.exists("/a/b/c"));
454 assert!(fs.exists("/a/b/c/file.txt"));
455 }
456
457 #[test]
458 fn test_append() {
459 let mut fs = SimFs::new(42);
460 fs.write_file("/log.txt", b"line1\n").unwrap();
461 fs.append_file("/log.txt", b"line2\n").unwrap();
462 assert_eq!(fs.read_file("/log.txt").unwrap(), b"line1\nline2\n");
463 }
464
465 #[test]
466 fn test_remove_file() {
467 let mut fs = SimFs::new(42);
468 fs.write_file("/tmp.txt", b"temp").unwrap();
469 assert!(fs.exists("/tmp.txt"));
470 fs.remove_file("/tmp.txt").unwrap();
471 assert!(!fs.exists("/tmp.txt"));
472 }
473
474 #[test]
475 fn test_remove_nonexistent_file() {
476 let mut fs = SimFs::new(42);
477 assert!(matches!(
478 fs.remove_file("/nope.txt"),
479 Err(VortexFsError::NotFound(_))
480 ));
481 }
482
483 #[test]
484 fn test_read_nonexistent() {
485 let fs = SimFs::new(42);
486 assert!(matches!(
487 fs.read_file("/nope.txt"),
488 Err(VortexFsError::NotFound(_))
489 ));
490 }
491
492 #[test]
493 fn test_rename() {
494 let mut fs = SimFs::new(42);
495 fs.write_file("/old.txt", b"data").unwrap();
496 fs.rename("/old.txt", "/new.txt").unwrap();
497 assert!(!fs.exists("/old.txt"));
498 assert_eq!(fs.read_file("/new.txt").unwrap(), b"data");
499 }
500
501 #[test]
502 fn test_read_dir() {
503 let mut fs = SimFs::new(42);
504 fs.write_file("/dir/a.txt", b"a").unwrap();
505 fs.write_file("/dir/b.txt", b"b").unwrap();
506 fs.create_dir_all("/dir/sub").unwrap();
507 let entries = fs.read_dir("/dir").unwrap();
508 assert_eq!(entries, vec!["a.txt", "b.txt", "sub"]);
509 }
510
511 #[test]
512 fn test_remove_nonempty_dir() {
513 let mut fs = SimFs::new(42);
514 fs.write_file("/dir/file.txt", b"data").unwrap();
515 assert!(matches!(
516 fs.remove_dir("/dir"),
517 Err(VortexFsError::NotEmpty(_))
518 ));
519 }
520
521 #[test]
522 fn test_metadata() {
523 let mut fs = SimFs::new(42);
524 fs.write_file("/data.bin", b"12345").unwrap();
525 let meta = fs.metadata("/data.bin").unwrap();
526 assert_eq!(meta.file_type, FileType::File);
527 assert_eq!(meta.size, 5);
528
529 fs.create_dir_all("/mydir").unwrap();
530 let meta = fs.metadata("/mydir").unwrap();
531 assert_eq!(meta.file_type, FileType::Directory);
532 }
533
534 #[test]
535 fn test_fault_enospc() {
536 let config = FsFaultConfig {
537 rules: vec![FsFaultRule {
538 path: "*.wal".into(),
539 op: FsOp::Write,
540 error: FsError::Enospc,
541 after_bytes: 0,
542 probability: 1.0, }],
544 };
545 let mut fs = SimFs::with_faults(42, config);
546 let result = fs.write_file("/data.wal", b"WAL data");
547 assert!(matches!(result, Err(VortexFsError::DiskFull(_))));
548 }
549
550 #[test]
551 fn test_fault_eio_on_read_path() {
552 let config = FsFaultConfig {
553 rules: vec![FsFaultRule {
554 path: "/data/*".into(),
555 op: FsOp::Write,
556 error: FsError::Eio,
557 after_bytes: 0,
558 probability: 1.0,
559 }],
560 };
561 let mut fs = SimFs::with_faults(42, config);
562 fs.write_file("/logs/app.log", b"OK").unwrap();
564 let result = fs.write_file("/data/table.sst", b"data");
566 assert!(matches!(result, Err(VortexFsError::IoError(_))));
567 }
568
569 #[test]
570 fn test_fault_after_bytes_threshold() {
571 let config = FsFaultConfig {
572 rules: vec![FsFaultRule {
573 path: "*".into(),
574 op: FsOp::Write,
575 error: FsError::Enospc,
576 after_bytes: 100, probability: 1.0,
578 }],
579 };
580 let mut fs = SimFs::with_faults(42, config);
581 fs.write_file("/a.txt", &[0u8; 50]).unwrap();
583 let result = fs.write_file("/b.txt", &[0u8; 60]);
585 assert!(matches!(result, Err(VortexFsError::DiskFull(_))));
586 }
587
588 #[test]
589 fn test_fault_torn_write() {
590 let config = FsFaultConfig {
591 rules: vec![FsFaultRule {
592 path: "*.wal".into(),
593 op: FsOp::Write,
594 error: FsError::TornWrite,
595 after_bytes: 0,
596 probability: 1.0,
597 }],
598 };
599 let mut fs = SimFs::with_faults(42, config);
600 let data = vec![0xAB; 100];
601 let result = fs.write_file("/log.wal", &data);
602 assert!(matches!(result, Err(VortexFsError::TornWrite { .. })));
603 assert!(fs.exists("/log.wal"));
605 let written = fs.read_file("/log.wal").unwrap();
606 assert!(written.len() < data.len());
607 }
608
609 #[test]
610 fn test_fault_corruption() {
611 let config = FsFaultConfig {
612 rules: vec![FsFaultRule {
613 path: "*".into(),
614 op: FsOp::Write,
615 error: FsError::Corrupt,
616 after_bytes: 0,
617 probability: 1.0,
618 }],
619 };
620 let original = vec![0x42; 100];
621 let mut fs = SimFs::with_faults(42, config);
622 fs.write_file("/data.bin", &original).unwrap();
623 let read_back = fs.read_file("/data.bin").unwrap();
624 assert_ne!(original, read_back, "Data should be corrupted");
626 }
627
628 #[test]
629 fn test_probability_based_faults() {
630 let config = FsFaultConfig {
631 rules: vec![FsFaultRule {
632 path: "*".into(),
633 op: FsOp::Write,
634 error: FsError::Eio,
635 after_bytes: 0,
636 probability: 0.5,
637 }],
638 };
639 let mut fs = SimFs::with_faults(42, config);
640 let mut successes = 0;
641 let mut failures = 0;
642 for i in 0..100 {
643 match fs.write_file(&format!("/file_{i}.txt"), b"data") {
644 Ok(()) => successes += 1,
645 Err(_) => failures += 1,
646 }
647 }
648 assert!(successes > 0, "Expected some successes");
650 assert!(failures > 0, "Expected some failures");
651 }
652
653 #[test]
656 fn test_glob_star_suffix() {
657 assert!(matches_glob("*.wal", "/data/log.wal"));
658 assert!(matches_glob("*.wal", "log.wal"));
659 assert!(!matches_glob("*.wal", "/data/log.txt"));
660 }
661
662 #[test]
663 fn test_glob_prefix_doublestar() {
664 assert!(matches_glob("/data/**", "/data/a/b/c.txt"));
665 assert!(matches_glob("/data/**", "/data/file.txt"));
666 assert!(!matches_glob("/data/**", "/logs/file.txt"));
667 }
668
669 #[test]
670 fn test_glob_exact() {
671 assert!(matches_glob("/specific/file.txt", "/specific/file.txt"));
672 assert!(!matches_glob("/specific/file.txt", "/other/file.txt"));
673 }
674
675 #[test]
676 fn test_normalise() {
677 assert_eq!(normalise("/a/b/c"), "/a/b/c");
678 assert_eq!(normalise("a/b/c"), "/a/b/c");
679 assert_eq!(normalise("/a//b///c/"), "/a/b/c");
680 assert_eq!(normalise("/"), "/");
681 assert_eq!(normalise(""), "/");
682 }
683}