Skip to main content

agent_os_kernel/
vfs.rs

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}