Skip to main content

secure_exec_vfs_core/posix/
vfs.rs

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