1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::error::Error;
4use std::fmt;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7pub const S_IFREG: u32 = 0o100000;
8pub const S_IFDIR: u32 = 0o040000;
9pub const S_IFLNK: u32 = 0o120000;
10const MEMORY_FILESYSTEM_DEVICE_ID: u64 = 1;
11
12const DEFAULT_UID: u32 = 1000;
13const DEFAULT_GID: u32 = 1000;
14const DIRECTORY_SIZE: u64 = 4096;
15pub const MAX_PATH_LENGTH: usize = 4096;
16const MAX_SYMLINK_DEPTH: usize = 40;
17
18pub type VfsResult<T> = Result<T, VfsError>;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct VfsError {
22 code: &'static str,
23 message: String,
24}
25
26impl VfsError {
27 pub fn new(code: &'static str, message: impl Into<String>) -> Self {
28 Self {
29 code,
30 message: message.into(),
31 }
32 }
33
34 pub fn io(message: impl Into<String>) -> Self {
35 Self::new("EIO", message)
36 }
37
38 pub fn unsupported(message: impl Into<String>) -> Self {
39 Self::new("ENOSYS", message)
40 }
41
42 pub fn code(&self) -> &'static str {
43 self.code
44 }
45
46 pub fn message(&self) -> &str {
47 &self.message
48 }
49
50 fn not_found(op: &'static str, path: &str) -> Self {
51 Self::new(
52 "ENOENT",
53 format!("no such file or directory, {op} '{path}'"),
54 )
55 }
56
57 fn already_exists(op: &'static str, path: &str) -> Self {
58 Self::new("EEXIST", format!("file already exists, {op} '{path}'"))
59 }
60
61 fn is_directory(op: &'static str, path: &str) -> Self {
62 Self::new(
63 "EISDIR",
64 format!("illegal operation on a directory, {op} '{path}'"),
65 )
66 }
67
68 fn not_directory(op: &'static str, path: &str) -> Self {
69 Self::new("ENOTDIR", format!("not a directory, {op} '{path}'"))
70 }
71
72 fn path_too_long(path: &str) -> Self {
73 Self::new("ENAMETOOLONG", format!("file name too long: {path}"))
74 }
75
76 fn not_empty(path: &str) -> Self {
77 Self::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
78 }
79
80 pub(crate) fn permission_denied(op: &'static str, path: &str) -> Self {
81 Self::new("EPERM", format!("operation not permitted, {op} '{path}'"))
82 }
83
84 pub fn access_denied(op: &'static str, path: &str, reason: Option<&str>) -> Self {
85 let message = match reason {
86 Some(reason) => format!("permission denied, {op} '{path}': {reason}"),
87 None => format!("permission denied, {op} '{path}'"),
88 };
89
90 Self::new("EACCES", message)
91 }
92
93 fn symlink_loop(path: &str) -> Self {
94 Self::new(
95 "ELOOP",
96 format!("too many levels of symbolic links, '{path}'"),
97 )
98 }
99
100 fn invalid_input(message: impl Into<String>) -> Self {
101 Self::new("EINVAL", message)
102 }
103
104 fn invalid_utf8(path: &str) -> Self {
105 Self::new("EINVAL", format!("file contains invalid UTF-8, '{path}'"))
106 }
107}
108
109impl fmt::Display for VfsError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "{}: {}", self.code, self.message)
112 }
113}
114
115impl Error for VfsError {}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum FileType {
119 File,
120 Directory,
121 SymbolicLink,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct VirtualDirEntry {
126 pub name: String,
127 pub is_directory: bool,
128 pub is_symbolic_link: bool,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct VirtualStat {
133 pub mode: u32,
134 pub size: u64,
135 pub blocks: u64,
136 pub dev: u64,
137 pub rdev: u64,
138 pub is_directory: bool,
139 pub is_symbolic_link: bool,
140 pub atime_ms: u64,
141 pub atime_nsec: u32,
142 pub mtime_ms: u64,
143 pub mtime_nsec: u32,
144 pub ctime_ms: u64,
145 pub ctime_nsec: u32,
146 pub birthtime_ms: u64,
147 pub ino: u64,
148 pub nlink: u64,
149 pub uid: u32,
150 pub gid: u32,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub struct VirtualTimeSpec {
155 pub sec: i64,
156 pub nsec: u32,
157}
158
159impl VirtualTimeSpec {
160 pub fn new(sec: i64, nsec: u32) -> VfsResult<Self> {
161 if nsec >= 1_000_000_000 {
162 return Err(VfsError::new(
163 "EINVAL",
164 format!("timespec nanoseconds out of range: {nsec}"),
165 ));
166 }
167 Ok(Self { sec, nsec })
168 }
169
170 pub fn from_millis(ms: u64) -> Self {
171 Self {
172 sec: (ms / 1_000) as i64,
173 nsec: ((ms % 1_000) * 1_000_000) as u32,
174 }
175 }
176
177 pub fn to_truncated_millis(self) -> VfsResult<u64> {
178 if self.sec < 0 {
179 return Err(VfsError::new(
180 "EINVAL",
181 format!(
182 "negative timestamps are not supported by this filesystem: {}",
183 self.sec
184 ),
185 ));
186 }
187 let seconds = u64::try_from(self.sec).map_err(|_| {
188 VfsError::new("EINVAL", format!("timestamp is out of range: {}", self.sec))
189 })?;
190 Ok(seconds.saturating_mul(1_000) + (self.nsec as u64 / 1_000_000))
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum VirtualUtimeSpec {
196 Set(VirtualTimeSpec),
197 Now,
198 Omit,
199}
200
201pub trait VirtualFileSystem {
202 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
203 fn read_text_file(&mut self, path: &str) -> VfsResult<String> {
204 String::from_utf8(self.read_file(path)?).map_err(|_| VfsError::invalid_utf8(path))
205 }
206 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
207 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
208 let entries = self.read_dir(path)?;
209 if entries.len() > max_entries {
210 return Err(VfsError::new(
211 "ENOMEM",
212 format!(
213 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
214 ),
215 ));
216 }
217 Ok(entries)
218 }
219 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
220 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()>;
221 fn write_file_with_mode(
222 &mut self,
223 path: &str,
224 content: impl Into<Vec<u8>>,
225 mode: Option<u32>,
226 ) -> VfsResult<()> {
227 let _ = mode;
228 self.write_file(path, content)
229 }
230 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
231 let content = content.into();
232 if self.exists(path) {
233 return Err(VfsError::already_exists("open", path));
234 }
235 self.write_file(path, content)
236 }
237 fn create_file_exclusive_with_mode(
238 &mut self,
239 path: &str,
240 content: impl Into<Vec<u8>>,
241 mode: Option<u32>,
242 ) -> VfsResult<()> {
243 let _ = mode;
244 self.create_file_exclusive(path, content)
245 }
246 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
247 let content = content.into();
248 let mut existing = self.read_file(path)?;
249 existing.extend_from_slice(&content);
250 let new_len = existing.len() as u64;
251 self.write_file(path, existing)?;
252 Ok(new_len)
253 }
254 fn create_dir(&mut self, path: &str) -> VfsResult<()>;
255 fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
256 let _ = mode;
257 self.create_dir(path)
258 }
259 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
260 fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
261 let _ = mode;
262 self.mkdir(path, recursive)
263 }
264 fn exists(&self, path: &str) -> bool;
265 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
266 fn remove_file(&mut self, path: &str) -> VfsResult<()>;
267 fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
268 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
269 fn realpath(&self, path: &str) -> VfsResult<String>;
270 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
271 fn read_link(&self, path: &str) -> VfsResult<String>;
272 fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
273 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
274 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
275 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
276 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
277 fn utimes_spec(
278 &mut self,
279 path: &str,
280 atime: VirtualUtimeSpec,
281 mtime: VirtualUtimeSpec,
282 follow_symlinks: bool,
283 ) -> VfsResult<()> {
284 if !follow_symlinks {
285 return Err(VfsError::unsupported(format!(
286 "lutimes is not supported for '{path}'"
287 )));
288 }
289 let existing = match (atime, mtime) {
290 (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
291 _ => None,
292 };
293 let now = now_ms();
294 let atime_ms = resolve_utime_millis(
295 atime,
296 now,
297 existing.as_ref().map(|stat| VirtualTimeSpec {
298 sec: (stat.atime_ms / 1_000) as i64,
299 nsec: stat.atime_nsec,
300 }),
301 )?;
302 let mtime_ms = resolve_utime_millis(
303 mtime,
304 now,
305 existing.as_ref().map(|stat| VirtualTimeSpec {
306 sec: (stat.mtime_ms / 1_000) as i64,
307 nsec: stat.mtime_nsec,
308 }),
309 )?;
310 self.utimes(path, atime_ms, mtime_ms)
311 }
312 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
313 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
314 fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
315 let content = content.into();
316 let mut existing = self.read_file(path)?;
317 let start = offset as usize;
318 if start > existing.len() {
319 existing.resize(start, 0);
320 }
321 let end = start.saturating_add(content.len());
322 if end > existing.len() {
323 existing.resize(end, 0);
324 }
325 existing[start..end].copy_from_slice(&content);
326 self.write_file(path, existing)
327 }
328}
329
330#[derive(Debug, Clone)]
331struct Metadata {
332 mode: u32,
333 uid: u32,
334 gid: u32,
335 nlink: u64,
336 ino: u64,
337 atime_ms: u64,
338 atime_nsec: u32,
339 mtime_ms: u64,
340 mtime_nsec: u32,
341 ctime_ms: u64,
342 ctime_nsec: u32,
343 birthtime_ms: u64,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub struct MemoryFileSystemSnapshotMetadata {
348 pub mode: u32,
349 pub uid: u32,
350 pub gid: u32,
351 pub nlink: u64,
352 pub ino: u64,
353 pub atime_ms: u64,
354 #[serde(default)]
355 pub atime_nsec: u32,
356 pub mtime_ms: u64,
357 #[serde(default)]
358 pub mtime_nsec: u32,
359 pub ctime_ms: u64,
360 #[serde(default)]
361 pub ctime_nsec: u32,
362 pub birthtime_ms: u64,
363}
364
365#[derive(Debug, Clone)]
366enum InodeKind {
367 File { data: Vec<u8> },
368 Directory,
369 SymbolicLink { target: String },
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub enum MemoryFileSystemSnapshotInodeKind {
374 File { data: Vec<u8> },
375 Directory,
376 SymbolicLink { target: String },
377}
378
379#[derive(Debug, Clone)]
380struct Inode {
381 metadata: Metadata,
382 kind: InodeKind,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct MemoryFileSystemSnapshotInode {
387 pub metadata: MemoryFileSystemSnapshotMetadata,
388 pub kind: MemoryFileSystemSnapshotInodeKind,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct MemoryFileSystemSnapshot {
393 pub path_index: BTreeMap<String, u64>,
394 pub inodes: BTreeMap<u64, MemoryFileSystemSnapshotInode>,
395 pub next_ino: u64,
396}
397
398#[derive(Debug)]
399pub struct MemoryFileSystem {
400 path_index: BTreeMap<String, u64>,
401 inodes: BTreeMap<u64, Inode>,
402 next_ino: u64,
403}
404
405impl MemoryFileSystem {
406 pub fn new() -> Self {
407 let mut filesystem = Self {
408 path_index: BTreeMap::new(),
409 inodes: BTreeMap::new(),
410 next_ino: 1,
411 };
412
413 let root_ino = filesystem.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
414 filesystem.path_index.insert(String::from("/"), root_ino);
415 filesystem
416 }
417
418 fn allocate_inode(&mut self, kind: InodeKind, mode: u32) -> u64 {
419 let ino = self.next_ino;
420 self.next_ino += 1;
421 let now = now_ms();
422 let nlink = if matches!(kind, InodeKind::Directory) {
423 2
424 } else {
425 1
426 };
427 self.inodes.insert(
428 ino,
429 Inode {
430 metadata: Metadata {
431 mode,
432 uid: DEFAULT_UID,
433 gid: DEFAULT_GID,
434 nlink,
435 ino,
436 atime_ms: now,
437 atime_nsec: 0,
438 mtime_ms: now,
439 mtime_nsec: 0,
440 ctime_ms: now,
441 ctime_nsec: 0,
442 birthtime_ms: now,
443 },
444 kind,
445 },
446 );
447 ino
448 }
449
450 pub fn symlink_with_metadata(
451 &mut self,
452 target: &str,
453 link_path: &str,
454 mode: u32,
455 uid: u32,
456 gid: u32,
457 ) -> VfsResult<()> {
458 let normalized = self.resolve_exact_path(link_path)?;
459 if self.path_index.contains_key(&normalized) {
460 return Err(VfsError::already_exists("symlink", link_path));
461 }
462
463 self.assert_directory_path(&dirname(&normalized), "symlink")?;
464 let ino = self.allocate_inode(
465 InodeKind::SymbolicLink {
466 target: String::from(target),
467 },
468 if mode & 0o170000 == 0 {
469 S_IFLNK | (mode & 0o7777)
470 } else {
471 mode
472 },
473 );
474 let inode = self
475 .inodes
476 .get_mut(&ino)
477 .expect("allocated inode should exist");
478 inode.metadata.uid = uid;
479 inode.metadata.gid = gid;
480 self.path_index.insert(normalized, ino);
481 Ok(())
482 }
483
484 fn resolve_path_with_options(
485 &self,
486 path: &str,
487 follow_final_symlink: bool,
488 depth: usize,
489 ) -> VfsResult<String> {
490 validate_path(path)?;
491 if depth > MAX_SYMLINK_DEPTH {
492 return Err(VfsError::symlink_loop(path));
493 }
494
495 let normalized = normalize_path(path);
496 if normalized == "/" {
497 return Ok(normalized);
498 }
499
500 let components: Vec<&str> = normalized
501 .split('/')
502 .filter(|part| !part.is_empty())
503 .collect();
504 let mut current = String::from("/");
505
506 for (index, component) in components.iter().enumerate() {
507 let candidate = if current == "/" {
508 format!("/{}", component)
509 } else {
510 format!("{current}/{}", component)
511 };
512 let is_final = index + 1 == components.len();
513 let should_follow = !is_final || follow_final_symlink;
514
515 if let Some(ino) = self.path_index.get(&candidate) {
516 let inode = self
517 .inodes
518 .get(ino)
519 .expect("path index should always point at a valid inode");
520
521 if should_follow {
522 if let InodeKind::SymbolicLink { target } = &inode.kind {
523 let target_path = if target.starts_with('/') {
524 target.clone()
525 } else {
526 normalize_path(&format!("{}/{}", dirname(&candidate), target))
527 };
528 let remainder = components[index + 1..].join("/");
529 let next_path = if remainder.is_empty() {
530 target_path
531 } else {
532 normalize_path(&format!("{target_path}/{remainder}"))
533 };
534 return self.resolve_path_with_options(
535 &next_path,
536 follow_final_symlink,
537 depth + 1,
538 );
539 }
540 }
541
542 if !is_final && !matches!(inode.kind, InodeKind::Directory) {
543 return Err(VfsError::not_directory("stat", &candidate));
544 }
545 }
546
547 current = candidate;
548 }
549
550 Ok(current)
551 }
552
553 fn resolve_path(&self, path: &str, depth: usize) -> VfsResult<String> {
554 self.resolve_path_with_options(path, true, depth)
555 }
556
557 fn resolve_exact_path(&self, path: &str) -> VfsResult<String> {
558 self.resolve_path_with_options(path, false, 0)
559 }
560
561 fn inode_id_for_existing_path(
562 &self,
563 path: &str,
564 op: &'static str,
565 follow_symlinks: bool,
566 ) -> VfsResult<u64> {
567 let normalized = normalize_path(path);
568 let resolved = if follow_symlinks {
569 self.resolve_path(&normalized, 0)?
570 } else {
571 self.resolve_exact_path(&normalized)?
572 };
573 self.path_index
574 .get(&resolved)
575 .copied()
576 .ok_or_else(|| VfsError::not_found(op, path))
577 }
578
579 fn inode_for_existing_path(
580 &self,
581 path: &str,
582 op: &'static str,
583 follow_symlinks: bool,
584 ) -> VfsResult<&Inode> {
585 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
586 Ok(self
587 .inodes
588 .get(&ino)
589 .expect("existing path should resolve to a live inode"))
590 }
591
592 fn inode_mut_for_existing_path(
593 &mut self,
594 path: &str,
595 op: &'static str,
596 follow_symlinks: bool,
597 ) -> VfsResult<&mut Inode> {
598 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
599 Ok(self
600 .inodes
601 .get_mut(&ino)
602 .expect("existing path should resolve to a live inode"))
603 }
604
605 fn assert_directory_path(&self, path: &str, op: &'static str) -> VfsResult<()> {
606 let inode = self.inode_for_existing_path(path, op, true)?;
607 if matches!(inode.kind, InodeKind::Directory) {
608 Ok(())
609 } else {
610 Err(VfsError::not_directory(op, path))
611 }
612 }
613
614 fn remove_exact_path(&mut self, path: &str) -> VfsResult<()> {
615 let normalized = self.resolve_exact_path(path)?;
616 let ino = self
617 .path_index
618 .get(&normalized)
619 .copied()
620 .ok_or_else(|| VfsError::not_found("unlink", path))?;
621 let inode = self
622 .inodes
623 .get(&ino)
624 .expect("existing path should resolve to a live inode");
625
626 if matches!(inode.kind, InodeKind::Directory) {
627 return Err(VfsError::is_directory("unlink", path));
628 }
629
630 self.inodes
631 .get_mut(&ino)
632 .expect("inode should exist when unlinking")
633 .metadata
634 .ctime_ms = now_ms();
635 self.path_index.remove(&normalized);
636 self.decrement_link_count(ino);
637 Ok(())
638 }
639
640 fn remove_existing_destination(&mut self, path: &str) -> VfsResult<()> {
641 let normalized = self.resolve_exact_path(path)?;
642 let Some(ino) = self.path_index.get(&normalized).copied() else {
643 return Ok(());
644 };
645
646 let inode = self
647 .inodes
648 .get(&ino)
649 .expect("existing path should resolve to a live inode");
650
651 if matches!(inode.kind, InodeKind::Directory) {
652 let prefix = format!("{normalized}/");
653 if self
654 .path_index
655 .keys()
656 .any(|candidate| candidate.starts_with(&prefix))
657 {
658 return Err(VfsError::not_empty(path));
659 }
660 }
661
662 self.inodes
663 .get_mut(&ino)
664 .expect("inode should exist when removing destination")
665 .metadata
666 .ctime_ms = now_ms();
667 self.path_index.remove(&normalized);
668 self.decrement_link_count(ino);
669 Ok(())
670 }
671
672 fn decrement_link_count(&mut self, ino: u64) {
673 let should_remove = {
674 let inode = self
675 .inodes
676 .get_mut(&ino)
677 .expect("inode should exist when decrementing link count");
678 inode.metadata.nlink = inode.metadata.nlink.saturating_sub(1);
679 inode.metadata.nlink == 0
680 };
681
682 if should_remove {
683 self.inodes.remove(&ino);
684 }
685 }
686
687 fn build_stat(&self, inode: &Inode) -> VirtualStat {
688 let size = match &inode.kind {
689 InodeKind::File { data } => data.len() as u64,
690 InodeKind::Directory => DIRECTORY_SIZE,
691 InodeKind::SymbolicLink { target } => target.len() as u64,
692 };
693
694 VirtualStat {
695 mode: inode.metadata.mode,
696 size,
697 blocks: block_count_for_size(size),
698 dev: MEMORY_FILESYSTEM_DEVICE_ID,
699 rdev: 0,
700 is_directory: matches!(inode.kind, InodeKind::Directory),
701 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
702 atime_ms: inode.metadata.atime_ms,
703 atime_nsec: inode.metadata.atime_nsec,
704 mtime_ms: inode.metadata.mtime_ms,
705 mtime_nsec: inode.metadata.mtime_nsec,
706 ctime_ms: inode.metadata.ctime_ms,
707 ctime_nsec: inode.metadata.ctime_nsec,
708 birthtime_ms: inode.metadata.birthtime_ms,
709 ino: inode.metadata.ino,
710 nlink: inode.metadata.nlink,
711 uid: inode.metadata.uid,
712 gid: inode.metadata.gid,
713 }
714 }
715
716 pub fn snapshot(&self) -> MemoryFileSystemSnapshot {
717 MemoryFileSystemSnapshot {
718 path_index: self.path_index.clone(),
719 inodes: self
720 .inodes
721 .iter()
722 .map(|(ino, inode)| {
723 (
724 *ino,
725 MemoryFileSystemSnapshotInode {
726 metadata: MemoryFileSystemSnapshotMetadata {
727 mode: inode.metadata.mode,
728 uid: inode.metadata.uid,
729 gid: inode.metadata.gid,
730 nlink: inode.metadata.nlink,
731 ino: inode.metadata.ino,
732 atime_ms: inode.metadata.atime_ms,
733 atime_nsec: inode.metadata.atime_nsec,
734 mtime_ms: inode.metadata.mtime_ms,
735 mtime_nsec: inode.metadata.mtime_nsec,
736 ctime_ms: inode.metadata.ctime_ms,
737 ctime_nsec: inode.metadata.ctime_nsec,
738 birthtime_ms: inode.metadata.birthtime_ms,
739 },
740 kind: match &inode.kind {
741 InodeKind::File { data } => {
742 MemoryFileSystemSnapshotInodeKind::File { data: data.clone() }
743 }
744 InodeKind::Directory => {
745 MemoryFileSystemSnapshotInodeKind::Directory
746 }
747 InodeKind::SymbolicLink { target } => {
748 MemoryFileSystemSnapshotInodeKind::SymbolicLink {
749 target: target.clone(),
750 }
751 }
752 },
753 },
754 )
755 })
756 .collect(),
757 next_ino: self.next_ino,
758 }
759 }
760
761 pub fn from_snapshot(snapshot: MemoryFileSystemSnapshot) -> Self {
762 Self {
763 path_index: snapshot.path_index,
764 inodes: snapshot
765 .inodes
766 .into_iter()
767 .map(|(ino, inode)| {
768 (
769 ino,
770 Inode {
771 metadata: Metadata {
772 mode: inode.metadata.mode,
773 uid: inode.metadata.uid,
774 gid: inode.metadata.gid,
775 nlink: inode.metadata.nlink,
776 ino: inode.metadata.ino,
777 atime_ms: inode.metadata.atime_ms,
778 atime_nsec: inode.metadata.atime_nsec,
779 mtime_ms: inode.metadata.mtime_ms,
780 mtime_nsec: inode.metadata.mtime_nsec,
781 ctime_ms: inode.metadata.ctime_ms,
782 ctime_nsec: inode.metadata.ctime_nsec,
783 birthtime_ms: inode.metadata.birthtime_ms,
784 },
785 kind: match inode.kind {
786 MemoryFileSystemSnapshotInodeKind::File { data } => {
787 InodeKind::File { data }
788 }
789 MemoryFileSystemSnapshotInodeKind::Directory => {
790 InodeKind::Directory
791 }
792 MemoryFileSystemSnapshotInodeKind::SymbolicLink { target } => {
793 InodeKind::SymbolicLink { target }
794 }
795 },
796 },
797 )
798 })
799 .collect(),
800 next_ino: snapshot.next_ino,
801 }
802 }
803}
804
805impl VirtualFileSystem for MemoryFileSystem {
806 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
807 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
808 match &inode.kind {
809 InodeKind::File { data } => {
810 inode.metadata.atime_ms = now_ms();
811 Ok(data.clone())
812 }
813 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
814 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
815 }
816 }
817
818 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
819 Ok(self
820 .read_dir_with_types(path)?
821 .into_iter()
822 .map(|entry| entry.name)
823 .collect())
824 }
825
826 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
827 self.assert_directory_path(path, "scandir")?;
828 let resolved = self.resolve_path(path, 0)?;
829 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
830 .metadata
831 .atime_ms = now_ms();
832 let prefix = if resolved == "/" {
833 String::from("/")
834 } else {
835 format!("{resolved}/")
836 };
837
838 let mut entries = BTreeMap::<String, String>::new();
839 for (candidate_path, _) in self.path_index.range(prefix.clone()..) {
840 if !candidate_path.starts_with(&prefix) {
841 break;
842 }
843
844 let rest = &candidate_path[prefix.len()..];
845 if rest.is_empty() || rest.contains('/') {
846 continue;
847 }
848
849 entries.insert(String::from(rest), String::from(rest));
850 if entries.len() > max_entries {
851 return Err(VfsError::new(
852 "ENOMEM",
853 format!(
854 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
855 ),
856 ));
857 }
858 }
859
860 Ok(entries.into_values().collect())
861 }
862
863 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
864 self.assert_directory_path(path, "scandir")?;
865 let resolved = self.resolve_path(path, 0)?;
866 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
867 .metadata
868 .atime_ms = now_ms();
869 let prefix = if resolved == "/" {
870 String::from("/")
871 } else {
872 format!("{resolved}/")
873 };
874
875 let mut entries = BTreeMap::<String, VirtualDirEntry>::new();
876 for (candidate_path, ino) in self.path_index.range(prefix.clone()..) {
877 if !candidate_path.starts_with(&prefix) {
878 break;
879 }
880
881 let rest = &candidate_path[prefix.len()..];
882 if rest.is_empty() || rest.contains('/') {
883 continue;
884 }
885
886 let inode = self
887 .inodes
888 .get(ino)
889 .expect("path index should always point at a valid inode");
890 entries.insert(
891 String::from(rest),
892 VirtualDirEntry {
893 name: String::from(rest),
894 is_directory: matches!(inode.kind, InodeKind::Directory),
895 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
896 },
897 );
898 }
899
900 Ok(entries.into_values().collect())
901 }
902
903 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
904 let normalized = self.resolve_path(path, 0)?;
905 self.mkdir(&dirname(&normalized), true)?;
906 let data = content.into();
907
908 if self.path_index.contains_key(&normalized) {
909 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
910 let now = now_ms();
911 match &mut inode.kind {
912 InodeKind::File { data: existing } => {
913 *existing = data;
914 inode.metadata.mtime_ms = now;
915 inode.metadata.ctime_ms = now;
916 return Ok(());
917 }
918 InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
919 InodeKind::SymbolicLink { .. } => return Err(VfsError::not_found("open", path)),
920 }
921 }
922
923 let ino = self.allocate_inode(InodeKind::File { data }, S_IFREG | 0o644);
924 self.path_index.insert(normalized, ino);
925 Ok(())
926 }
927
928 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
929 let normalized = self.resolve_path(path, 0)?;
930 self.mkdir(&dirname(&normalized), true)?;
931 if self.path_index.contains_key(&normalized) {
932 return Err(VfsError::already_exists("open", path));
933 }
934
935 let ino = self.allocate_inode(
936 InodeKind::File {
937 data: content.into(),
938 },
939 S_IFREG | 0o644,
940 );
941 self.path_index.insert(normalized, ino);
942 Ok(())
943 }
944
945 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
946 let normalized = self.resolve_path(path, 0)?;
947 let data = content.into();
948 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
949 let now = now_ms();
950 match &mut inode.kind {
951 InodeKind::File { data: existing } => {
952 existing.extend_from_slice(&data);
953 inode.metadata.mtime_ms = now;
954 inode.metadata.ctime_ms = now;
955 Ok(existing.len() as u64)
956 }
957 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
958 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
959 }
960 }
961
962 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
963 let normalized = self.resolve_exact_path(path)?;
964 if normalized == "/" {
965 return Ok(());
966 }
967
968 self.assert_directory_path(&dirname(&normalized), "mkdir")?;
969 if let Some(existing) = self.path_index.get(&normalized) {
970 let inode = self
971 .inodes
972 .get(existing)
973 .expect("path index should always point at a valid inode");
974 if matches!(inode.kind, InodeKind::Directory) {
975 return Ok(());
976 }
977 return Err(VfsError::already_exists("mkdir", path));
978 }
979
980 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
981 self.path_index.insert(normalized, ino);
982 Ok(())
983 }
984
985 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
986 let normalized = normalize_path(path);
987 if normalized == "/" {
988 return Ok(());
989 }
990
991 if !recursive {
992 return self.create_dir(path);
993 }
994
995 let parts: Vec<&str> = normalized
996 .split('/')
997 .filter(|part| !part.is_empty())
998 .collect();
999 let mut current = String::from("/");
1000
1001 for (index, part) in parts.iter().enumerate() {
1002 let raw_path = if current == "/" {
1003 format!("/{}", part)
1004 } else {
1005 format!("{current}/{}", part)
1006 };
1007 let resolved =
1008 self.resolve_path_with_options(&raw_path, index + 1 != parts.len(), 0)?;
1009
1010 match self.path_index.get(&resolved).copied() {
1011 Some(ino) => {
1012 let inode = self
1013 .inodes
1014 .get(&ino)
1015 .expect("path index should always point at a valid inode");
1016 if !matches!(inode.kind, InodeKind::Directory) {
1017 return Err(VfsError::not_directory("mkdir", &raw_path));
1018 }
1019 }
1020 None => {
1021 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1022 self.path_index.insert(resolved.clone(), ino);
1023 }
1024 }
1025
1026 current = resolved;
1027 }
1028
1029 Ok(())
1030 }
1031
1032 fn exists(&self, path: &str) -> bool {
1033 self.resolve_path(path, 0)
1034 .ok()
1035 .is_some_and(|resolved| self.path_index.contains_key(&resolved))
1036 }
1037
1038 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1039 let inode = self.inode_for_existing_path(path, "stat", true)?;
1040 Ok(self.build_stat(inode))
1041 }
1042
1043 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1044 self.remove_exact_path(path)
1045 }
1046
1047 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1048 let normalized = self.resolve_exact_path(path)?;
1049 if normalized == "/" {
1050 return Err(VfsError::permission_denied("rmdir", path));
1051 }
1052
1053 let ino = self
1054 .path_index
1055 .get(&normalized)
1056 .copied()
1057 .ok_or_else(|| VfsError::not_found("rmdir", path))?;
1058 let inode = self
1059 .inodes
1060 .get(&ino)
1061 .expect("path index should always point at a valid inode");
1062 if !matches!(inode.kind, InodeKind::Directory) {
1063 return Err(VfsError::not_directory("rmdir", path));
1064 }
1065
1066 let prefix = format!("{normalized}/");
1067 if self
1068 .path_index
1069 .keys()
1070 .any(|candidate| candidate.starts_with(&prefix))
1071 {
1072 return Err(VfsError::not_empty(path));
1073 }
1074
1075 self.path_index.remove(&normalized);
1076 self.decrement_link_count(ino);
1077 Ok(())
1078 }
1079
1080 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1081 let old_normalized = self.resolve_exact_path(old_path)?;
1082 let new_normalized = self.resolve_exact_path(new_path)?;
1083
1084 if old_normalized == "/" {
1085 return Err(VfsError::permission_denied("rename", old_path));
1086 }
1087
1088 if old_normalized == new_normalized {
1089 return Ok(());
1090 }
1091
1092 self.assert_directory_path(&dirname(&new_normalized), "rename")?;
1093
1094 if new_normalized.starts_with(&(old_normalized.clone() + "/")) {
1095 return Err(VfsError::invalid_input(format!(
1096 "cannot move '{}' into its own descendant '{}'",
1097 old_path, new_path
1098 )));
1099 }
1100
1101 let ino = self
1102 .path_index
1103 .get(&old_normalized)
1104 .copied()
1105 .ok_or_else(|| VfsError::not_found("rename", old_path))?;
1106 let is_directory = matches!(
1107 self.inodes
1108 .get(&ino)
1109 .expect("path index should always point at a valid inode")
1110 .kind,
1111 InodeKind::Directory
1112 );
1113
1114 self.remove_existing_destination(new_path)?;
1115
1116 if !is_directory {
1117 self.path_index.remove(&old_normalized);
1118 self.path_index.insert(new_normalized, ino);
1119 self.inodes
1120 .get_mut(&ino)
1121 .expect("renamed inode should exist")
1122 .metadata
1123 .ctime_ms = now_ms();
1124 return Ok(());
1125 }
1126
1127 let prefix = format!("{old_normalized}/");
1128 let to_move: Vec<(String, u64)> = self
1129 .path_index
1130 .iter()
1131 .filter(|(path, _)| **path == old_normalized || path.starts_with(&prefix))
1132 .map(|(path, inode_id)| (path.clone(), *inode_id))
1133 .collect();
1134
1135 for (path, _) in &to_move {
1136 self.path_index.remove(path);
1137 }
1138
1139 for (path, inode_id) in to_move {
1140 let relocated_path = if path == old_normalized {
1141 new_normalized.clone()
1142 } else {
1143 format!("{new_normalized}{}", &path[old_normalized.len()..])
1144 };
1145 self.path_index.insert(relocated_path, inode_id);
1146 }
1147
1148 self.inodes
1149 .get_mut(&ino)
1150 .expect("renamed directory inode should exist")
1151 .metadata
1152 .ctime_ms = now_ms();
1153
1154 Ok(())
1155 }
1156
1157 fn realpath(&self, path: &str) -> VfsResult<String> {
1158 let resolved = self.resolve_path(path, 0)?;
1159 if !self.path_index.contains_key(&resolved) {
1160 return Err(VfsError::not_found("realpath", path));
1161 }
1162 Ok(resolved)
1163 }
1164
1165 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1166 self.symlink_with_metadata(target, link_path, S_IFLNK | 0o777, DEFAULT_UID, DEFAULT_GID)
1167 }
1168
1169 fn read_link(&self, path: &str) -> VfsResult<String> {
1170 let inode = self.inode_for_existing_path(path, "readlink", false)?;
1171 match &inode.kind {
1172 InodeKind::SymbolicLink { target } => Ok(target.clone()),
1173 _ => Err(VfsError::invalid_input(format!(
1174 "invalid argument, readlink '{path}'"
1175 ))),
1176 }
1177 }
1178
1179 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1180 let inode = self.inode_for_existing_path(path, "lstat", false)?;
1181 Ok(self.build_stat(inode))
1182 }
1183
1184 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1185 let ino = self.inode_id_for_existing_path(old_path, "link", true)?;
1186 let inode = self
1187 .inodes
1188 .get(&ino)
1189 .expect("path index should always point at a valid inode");
1190 if !matches!(inode.kind, InodeKind::File { .. }) {
1191 return Err(VfsError::permission_denied("link", old_path));
1192 }
1193
1194 let normalized = self.resolve_exact_path(new_path)?;
1195 if self.path_index.contains_key(&normalized) {
1196 return Err(VfsError::already_exists("link", new_path));
1197 }
1198
1199 self.assert_directory_path(&dirname(&normalized), "link")?;
1200 self.path_index.insert(normalized, ino);
1201 let inode = self
1202 .inodes
1203 .get_mut(&ino)
1204 .expect("path index should always point at a valid inode");
1205 inode.metadata.nlink += 1;
1206 inode.metadata.ctime_ms = now_ms();
1207 Ok(())
1208 }
1209
1210 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1211 let inode = self.inode_mut_for_existing_path(path, "chmod", true)?;
1212 let type_bits = if mode & 0o170000 == 0 {
1213 inode.metadata.mode & 0o170000
1214 } else {
1215 mode & 0o170000
1216 };
1217 inode.metadata.mode = type_bits | (mode & 0o7777);
1218 inode.metadata.ctime_ms = now_ms();
1219 Ok(())
1220 }
1221
1222 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1223 let inode = self.inode_mut_for_existing_path(path, "chown", true)?;
1224 inode.metadata.uid = uid;
1225 inode.metadata.gid = gid;
1226 inode.metadata.ctime_ms = now_ms();
1227 Ok(())
1228 }
1229
1230 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1231 let inode = self.inode_mut_for_existing_path(path, "utimes", true)?;
1232 inode.metadata.atime_ms = atime_ms;
1233 inode.metadata.atime_nsec = 0;
1234 inode.metadata.mtime_ms = mtime_ms;
1235 inode.metadata.mtime_nsec = 0;
1236 inode.metadata.ctime_ms = now_ms();
1237 inode.metadata.ctime_nsec = 0;
1238 Ok(())
1239 }
1240
1241 fn utimes_spec(
1242 &mut self,
1243 path: &str,
1244 atime: VirtualUtimeSpec,
1245 mtime: VirtualUtimeSpec,
1246 follow_symlinks: bool,
1247 ) -> VfsResult<()> {
1248 let stat = if follow_symlinks {
1249 self.stat(path)?
1250 } else {
1251 self.lstat(path)?
1252 };
1253 let inode = self.inode_mut_for_existing_path(path, "utimes", follow_symlinks)?;
1254 let now = now_time_spec();
1255 let atime = resolve_utime_spec(
1256 atime,
1257 now,
1258 VirtualTimeSpec {
1259 sec: (stat.atime_ms / 1_000) as i64,
1260 nsec: stat.atime_nsec,
1261 },
1262 )?;
1263 let mtime = resolve_utime_spec(
1264 mtime,
1265 now,
1266 VirtualTimeSpec {
1267 sec: (stat.mtime_ms / 1_000) as i64,
1268 nsec: stat.mtime_nsec,
1269 },
1270 )?;
1271 inode.metadata.atime_ms = atime.to_truncated_millis()?;
1272 inode.metadata.atime_nsec = atime.nsec;
1273 inode.metadata.mtime_ms = mtime.to_truncated_millis()?;
1274 inode.metadata.mtime_nsec = mtime.nsec;
1275 let ctime = now_time_spec();
1276 inode.metadata.ctime_ms = ctime.to_truncated_millis()?;
1277 inode.metadata.ctime_nsec = ctime.nsec;
1278 Ok(())
1279 }
1280
1281 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1282 let inode = self.inode_mut_for_existing_path(path, "truncate", true)?;
1283 let now = now_ms();
1284 match &mut inode.kind {
1285 InodeKind::File { data } => {
1286 data.resize(length as usize, 0);
1287 inode.metadata.mtime_ms = now;
1288 inode.metadata.ctime_ms = now;
1289 Ok(())
1290 }
1291 InodeKind::Directory => Err(VfsError::is_directory("truncate", path)),
1292 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("truncate", path)),
1293 }
1294 }
1295
1296 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1297 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
1298 match &mut inode.kind {
1299 InodeKind::File { data } => {
1300 inode.metadata.atime_ms = now_ms();
1301 let start = offset as usize;
1302 if start >= data.len() {
1303 return Ok(Vec::new());
1304 }
1305 let end = start.saturating_add(length).min(data.len());
1306 Ok(data[start..end].to_vec())
1307 }
1308 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1309 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1310 }
1311 }
1312}
1313
1314impl Default for MemoryFileSystem {
1315 fn default() -> Self {
1316 Self::new()
1317 }
1318}
1319
1320fn resolve_utime_spec(
1321 spec: VirtualUtimeSpec,
1322 now: VirtualTimeSpec,
1323 existing: VirtualTimeSpec,
1324) -> VfsResult<VirtualTimeSpec> {
1325 match spec {
1326 VirtualUtimeSpec::Set(spec) => Ok(spec),
1327 VirtualUtimeSpec::Now => Ok(now),
1328 VirtualUtimeSpec::Omit => Ok(existing),
1329 }
1330}
1331
1332fn resolve_utime_millis(
1333 spec: VirtualUtimeSpec,
1334 now_ms: u64,
1335 existing: Option<VirtualTimeSpec>,
1336) -> VfsResult<u64> {
1337 match spec {
1338 VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis(),
1339 VirtualUtimeSpec::Now => Ok(now_ms),
1340 VirtualUtimeSpec::Omit => existing
1341 .ok_or_else(|| VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata"))?
1342 .to_truncated_millis(),
1343 }
1344}
1345
1346pub fn validate_path(path: &str) -> VfsResult<()> {
1347 if path.as_bytes().contains(&0) {
1348 return Err(VfsError::invalid_input("path contains NUL byte"));
1349 }
1350 if let Some(control) = path
1351 .bytes()
1352 .find(|byte| byte.is_ascii_control() && *byte != b'\0')
1353 {
1354 return Err(VfsError::invalid_input(format!(
1355 "path contains control character byte 0x{control:02x}"
1356 )));
1357 }
1358 let normalized = normalize_path(path);
1359 if normalized.len() > MAX_PATH_LENGTH {
1360 return Err(VfsError::path_too_long(path));
1361 }
1362 Ok(())
1363}
1364
1365pub fn normalize_path(path: &str) -> String {
1366 if path.is_empty() {
1367 return String::from("/");
1368 }
1369
1370 let candidate = if path.starts_with('/') {
1371 path.to_owned()
1372 } else {
1373 format!("/{path}")
1374 };
1375
1376 let mut resolved = Vec::new();
1377 for part in candidate.split('/') {
1378 match part {
1379 "" | "." => {}
1380 ".." => {
1381 resolved.pop();
1382 }
1383 component => resolved.push(component),
1384 }
1385 }
1386
1387 if resolved.is_empty() {
1388 String::from("/")
1389 } else {
1390 format!("/{}", resolved.join("/"))
1391 }
1392}
1393
1394fn block_count_for_size(size: u64) -> u64 {
1395 if size == 0 {
1396 0
1397 } else {
1398 size.div_ceil(512)
1399 }
1400}
1401
1402fn dirname(path: &str) -> String {
1403 let normalized = normalize_path(path);
1404 let Some((head, _)) = normalized.rsplit_once('/') else {
1405 return String::from("/");
1406 };
1407
1408 if head.is_empty() {
1409 String::from("/")
1410 } else {
1411 String::from(head)
1412 }
1413}
1414
1415fn now_ms() -> u64 {
1416 SystemTime::now()
1417 .duration_since(UNIX_EPOCH)
1418 .unwrap_or_default()
1419 .as_millis() as u64
1420}
1421
1422fn now_time_spec() -> VirtualTimeSpec {
1423 let now = SystemTime::now()
1424 .duration_since(UNIX_EPOCH)
1425 .unwrap_or_default();
1426 VirtualTimeSpec {
1427 sec: now.as_secs() as i64,
1428 nsec: now.subsec_nanos(),
1429 }
1430}