Skip to main content

vfs/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 web_time::{SystemTime, UNIX_EPOCH};
7
8pub const S_IFREG: u32 = 0o100000;
9pub const S_IFDIR: u32 = 0o040000;
10pub const S_IFLNK: u32 = 0o120000;
11pub const S_IFCHR: u32 = 0o020000;
12pub const S_IFBLK: u32 = 0o060000;
13pub const S_IFIFO: u32 = 0o010000;
14pub const RENAME_NOREPLACE: u32 = 1;
15pub const RENAME_EXCHANGE: u32 = 2;
16pub const RENAME_WHITEOUT: u32 = 4;
17
18// Each MemoryFileSystem instance gets its own device id, like a Linux
19// superblock. Inode numbers are only unique within one instance, so layered
20// or mounted compositions need distinct dev values for (dev, ino) file
21// identity comparisons to be meaningful. The counter starts above the small
22// constants reserved for synthetic device and pipe stats.
23static NEXT_MEMORY_FILESYSTEM_DEVICE_ID: AtomicU64 = AtomicU64::new(256);
24static NEXT_RENAME_EXCHANGE_ID: AtomicU64 = AtomicU64::new(1);
25
26fn allocate_memory_filesystem_device_id() -> u64 {
27    NEXT_MEMORY_FILESYSTEM_DEVICE_ID.fetch_add(1, Ordering::Relaxed)
28}
29
30const DEFAULT_UID: u32 = 1000;
31const DEFAULT_GID: u32 = 1000;
32const DIRECTORY_SIZE: u64 = 4096;
33pub const MAX_PATH_LENGTH: usize = 4096;
34const MAX_SYMLINK_DEPTH: usize = 40;
35pub const XATTR_CREATE: u32 = 1;
36pub const XATTR_REPLACE: u32 = 2;
37pub const XATTR_NAME_MAX: usize = 255;
38pub const XATTR_SIZE_MAX: usize = 64 * 1024;
39pub const XATTR_LIST_MAX: usize = 64 * 1024;
40
41pub type VfsResult<T> = Result<T, VfsError>;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct VfsError {
45    code: &'static str,
46    message: String,
47}
48
49impl VfsError {
50    pub fn new(code: &'static str, message: impl Into<String>) -> Self {
51        Self {
52            code,
53            message: message.into(),
54        }
55    }
56
57    pub fn io(message: impl Into<String>) -> Self {
58        Self::new("EIO", message)
59    }
60
61    pub fn unsupported(message: impl Into<String>) -> Self {
62        Self::new("ENOSYS", message)
63    }
64
65    pub fn code(&self) -> &'static str {
66        self.code
67    }
68
69    pub fn message(&self) -> &str {
70        &self.message
71    }
72
73    fn not_found(op: &'static str, path: &str) -> Self {
74        Self::new(
75            "ENOENT",
76            format!("no such file or directory, {op} '{path}'"),
77        )
78    }
79
80    fn already_exists(op: &'static str, path: &str) -> Self {
81        Self::new("EEXIST", format!("file already exists, {op} '{path}'"))
82    }
83
84    fn is_directory(op: &'static str, path: &str) -> Self {
85        Self::new(
86            "EISDIR",
87            format!("illegal operation on a directory, {op} '{path}'"),
88        )
89    }
90
91    fn not_directory(op: &'static str, path: &str) -> Self {
92        Self::new("ENOTDIR", format!("not a directory, {op} '{path}'"))
93    }
94
95    fn path_too_long(path: &str) -> Self {
96        Self::new("ENAMETOOLONG", format!("file name too long: {path}"))
97    }
98
99    fn not_empty(path: &str) -> Self {
100        Self::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
101    }
102
103    pub fn permission_denied(op: &'static str, path: &str) -> Self {
104        Self::new("EPERM", format!("operation not permitted, {op} '{path}'"))
105    }
106
107    pub fn access_denied(op: &'static str, path: &str, reason: Option<&str>) -> Self {
108        let message = match reason {
109            Some(reason) => format!("permission denied, {op} '{path}': {reason}"),
110            None => format!("permission denied, {op} '{path}'"),
111        };
112
113        Self::new("EACCES", message)
114    }
115
116    fn symlink_loop(path: &str) -> Self {
117        Self::new(
118            "ELOOP",
119            format!("too many levels of symbolic links, '{path}'"),
120        )
121    }
122
123    fn invalid_input(message: impl Into<String>) -> Self {
124        Self::new("EINVAL", message)
125    }
126
127    fn invalid_utf8(path: &str) -> Self {
128        Self::new("EINVAL", format!("file contains invalid UTF-8, '{path}'"))
129    }
130}
131
132impl fmt::Display for VfsError {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}: {}", self.code, self.message)
135    }
136}
137
138impl Error for VfsError {}
139
140pub fn validate_xattr_name(name: &str) -> VfsResult<()> {
141    if name.is_empty() || name.len() > XATTR_NAME_MAX || !name.contains('.') || name.contains('\0')
142    {
143        return Err(VfsError::new(
144            "ERANGE",
145            format!("invalid extended attribute name: {name:?}"),
146        ));
147    }
148    Ok(())
149}
150
151pub fn set_xattr_value(
152    xattrs: &mut BTreeMap<String, Vec<u8>>,
153    name: &str,
154    value: &[u8],
155    flags: u32,
156) -> VfsResult<()> {
157    validate_xattr_name(name)?;
158    if flags & !(XATTR_CREATE | XATTR_REPLACE) != 0 || flags == (XATTR_CREATE | XATTR_REPLACE) {
159        return Err(VfsError::new(
160            "EINVAL",
161            format!("invalid xattr flags: {flags}"),
162        ));
163    }
164    if value.len() > XATTR_SIZE_MAX {
165        return Err(VfsError::new(
166            "E2BIG",
167            format!(
168                "extended attribute value is {} bytes; Linux-compatible limit is {XATTR_SIZE_MAX} bytes",
169                value.len()
170            ),
171        ));
172    }
173    let exists = xattrs.contains_key(name);
174    if flags == XATTR_CREATE && exists {
175        return Err(VfsError::new(
176            "EEXIST",
177            format!("extended attribute already exists: {name}"),
178        ));
179    }
180    if flags == XATTR_REPLACE && !exists {
181        return Err(VfsError::new(
182            "ENODATA",
183            format!("extended attribute does not exist: {name}"),
184        ));
185    }
186    let old_len = xattrs.get(name).map_or(0, Vec::len);
187    let list_bytes = xattrs.keys().map(|key| key.len() + 1).sum::<usize>();
188    let value_bytes = xattrs.values().map(Vec::len).sum::<usize>();
189    let new_total = list_bytes
190        .saturating_add(value_bytes)
191        .saturating_sub(old_len)
192        .saturating_add(if exists { 0 } else { name.len() + 1 })
193        .saturating_add(value.len());
194    if new_total > XATTR_LIST_MAX {
195        return Err(VfsError::new(
196            "ENOSPC",
197            format!(
198                "inode extended attributes require {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes"
199            ),
200        ));
201    }
202    xattrs.insert(name.to_string(), value.to_vec());
203    Ok(())
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum FileType {
208    File,
209    Directory,
210    SymbolicLink,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct VirtualDirEntry {
215    pub name: String,
216    pub is_directory: bool,
217    pub is_symbolic_link: bool,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct VirtualStat {
222    pub mode: u32,
223    pub size: u64,
224    pub blocks: u64,
225    pub dev: u64,
226    pub rdev: u64,
227    pub is_directory: bool,
228    pub is_symbolic_link: bool,
229    pub atime_ms: u64,
230    pub atime_nsec: u32,
231    pub mtime_ms: u64,
232    pub mtime_nsec: u32,
233    pub ctime_ms: u64,
234    pub ctime_nsec: u32,
235    pub birthtime_ms: u64,
236    pub ino: u64,
237    pub nlink: u64,
238    pub uid: u32,
239    pub gid: u32,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub struct VirtualTimeSpec {
244    pub sec: i64,
245    pub nsec: u32,
246}
247
248impl VirtualTimeSpec {
249    pub fn new(sec: i64, nsec: u32) -> VfsResult<Self> {
250        if nsec >= 1_000_000_000 {
251            return Err(VfsError::new(
252                "EINVAL",
253                format!("timespec nanoseconds out of range: {nsec}"),
254            ));
255        }
256        Ok(Self { sec, nsec })
257    }
258
259    pub fn from_millis(ms: u64) -> Self {
260        Self {
261            sec: (ms / 1_000) as i64,
262            nsec: ((ms % 1_000) * 1_000_000) as u32,
263        }
264    }
265
266    pub fn to_truncated_millis(self) -> VfsResult<u64> {
267        if self.sec < 0 {
268            return Err(VfsError::new(
269                "EINVAL",
270                format!(
271                    "negative timestamps are not supported by this filesystem: {}",
272                    self.sec
273                ),
274            ));
275        }
276        let seconds = u64::try_from(self.sec).map_err(|_| {
277            VfsError::new("EINVAL", format!("timestamp is out of range: {}", self.sec))
278        })?;
279        Ok(seconds.saturating_mul(1_000) + (self.nsec as u64 / 1_000_000))
280    }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum VirtualUtimeSpec {
285    Set(VirtualTimeSpec),
286    Now,
287    Omit,
288}
289
290pub trait VirtualFileSystem {
291    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
292    fn read_text_file(&mut self, path: &str) -> VfsResult<String> {
293        String::from_utf8(self.read_file(path)?).map_err(|_| VfsError::invalid_utf8(path))
294    }
295    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
296    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
297        let entries = self.read_dir(path)?;
298        if entries.len() > max_entries {
299            return Err(VfsError::new(
300                "ENOMEM",
301                format!(
302                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
303                ),
304            ));
305        }
306        Ok(entries)
307    }
308    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
309    /// Writes caller-owned bytes into the filesystem.
310    ///
311    /// This raw VFS primitive does not enforce VM resource policy. Kernel entry
312    /// points must preflight file sizes and inode growth before calling it.
313    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()>;
314    fn write_file_with_mode(
315        &mut self,
316        path: &str,
317        content: impl Into<Vec<u8>>,
318        mode: Option<u32>,
319    ) -> VfsResult<()> {
320        let _ = mode;
321        self.write_file(path, content)
322    }
323    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
324        let content = content.into();
325        if self.exists(path) {
326            return Err(VfsError::already_exists("open", path));
327        }
328        self.write_file(path, content)
329    }
330    fn create_file_exclusive_with_mode(
331        &mut self,
332        path: &str,
333        content: impl Into<Vec<u8>>,
334        mode: Option<u32>,
335    ) -> VfsResult<()> {
336        let _ = mode;
337        self.create_file_exclusive(path, content)
338    }
339    /// Appends caller-owned bytes into the filesystem after checking that the
340    /// in-memory file can grow without overflowing addressable memory.
341    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
342        let content = content.into();
343        let mut existing = self.read_file(path)?;
344        reserve_file_growth(&mut existing, content.len())?;
345        existing.extend_from_slice(&content);
346        let new_len = existing.len() as u64;
347        self.write_file(path, existing)?;
348        Ok(new_len)
349    }
350    fn create_dir(&mut self, path: &str) -> VfsResult<()>;
351    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
352        let _ = mode;
353        self.create_dir(path)
354    }
355    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
356    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
357        let _ = (mode, rdev);
358        Err(VfsError::new(
359            "EOPNOTSUPP",
360            format!("special inode creation is not supported for {path}"),
361        ))
362    }
363    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
364        let _ = mode;
365        self.mkdir(path, recursive)
366    }
367    fn exists(&self, path: &str) -> bool;
368    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
369    fn remove_file(&mut self, path: &str) -> VfsResult<()>;
370    fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
371    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
372    fn rename_at2(&mut self, old_path: &str, new_path: &str, flags: u32) -> VfsResult<()> {
373        match flags {
374            0 => self.rename(old_path, new_path),
375            RENAME_NOREPLACE => {
376                self.lstat(old_path)?;
377                match self.lstat(new_path) {
378                    Ok(_) => Err(VfsError::new(
379                        "EEXIST",
380                        format!("file exists, rename '{old_path}' -> '{new_path}'"),
381                    )),
382                    Err(error) if error.code() == "ENOENT" => self.rename(old_path, new_path),
383                    Err(error) => Err(error),
384                }
385            }
386            RENAME_EXCHANGE => {
387                self.lstat(old_path)?;
388                self.lstat(new_path)?;
389                if normalize_path(old_path) == normalize_path(new_path) {
390                    return Ok(());
391                }
392
393                let parent = dirname(old_path);
394                let temporary = (0..128)
395                    .find_map(|_| {
396                        let id = NEXT_RENAME_EXCHANGE_ID.fetch_add(1, Ordering::Relaxed);
397                        let candidate = if parent == "/" {
398                            format!("/.agentos-rename-exchange-{id}")
399                        } else {
400                            format!("{parent}/.agentos-rename-exchange-{id}")
401                        };
402                        if self
403                            .lstat(&candidate)
404                            .is_err_and(|error| error.code() == "ENOENT")
405                        {
406                            Some(candidate)
407                        } else {
408                            None
409                        }
410                    })
411                    .ok_or_else(|| {
412                        VfsError::new(
413                            "EEXIST",
414                            "could not allocate a bounded temporary rename-exchange path",
415                        )
416                    })?;
417
418                self.rename(old_path, &temporary)?;
419                if let Err(error) = self.rename(new_path, old_path) {
420                    return match self.rename(&temporary, old_path) {
421                        Ok(()) => Err(error),
422                        Err(rollback) => Err(VfsError::new(
423                            "EIO",
424                            format!("rename exchange failed: {error}; rollback failed: {rollback}"),
425                        )),
426                    };
427                }
428                if let Err(error) = self.rename(&temporary, new_path) {
429                    let rollback_destination = self.rename(old_path, new_path);
430                    let rollback_source = self.rename(&temporary, old_path);
431                    return match (rollback_destination, rollback_source) {
432                        (Ok(()), Ok(())) => Err(error),
433                        (destination, source) => Err(VfsError::new(
434                            "EIO",
435                            format!(
436                                "rename exchange failed: {error}; rollback destination: {destination:?}; rollback source: {source:?}"
437                            ),
438                        )),
439                    };
440                }
441                Ok(())
442            }
443            _ => Err(VfsError::new(
444                "EINVAL",
445                format!("invalid renameat2 flags: {flags:#x}"),
446            )),
447        }
448    }
449    fn realpath(&self, path: &str) -> VfsResult<String>;
450    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
451    fn read_link(&self, path: &str) -> VfsResult<String>;
452    fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
453    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
454    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
455    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
456    fn chown_spec(
457        &mut self,
458        path: &str,
459        uid: u32,
460        gid: u32,
461        follow_symlinks: bool,
462    ) -> VfsResult<()> {
463        if !follow_symlinks {
464            return Err(VfsError::unsupported(format!(
465                "lchown is not supported for '{path}'"
466            )));
467        }
468        self.chown(path, uid, gid)
469    }
470
471    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
472        self.chown(path, uid, gid)
473    }
474    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
475        let _ = (name, follow_symlinks);
476        Err(VfsError::new(
477            "EOPNOTSUPP",
478            format!("extended attributes are not supported for {path}"),
479        ))
480    }
481    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
482        let _ = follow_symlinks;
483        Err(VfsError::new(
484            "EOPNOTSUPP",
485            format!("extended attributes are not supported for {path}"),
486        ))
487    }
488    fn set_xattr(
489        &mut self,
490        path: &str,
491        name: &str,
492        value: Vec<u8>,
493        flags: u32,
494        follow_symlinks: bool,
495    ) -> VfsResult<()> {
496        let _ = (name, value, flags, follow_symlinks);
497        Err(VfsError::new(
498            "EOPNOTSUPP",
499            format!("extended attributes are not supported for {path}"),
500        ))
501    }
502    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
503        let _ = (name, follow_symlinks);
504        Err(VfsError::new(
505            "EOPNOTSUPP",
506            format!("extended attributes are not supported for {path}"),
507        ))
508    }
509    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
510    fn utimes_spec(
511        &mut self,
512        path: &str,
513        atime: VirtualUtimeSpec,
514        mtime: VirtualUtimeSpec,
515        follow_symlinks: bool,
516    ) -> VfsResult<()> {
517        if !follow_symlinks {
518            return Err(VfsError::unsupported(format!(
519                "lutimes is not supported for '{path}'"
520            )));
521        }
522        let existing = match (atime, mtime) {
523            (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
524            _ => None,
525        };
526        let now = now_ms();
527        let atime_ms = resolve_utime_millis(
528            atime,
529            now,
530            existing.as_ref().map(|stat| VirtualTimeSpec {
531                sec: (stat.atime_ms / 1_000) as i64,
532                nsec: stat.atime_nsec,
533            }),
534        )?;
535        let mtime_ms = resolve_utime_millis(
536            mtime,
537            now,
538            existing.as_ref().map(|stat| VirtualTimeSpec {
539                sec: (stat.mtime_ms / 1_000) as i64,
540                nsec: stat.mtime_nsec,
541            }),
542        )?;
543        self.utimes(path, atime_ms, mtime_ms)
544    }
545    /// Resizes a file. VM resource policy must be enforced by the caller.
546    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
547    fn sync(&mut self, _path: &str) -> VfsResult<()> {
548        Ok(())
549    }
550    /// Allocates storage for a range without changing existing bytes.
551    /// VM resource policy must be enforced by the caller.
552    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
553        const ALLOCATION_CHUNK_BYTES: u64 = 64 * 1024;
554
555        let end = offset
556            .checked_add(length)
557            .ok_or_else(|| VfsError::new("EINVAL", "allocation range overflows"))?;
558        if length == 0 {
559            return Ok(());
560        }
561        let stat = self.stat(path)?;
562        if end > stat.size {
563            self.truncate(path, end)?;
564        }
565        let mut cursor = offset;
566        while cursor < end {
567            let chunk_len = (end - cursor).min(ALLOCATION_CHUNK_BYTES) as usize;
568            let mut bytes = self.pread(path, cursor, chunk_len)?;
569            bytes.resize(chunk_len, 0);
570            self.pwrite(path, bytes, cursor)?;
571            cursor += chunk_len as u64;
572        }
573        Ok(())
574    }
575    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
576        validate_shift_range(offset, length)?;
577        let size = self.stat(path)?.size;
578        if offset >= size {
579            return Err(VfsError::new(
580                "EINVAL",
581                "insert range offset must be before EOF",
582            ));
583        }
584        let tail_len = usize::try_from(size - offset)
585            .map_err(|_| VfsError::new("EINVAL", "insert range tail is too large"))?;
586        let tail = self.pread(path, offset, tail_len)?;
587        self.truncate(
588            path,
589            size.checked_add(length)
590                .ok_or_else(|| VfsError::new("EINVAL", "insert range size overflows"))?,
591        )?;
592        self.pwrite(path, tail, offset + length)?;
593        self.punch_hole(path, offset, length)
594    }
595    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
596        validate_shift_range(offset, length)?;
597        let size = self.stat(path)?.size;
598        let end = offset
599            .checked_add(length)
600            .ok_or_else(|| VfsError::new("EINVAL", "collapse range overflows"))?;
601        if end >= size {
602            return Err(VfsError::new(
603                "EINVAL",
604                "collapse range must end before EOF",
605            ));
606        }
607        let tail_len = usize::try_from(size - end)
608            .map_err(|_| VfsError::new("EINVAL", "collapse range tail is too large"))?;
609        let tail = self.pread(path, end, tail_len)?;
610        self.pwrite(path, tail, offset)?;
611        self.truncate(path, size - length)
612    }
613    /// Zeroes and allocates a byte range, optionally preserving the file size.
614    fn zero_range(
615        &mut self,
616        path: &str,
617        offset: u64,
618        length: u64,
619        keep_size: bool,
620    ) -> VfsResult<()> {
621        let end = offset
622            .checked_add(length)
623            .ok_or_else(|| VfsError::new("EINVAL", "zero range overflows"))?;
624        if length == 0 {
625            return Err(VfsError::new("EINVAL", "zero range length must be nonzero"));
626        }
627        let original_size = self.stat(path)?.size;
628        self.allocate(path, offset, length)?;
629        let zero_end = if keep_size {
630            end.min(original_size)
631        } else {
632            end
633        };
634        let mut cursor = offset.min(zero_end);
635        while cursor < zero_end {
636            let chunk_len = (zero_end - cursor).min(64 * 1024) as usize;
637            self.pwrite(path, vec![0; chunk_len], cursor)?;
638            cursor += chunk_len as u64;
639        }
640        if keep_size && self.stat(path)?.size != original_size {
641            self.truncate(path, original_size)?;
642        }
643        Ok(())
644    }
645    /// Deallocates a byte range while preserving the file size. Bytes in the
646    /// intersecting range read back as zeroes.
647    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
648        let requested_end = offset
649            .checked_add(length)
650            .ok_or_else(|| VfsError::new("EINVAL", "hole-punch range overflows"))?;
651        let size = self.stat(path)?.size;
652        let end = requested_end.min(size);
653        let mut cursor = offset.min(size);
654        while cursor < end {
655            let chunk_len = (end - cursor).min(64 * 1024) as usize;
656            self.pwrite(path, vec![0; chunk_len], cursor)?;
657            cursor += chunk_len as u64;
658        }
659        Ok(())
660    }
661    /// Returns allocated byte ranges as half-open `(start, end)` intervals.
662    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
663        Err(VfsError::new(
664            "EOPNOTSUPP",
665            format!("extent mapping is not supported for {path}"),
666        ))
667    }
668    /// Returns allocated byte ranges whose contents are logically zero until
669    /// first written, as half-open `(start, end)` intervals.
670    fn unwritten_ranges(&mut self, _path: &str) -> VfsResult<Vec<(u64, u64)>> {
671        Ok(Vec::new())
672    }
673    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
674    /// Writes caller-owned bytes at an offset after checking that the in-memory
675    /// file can grow without overflowing addressable memory.
676    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
677        let content = content.into();
678        let mut existing = self.read_file(path)?;
679        let start = checked_file_len(offset, "pwrite offset")?;
680        if start > existing.len() {
681            resize_file_data(&mut existing, start)?;
682        }
683        let end = start.checked_add(content.len()).ok_or_else(|| {
684            VfsError::new(
685                "ENOMEM",
686                format!(
687                    "pwrite result length overflows addressable memory: offset {offset}, content length {}",
688                    content.len()
689                ),
690            )
691        })?;
692        if end > existing.len() {
693            resize_file_data(&mut existing, end)?;
694        }
695        existing[start..end].copy_from_slice(&content);
696        self.write_file(path, existing)
697    }
698}
699
700#[derive(Debug, Clone)]
701struct Metadata {
702    mode: u32,
703    uid: u32,
704    gid: u32,
705    nlink: u64,
706    ino: u64,
707    atime_ms: u64,
708    atime_nsec: u32,
709    mtime_ms: u64,
710    mtime_nsec: u32,
711    ctime_ms: u64,
712    ctime_nsec: u32,
713    birthtime_ms: u64,
714    allocated_extents: Vec<(u64, u64)>,
715    unwritten_extents: Vec<(u64, u64)>,
716    xattrs: BTreeMap<String, Vec<u8>>,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub struct MemoryFileSystemSnapshotMetadata {
721    pub mode: u32,
722    pub uid: u32,
723    pub gid: u32,
724    pub nlink: u64,
725    pub ino: u64,
726    pub atime_ms: u64,
727    #[serde(default)]
728    pub atime_nsec: u32,
729    pub mtime_ms: u64,
730    #[serde(default)]
731    pub mtime_nsec: u32,
732    pub ctime_ms: u64,
733    #[serde(default)]
734    pub ctime_nsec: u32,
735    pub birthtime_ms: u64,
736    #[serde(default)]
737    pub allocated_extents: Vec<(u64, u64)>,
738    #[serde(default)]
739    pub unwritten_extents: Vec<(u64, u64)>,
740    #[serde(default)]
741    pub xattrs: BTreeMap<String, Vec<u8>>,
742}
743
744#[derive(Debug, Clone)]
745enum InodeKind {
746    File { data: Vec<u8> },
747    Directory,
748    SymbolicLink { target: String },
749    CharacterDevice { rdev: u64 },
750    BlockDevice { rdev: u64 },
751    Fifo,
752}
753
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub enum MemoryFileSystemSnapshotInodeKind {
756    File { data: Vec<u8> },
757    Directory,
758    SymbolicLink { target: String },
759    CharacterDevice { rdev: u64 },
760    BlockDevice { rdev: u64 },
761    Fifo,
762}
763
764#[derive(Debug, Clone)]
765struct Inode {
766    metadata: Metadata,
767    kind: InodeKind,
768}
769
770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
771pub struct MemoryFileSystemSnapshotInode {
772    pub metadata: MemoryFileSystemSnapshotMetadata,
773    pub kind: MemoryFileSystemSnapshotInodeKind,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777pub struct MemoryFileSystemSnapshot {
778    pub path_index: BTreeMap<String, u64>,
779    pub inodes: BTreeMap<u64, MemoryFileSystemSnapshotInode>,
780    pub next_ino: u64,
781}
782
783#[derive(Debug)]
784pub struct MemoryFileSystem {
785    device_id: u64,
786    path_index: BTreeMap<String, u64>,
787    inodes: BTreeMap<u64, Inode>,
788    next_ino: u64,
789}
790
791impl MemoryFileSystem {
792    pub fn new() -> Self {
793        let mut filesystem = Self {
794            device_id: allocate_memory_filesystem_device_id(),
795            path_index: BTreeMap::new(),
796            inodes: BTreeMap::new(),
797            next_ino: 1,
798        };
799
800        let root_ino = filesystem.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
801        filesystem.path_index.insert(String::from("/"), root_ino);
802        filesystem
803    }
804
805    pub fn read_dir_filtered_limited<F>(
806        &mut self,
807        path: &str,
808        max_entries: usize,
809        mut include: F,
810    ) -> VfsResult<Vec<String>>
811    where
812        F: FnMut(&str) -> bool,
813    {
814        self.assert_directory_path(path, "scandir")?;
815        let resolved = self.resolve_path(path, 0)?;
816        self.inode_mut_for_existing_path(&resolved, "scandir", false)?
817            .metadata
818            .atime_ms = now_ms();
819        let prefix = if resolved == "/" {
820            String::from("/")
821        } else {
822            format!("{resolved}/")
823        };
824
825        let mut entries = BTreeMap::<String, String>::new();
826        for (candidate_path, _) in self.path_index.range(prefix.clone()..) {
827            if !candidate_path.starts_with(&prefix) {
828                break;
829            }
830
831            let rest = &candidate_path[prefix.len()..];
832            if rest.is_empty() || rest.contains('/') || !include(rest) {
833                continue;
834            }
835
836            entries.insert(String::from(rest), String::from(rest));
837            if entries.len() > max_entries {
838                return Err(VfsError::new(
839                    "ENOMEM",
840                    format!(
841                        "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
842                    ),
843                ));
844            }
845        }
846
847        Ok(entries.into_values().collect())
848    }
849
850    pub fn link_count_in_subtree(&self, ino: u64, path: &str) -> usize {
851        let normalized = normalize_path(path);
852        let prefix = if normalized == "/" {
853            String::from("/")
854        } else {
855            format!("{normalized}/")
856        };
857
858        self.path_index
859            .iter()
860            .filter(|(candidate_path, candidate_ino)| {
861                **candidate_ino == ino
862                    && (candidate_path.as_str() == normalized
863                        || candidate_path.starts_with(&prefix))
864            })
865            .count()
866    }
867
868    fn allocate_inode(&mut self, kind: InodeKind, mode: u32) -> u64 {
869        let ino = self.next_ino;
870        self.next_ino += 1;
871        let now = now_ms();
872        let nlink = if matches!(kind, InodeKind::Directory) {
873            2
874        } else {
875            1
876        };
877        let allocated_extents = match &kind {
878            InodeKind::File { data } => dense_allocation(data.len() as u64),
879            InodeKind::Directory
880            | InodeKind::SymbolicLink { .. }
881            | InodeKind::CharacterDevice { .. }
882            | InodeKind::BlockDevice { .. }
883            | InodeKind::Fifo => Vec::new(),
884        };
885        self.inodes.insert(
886            ino,
887            Inode {
888                metadata: Metadata {
889                    mode,
890                    uid: DEFAULT_UID,
891                    gid: DEFAULT_GID,
892                    nlink,
893                    ino,
894                    atime_ms: now,
895                    atime_nsec: 0,
896                    mtime_ms: now,
897                    mtime_nsec: 0,
898                    ctime_ms: now,
899                    ctime_nsec: 0,
900                    birthtime_ms: now,
901                    allocated_extents,
902                    unwritten_extents: Vec::new(),
903                    xattrs: BTreeMap::new(),
904                },
905                kind,
906            },
907        );
908        ino
909    }
910
911    pub fn symlink_with_metadata(
912        &mut self,
913        target: &str,
914        link_path: &str,
915        mode: u32,
916        uid: u32,
917        gid: u32,
918    ) -> VfsResult<()> {
919        let normalized = self.resolve_exact_path(link_path)?;
920        if self.path_index.contains_key(&normalized) {
921            return Err(VfsError::already_exists("symlink", link_path));
922        }
923
924        self.assert_directory_path(&dirname(&normalized), "symlink")?;
925        let ino = self.allocate_inode(
926            InodeKind::SymbolicLink {
927                target: String::from(target),
928            },
929            if mode & 0o170000 == 0 {
930                S_IFLNK | (mode & 0o7777)
931            } else {
932                mode
933            },
934        );
935        let inode = self
936            .inodes
937            .get_mut(&ino)
938            .expect("allocated inode should exist");
939        inode.metadata.uid = uid;
940        inode.metadata.gid = gid;
941        self.path_index.insert(normalized, ino);
942        Ok(())
943    }
944
945    fn resolve_path_with_options(
946        &self,
947        path: &str,
948        follow_final_symlink: bool,
949        depth: usize,
950    ) -> VfsResult<String> {
951        validate_path(path)?;
952        if depth > MAX_SYMLINK_DEPTH {
953            return Err(VfsError::symlink_loop(path));
954        }
955
956        let normalized = normalize_path(path);
957        if normalized == "/" {
958            return Ok(normalized);
959        }
960
961        let components: Vec<&str> = normalized
962            .split('/')
963            .filter(|part| !part.is_empty())
964            .collect();
965        let mut current = String::from("/");
966
967        for (index, component) in components.iter().enumerate() {
968            let candidate = if current == "/" {
969                format!("/{}", component)
970            } else {
971                format!("{current}/{}", component)
972            };
973            let is_final = index + 1 == components.len();
974            let should_follow = !is_final || follow_final_symlink;
975
976            if let Some(ino) = self.path_index.get(&candidate) {
977                let inode = self
978                    .inodes
979                    .get(ino)
980                    .expect("path index should always point at a valid inode");
981
982                if should_follow {
983                    if let InodeKind::SymbolicLink { target } = &inode.kind {
984                        let target_path = if target.starts_with('/') {
985                            target.clone()
986                        } else {
987                            normalize_path(&format!("{}/{}", dirname(&candidate), target))
988                        };
989                        let remainder = components[index + 1..].join("/");
990                        let next_path = if remainder.is_empty() {
991                            target_path
992                        } else {
993                            normalize_path(&format!("{target_path}/{remainder}"))
994                        };
995                        return self.resolve_path_with_options(
996                            &next_path,
997                            follow_final_symlink,
998                            depth + 1,
999                        );
1000                    }
1001                }
1002
1003                if !is_final && !matches!(inode.kind, InodeKind::Directory) {
1004                    return Err(VfsError::not_directory("stat", &candidate));
1005                }
1006            }
1007
1008            current = candidate;
1009        }
1010
1011        Ok(current)
1012    }
1013
1014    fn resolve_path(&self, path: &str, depth: usize) -> VfsResult<String> {
1015        self.resolve_path_with_options(path, true, depth)
1016    }
1017
1018    fn resolve_exact_path(&self, path: &str) -> VfsResult<String> {
1019        self.resolve_path_with_options(path, false, 0)
1020    }
1021
1022    fn inode_id_for_existing_path(
1023        &self,
1024        path: &str,
1025        op: &'static str,
1026        follow_symlinks: bool,
1027    ) -> VfsResult<u64> {
1028        let normalized = normalize_path(path);
1029        let resolved = if follow_symlinks {
1030            self.resolve_path(&normalized, 0)?
1031        } else {
1032            self.resolve_exact_path(&normalized)?
1033        };
1034        self.path_index
1035            .get(&resolved)
1036            .copied()
1037            .ok_or_else(|| VfsError::not_found(op, path))
1038    }
1039
1040    fn inode_for_existing_path(
1041        &self,
1042        path: &str,
1043        op: &'static str,
1044        follow_symlinks: bool,
1045    ) -> VfsResult<&Inode> {
1046        let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
1047        Ok(self
1048            .inodes
1049            .get(&ino)
1050            .expect("existing path should resolve to a live inode"))
1051    }
1052
1053    fn inode_mut_for_existing_path(
1054        &mut self,
1055        path: &str,
1056        op: &'static str,
1057        follow_symlinks: bool,
1058    ) -> VfsResult<&mut Inode> {
1059        let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
1060        Ok(self
1061            .inodes
1062            .get_mut(&ino)
1063            .expect("existing path should resolve to a live inode"))
1064    }
1065
1066    fn assert_directory_path(&self, path: &str, op: &'static str) -> VfsResult<()> {
1067        let inode = self.inode_for_existing_path(path, op, true)?;
1068        if matches!(inode.kind, InodeKind::Directory) {
1069            Ok(())
1070        } else {
1071            Err(VfsError::not_directory(op, path))
1072        }
1073    }
1074
1075    fn remove_exact_path(&mut self, path: &str) -> VfsResult<()> {
1076        let normalized = self.resolve_exact_path(path)?;
1077        let ino = self
1078            .path_index
1079            .get(&normalized)
1080            .copied()
1081            .ok_or_else(|| VfsError::not_found("unlink", path))?;
1082        let inode = self
1083            .inodes
1084            .get(&ino)
1085            .expect("existing path should resolve to a live inode");
1086
1087        if matches!(inode.kind, InodeKind::Directory) {
1088            return Err(VfsError::is_directory("unlink", path));
1089        }
1090
1091        self.inodes
1092            .get_mut(&ino)
1093            .expect("inode should exist when unlinking")
1094            .metadata
1095            .ctime_ms = now_ms();
1096        self.path_index.remove(&normalized);
1097        self.decrement_link_count(ino);
1098        Ok(())
1099    }
1100
1101    fn remove_existing_destination(&mut self, path: &str) -> VfsResult<()> {
1102        let normalized = self.resolve_exact_path(path)?;
1103        let Some(ino) = self.path_index.get(&normalized).copied() else {
1104            return Ok(());
1105        };
1106
1107        let inode = self
1108            .inodes
1109            .get(&ino)
1110            .expect("existing path should resolve to a live inode");
1111
1112        if matches!(inode.kind, InodeKind::Directory) {
1113            let prefix = format!("{normalized}/");
1114            if self
1115                .path_index
1116                .keys()
1117                .any(|candidate| candidate.starts_with(&prefix))
1118            {
1119                return Err(VfsError::not_empty(path));
1120            }
1121        }
1122
1123        self.inodes
1124            .get_mut(&ino)
1125            .expect("inode should exist when removing destination")
1126            .metadata
1127            .ctime_ms = now_ms();
1128        self.path_index.remove(&normalized);
1129        self.decrement_link_count(ino);
1130        Ok(())
1131    }
1132
1133    fn decrement_link_count(&mut self, ino: u64) {
1134        let should_remove = {
1135            let inode = self
1136                .inodes
1137                .get_mut(&ino)
1138                .expect("inode should exist when decrementing link count");
1139            inode.metadata.nlink = inode.metadata.nlink.saturating_sub(1);
1140            inode.metadata.nlink == 0
1141        };
1142
1143        if should_remove {
1144            self.inodes.remove(&ino);
1145        }
1146    }
1147
1148    fn build_stat(&self, inode: &Inode) -> VirtualStat {
1149        let size = match &inode.kind {
1150            InodeKind::File { data } => data.len() as u64,
1151            InodeKind::Directory => DIRECTORY_SIZE,
1152            InodeKind::SymbolicLink { target } => target.len() as u64,
1153            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1154                0
1155            }
1156        };
1157
1158        VirtualStat {
1159            mode: inode.metadata.mode,
1160            size,
1161            blocks: allocated_block_count(&inode.metadata.allocated_extents),
1162            dev: self.device_id,
1163            rdev: match inode.kind {
1164                InodeKind::CharacterDevice { rdev } => rdev,
1165                InodeKind::BlockDevice { rdev } => rdev,
1166                _ => 0,
1167            },
1168            is_directory: matches!(inode.kind, InodeKind::Directory),
1169            is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
1170            atime_ms: inode.metadata.atime_ms,
1171            atime_nsec: inode.metadata.atime_nsec,
1172            mtime_ms: inode.metadata.mtime_ms,
1173            mtime_nsec: inode.metadata.mtime_nsec,
1174            ctime_ms: inode.metadata.ctime_ms,
1175            ctime_nsec: inode.metadata.ctime_nsec,
1176            birthtime_ms: inode.metadata.birthtime_ms,
1177            ino: inode.metadata.ino,
1178            nlink: inode.metadata.nlink,
1179            uid: inode.metadata.uid,
1180            gid: inode.metadata.gid,
1181        }
1182    }
1183
1184    /// Clones the full in-memory filesystem state.
1185    ///
1186    /// Callers that expose snapshots outside the kernel must enforce their own
1187    /// byte and inode limits before reaching this raw clone operation.
1188    pub fn snapshot(&self) -> MemoryFileSystemSnapshot {
1189        MemoryFileSystemSnapshot {
1190            path_index: self.path_index.clone(),
1191            inodes: self
1192                .inodes
1193                .iter()
1194                .map(|(ino, inode)| {
1195                    (
1196                        *ino,
1197                        MemoryFileSystemSnapshotInode {
1198                            metadata: MemoryFileSystemSnapshotMetadata {
1199                                mode: inode.metadata.mode,
1200                                uid: inode.metadata.uid,
1201                                gid: inode.metadata.gid,
1202                                nlink: inode.metadata.nlink,
1203                                ino: inode.metadata.ino,
1204                                atime_ms: inode.metadata.atime_ms,
1205                                atime_nsec: inode.metadata.atime_nsec,
1206                                mtime_ms: inode.metadata.mtime_ms,
1207                                mtime_nsec: inode.metadata.mtime_nsec,
1208                                ctime_ms: inode.metadata.ctime_ms,
1209                                ctime_nsec: inode.metadata.ctime_nsec,
1210                                birthtime_ms: inode.metadata.birthtime_ms,
1211                                allocated_extents: inode.metadata.allocated_extents.clone(),
1212                                unwritten_extents: inode.metadata.unwritten_extents.clone(),
1213                                xattrs: inode.metadata.xattrs.clone(),
1214                            },
1215                            kind: match &inode.kind {
1216                                InodeKind::File { data } => {
1217                                    MemoryFileSystemSnapshotInodeKind::File { data: data.clone() }
1218                                }
1219                                InodeKind::Directory => {
1220                                    MemoryFileSystemSnapshotInodeKind::Directory
1221                                }
1222                                InodeKind::SymbolicLink { target } => {
1223                                    MemoryFileSystemSnapshotInodeKind::SymbolicLink {
1224                                        target: target.clone(),
1225                                    }
1226                                }
1227                                InodeKind::CharacterDevice { rdev } => {
1228                                    MemoryFileSystemSnapshotInodeKind::CharacterDevice {
1229                                        rdev: *rdev,
1230                                    }
1231                                }
1232                                InodeKind::BlockDevice { rdev } => {
1233                                    MemoryFileSystemSnapshotInodeKind::BlockDevice { rdev: *rdev }
1234                                }
1235                                InodeKind::Fifo => MemoryFileSystemSnapshotInodeKind::Fifo,
1236                            },
1237                        },
1238                    )
1239                })
1240                .collect(),
1241            next_ino: self.next_ino,
1242        }
1243    }
1244
1245    pub fn from_snapshot(snapshot: MemoryFileSystemSnapshot) -> Self {
1246        Self {
1247            device_id: allocate_memory_filesystem_device_id(),
1248            path_index: snapshot.path_index,
1249            inodes: snapshot
1250                .inodes
1251                .into_iter()
1252                .map(|(ino, inode)| {
1253                    (
1254                        ino,
1255                        Inode {
1256                            metadata: Metadata {
1257                                mode: inode.metadata.mode,
1258                                uid: inode.metadata.uid,
1259                                gid: inode.metadata.gid,
1260                                nlink: inode.metadata.nlink,
1261                                ino: inode.metadata.ino,
1262                                atime_ms: inode.metadata.atime_ms,
1263                                atime_nsec: inode.metadata.atime_nsec,
1264                                mtime_ms: inode.metadata.mtime_ms,
1265                                mtime_nsec: inode.metadata.mtime_nsec,
1266                                ctime_ms: inode.metadata.ctime_ms,
1267                                ctime_nsec: inode.metadata.ctime_nsec,
1268                                birthtime_ms: inode.metadata.birthtime_ms,
1269                                allocated_extents: inode.metadata.allocated_extents,
1270                                unwritten_extents: inode.metadata.unwritten_extents,
1271                                xattrs: inode.metadata.xattrs,
1272                            },
1273                            kind: match inode.kind {
1274                                MemoryFileSystemSnapshotInodeKind::File { data } => {
1275                                    InodeKind::File { data }
1276                                }
1277                                MemoryFileSystemSnapshotInodeKind::Directory => {
1278                                    InodeKind::Directory
1279                                }
1280                                MemoryFileSystemSnapshotInodeKind::SymbolicLink { target } => {
1281                                    InodeKind::SymbolicLink { target }
1282                                }
1283                                MemoryFileSystemSnapshotInodeKind::CharacterDevice { rdev } => {
1284                                    InodeKind::CharacterDevice { rdev }
1285                                }
1286                                MemoryFileSystemSnapshotInodeKind::BlockDevice { rdev } => {
1287                                    InodeKind::BlockDevice { rdev }
1288                                }
1289                                MemoryFileSystemSnapshotInodeKind::Fifo => InodeKind::Fifo,
1290                            },
1291                        },
1292                    )
1293                })
1294                .collect(),
1295            next_ino: snapshot.next_ino,
1296        }
1297    }
1298}
1299
1300impl VirtualFileSystem for MemoryFileSystem {
1301    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
1302        let inode = self.inode_mut_for_existing_path(path, "open", true)?;
1303        match &inode.kind {
1304            InodeKind::File { data } => {
1305                inode.metadata.atime_ms = now_ms();
1306                Ok(data.clone())
1307            }
1308            InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1309            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1310            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1311                Err(VfsError::new(
1312                    "ENXIO",
1313                    format!("device I/O requires kernel dispatch: {path}"),
1314                ))
1315            }
1316        }
1317    }
1318
1319    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
1320        Ok(self
1321            .read_dir_with_types(path)?
1322            .into_iter()
1323            .map(|entry| entry.name)
1324            .collect())
1325    }
1326
1327    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
1328        self.read_dir_filtered_limited(path, max_entries, |_| true)
1329    }
1330
1331    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1332        self.assert_directory_path(path, "scandir")?;
1333        let resolved = self.resolve_path(path, 0)?;
1334        self.inode_mut_for_existing_path(&resolved, "scandir", false)?
1335            .metadata
1336            .atime_ms = now_ms();
1337        let prefix = if resolved == "/" {
1338            String::from("/")
1339        } else {
1340            format!("{resolved}/")
1341        };
1342
1343        let mut entries = BTreeMap::<String, VirtualDirEntry>::new();
1344        for (candidate_path, ino) in self.path_index.range(prefix.clone()..) {
1345            if !candidate_path.starts_with(&prefix) {
1346                break;
1347            }
1348
1349            let rest = &candidate_path[prefix.len()..];
1350            if rest.is_empty() || rest.contains('/') {
1351                continue;
1352            }
1353
1354            let inode = self
1355                .inodes
1356                .get(ino)
1357                .expect("path index should always point at a valid inode");
1358            entries.insert(
1359                String::from(rest),
1360                VirtualDirEntry {
1361                    name: String::from(rest),
1362                    is_directory: matches!(inode.kind, InodeKind::Directory),
1363                    is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
1364                },
1365            );
1366        }
1367
1368        Ok(entries.into_values().collect())
1369    }
1370
1371    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1372        let normalized = self.resolve_path(path, 0)?;
1373        self.mkdir(&dirname(&normalized), true)?;
1374        let data = content.into();
1375
1376        if self.path_index.contains_key(&normalized) {
1377            let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
1378            let now = now_ms();
1379            match &mut inode.kind {
1380                InodeKind::File { data: existing } => {
1381                    *existing = data;
1382                    inode.metadata.allocated_extents = dense_allocation(existing.len() as u64);
1383                    inode.metadata.unwritten_extents.clear();
1384                    inode.metadata.mtime_ms = now;
1385                    inode.metadata.ctime_ms = now;
1386                    return Ok(());
1387                }
1388                InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
1389                InodeKind::SymbolicLink { .. } => return Err(VfsError::not_found("open", path)),
1390                InodeKind::CharacterDevice { .. }
1391                | InodeKind::BlockDevice { .. }
1392                | InodeKind::Fifo => {
1393                    return Err(VfsError::new(
1394                        "ENXIO",
1395                        format!("device write requires kernel dispatch: {path}"),
1396                    ))
1397                }
1398            }
1399        }
1400
1401        let ino = self.allocate_inode(InodeKind::File { data }, S_IFREG | 0o644);
1402        self.path_index.insert(normalized, ino);
1403        Ok(())
1404    }
1405
1406    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1407        let normalized = self.resolve_path(path, 0)?;
1408        self.mkdir(&dirname(&normalized), true)?;
1409        if self.path_index.contains_key(&normalized) {
1410            return Err(VfsError::already_exists("open", path));
1411        }
1412
1413        let ino = self.allocate_inode(
1414            InodeKind::File {
1415                data: content.into(),
1416            },
1417            S_IFREG | 0o644,
1418        );
1419        self.path_index.insert(normalized, ino);
1420        Ok(())
1421    }
1422
1423    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1424        let normalized = self.resolve_path(path, 0)?;
1425        let data = content.into();
1426        let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
1427        let now = now_ms();
1428        match &mut inode.kind {
1429            InodeKind::File { data: existing } => {
1430                let offset = existing.len() as u64;
1431                reserve_file_growth(existing, data.len())?;
1432                existing.extend_from_slice(&data);
1433                allocate_range(
1434                    &mut inode.metadata.allocated_extents,
1435                    offset,
1436                    data.len() as u64,
1437                );
1438                remove_extent_range(
1439                    &mut inode.metadata.unwritten_extents,
1440                    offset,
1441                    data.len() as u64,
1442                );
1443                inode.metadata.mtime_ms = now;
1444                inode.metadata.ctime_ms = now;
1445                Ok(existing.len() as u64)
1446            }
1447            InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1448            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1449            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1450                Err(VfsError::new(
1451                    "ENXIO",
1452                    format!("device I/O requires kernel dispatch: {path}"),
1453                ))
1454            }
1455        }
1456    }
1457
1458    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1459        let normalized = self.resolve_exact_path(path)?;
1460        if normalized == "/" {
1461            return Ok(());
1462        }
1463
1464        self.assert_directory_path(&dirname(&normalized), "mkdir")?;
1465        if let Some(existing) = self.path_index.get(&normalized) {
1466            let inode = self
1467                .inodes
1468                .get(existing)
1469                .expect("path index should always point at a valid inode");
1470            if matches!(inode.kind, InodeKind::Directory) {
1471                return Ok(());
1472            }
1473            return Err(VfsError::already_exists("mkdir", path));
1474        }
1475
1476        let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1477        self.path_index.insert(normalized, ino);
1478        Ok(())
1479    }
1480
1481    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1482        let normalized = normalize_path(path);
1483        if normalized == "/" {
1484            return Ok(());
1485        }
1486
1487        if !recursive {
1488            return self.create_dir(path);
1489        }
1490
1491        let parts: Vec<&str> = normalized
1492            .split('/')
1493            .filter(|part| !part.is_empty())
1494            .collect();
1495        let mut current = String::from("/");
1496
1497        for (index, part) in parts.iter().enumerate() {
1498            let raw_path = if current == "/" {
1499                format!("/{}", part)
1500            } else {
1501                format!("{current}/{}", part)
1502            };
1503            let resolved =
1504                self.resolve_path_with_options(&raw_path, index + 1 != parts.len(), 0)?;
1505
1506            match self.path_index.get(&resolved).copied() {
1507                Some(ino) => {
1508                    let inode = self
1509                        .inodes
1510                        .get(&ino)
1511                        .expect("path index should always point at a valid inode");
1512                    if !matches!(inode.kind, InodeKind::Directory) {
1513                        return Err(VfsError::not_directory("mkdir", &raw_path));
1514                    }
1515                }
1516                None => {
1517                    let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1518                    self.path_index.insert(resolved.clone(), ino);
1519                }
1520            }
1521
1522            current = resolved;
1523        }
1524
1525        Ok(())
1526    }
1527
1528    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
1529        let normalized = self.resolve_path(path, 0)?;
1530        self.mkdir(&dirname(&normalized), true)?;
1531        if self.path_index.contains_key(&normalized) {
1532            return Err(VfsError::already_exists("mknod", path));
1533        }
1534        let (kind, type_mode) = match mode & 0o170000 {
1535            S_IFCHR => (InodeKind::CharacterDevice { rdev }, S_IFCHR),
1536            S_IFBLK => (InodeKind::BlockDevice { rdev }, S_IFBLK),
1537            S_IFIFO => (InodeKind::Fifo, S_IFIFO),
1538            _ => return Err(VfsError::invalid_input("unsupported special inode type")),
1539        };
1540        let ino = self.allocate_inode(kind, type_mode | (mode & 0o7777));
1541        self.path_index.insert(normalized, ino);
1542        Ok(())
1543    }
1544
1545    fn exists(&self, path: &str) -> bool {
1546        self.resolve_path(path, 0)
1547            .ok()
1548            .is_some_and(|resolved| self.path_index.contains_key(&resolved))
1549    }
1550
1551    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1552        let inode = self.inode_for_existing_path(path, "stat", true)?;
1553        Ok(self.build_stat(inode))
1554    }
1555
1556    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1557        self.remove_exact_path(path)
1558    }
1559
1560    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1561        let normalized = self.resolve_exact_path(path)?;
1562        if normalized == "/" {
1563            return Err(VfsError::permission_denied("rmdir", path));
1564        }
1565
1566        let ino = self
1567            .path_index
1568            .get(&normalized)
1569            .copied()
1570            .ok_or_else(|| VfsError::not_found("rmdir", path))?;
1571        let inode = self
1572            .inodes
1573            .get(&ino)
1574            .expect("path index should always point at a valid inode");
1575        if !matches!(inode.kind, InodeKind::Directory) {
1576            return Err(VfsError::not_directory("rmdir", path));
1577        }
1578
1579        let prefix = format!("{normalized}/");
1580        if self
1581            .path_index
1582            .keys()
1583            .any(|candidate| candidate.starts_with(&prefix))
1584        {
1585            return Err(VfsError::not_empty(path));
1586        }
1587
1588        self.path_index.remove(&normalized);
1589        self.decrement_link_count(ino);
1590        Ok(())
1591    }
1592
1593    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1594        let old_normalized = self.resolve_exact_path(old_path)?;
1595        let new_normalized = self.resolve_exact_path(new_path)?;
1596
1597        if old_normalized == "/" {
1598            return Err(VfsError::permission_denied("rename", old_path));
1599        }
1600
1601        if old_normalized == new_normalized {
1602            return Ok(());
1603        }
1604
1605        self.assert_directory_path(&dirname(&new_normalized), "rename")?;
1606
1607        if new_normalized.starts_with(&(old_normalized.clone() + "/")) {
1608            return Err(VfsError::invalid_input(format!(
1609                "cannot move '{}' into its own descendant '{}'",
1610                old_path, new_path
1611            )));
1612        }
1613
1614        let ino = self
1615            .path_index
1616            .get(&old_normalized)
1617            .copied()
1618            .ok_or_else(|| VfsError::not_found("rename", old_path))?;
1619        let is_directory = matches!(
1620            self.inodes
1621                .get(&ino)
1622                .expect("path index should always point at a valid inode")
1623                .kind,
1624            InodeKind::Directory
1625        );
1626
1627        if let Some(destination_ino) = self.path_index.get(&new_normalized).copied() {
1628            let destination_is_directory = matches!(
1629                self.inodes
1630                    .get(&destination_ino)
1631                    .expect("destination path should point at a valid inode")
1632                    .kind,
1633                InodeKind::Directory
1634            );
1635            match (is_directory, destination_is_directory) {
1636                (false, true) => return Err(VfsError::is_directory("rename", new_path)),
1637                (true, false) => return Err(VfsError::not_directory("rename", new_path)),
1638                _ => {}
1639            }
1640        }
1641
1642        self.remove_existing_destination(new_path)?;
1643
1644        if !is_directory {
1645            self.path_index.remove(&old_normalized);
1646            self.path_index.insert(new_normalized, ino);
1647            self.inodes
1648                .get_mut(&ino)
1649                .expect("renamed inode should exist")
1650                .metadata
1651                .ctime_ms = now_ms();
1652            return Ok(());
1653        }
1654
1655        let prefix = format!("{old_normalized}/");
1656        let to_move: Vec<(String, u64)> = self
1657            .path_index
1658            .iter()
1659            .filter(|(path, _)| **path == old_normalized || path.starts_with(&prefix))
1660            .map(|(path, inode_id)| (path.clone(), *inode_id))
1661            .collect();
1662
1663        for (path, _) in &to_move {
1664            self.path_index.remove(path);
1665        }
1666
1667        for (path, inode_id) in to_move {
1668            let relocated_path = if path == old_normalized {
1669                new_normalized.clone()
1670            } else {
1671                format!("{new_normalized}{}", &path[old_normalized.len()..])
1672            };
1673            self.path_index.insert(relocated_path, inode_id);
1674        }
1675
1676        self.inodes
1677            .get_mut(&ino)
1678            .expect("renamed directory inode should exist")
1679            .metadata
1680            .ctime_ms = now_ms();
1681
1682        Ok(())
1683    }
1684
1685    fn realpath(&self, path: &str) -> VfsResult<String> {
1686        let resolved = self.resolve_path(path, 0)?;
1687        if !self.path_index.contains_key(&resolved) {
1688            return Err(VfsError::not_found("realpath", path));
1689        }
1690        Ok(resolved)
1691    }
1692
1693    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1694        self.symlink_with_metadata(target, link_path, S_IFLNK | 0o777, DEFAULT_UID, DEFAULT_GID)
1695    }
1696
1697    fn read_link(&self, path: &str) -> VfsResult<String> {
1698        let inode = self.inode_for_existing_path(path, "readlink", false)?;
1699        match &inode.kind {
1700            InodeKind::SymbolicLink { target } => Ok(target.clone()),
1701            _ => Err(VfsError::invalid_input(format!(
1702                "invalid argument, readlink '{path}'"
1703            ))),
1704        }
1705    }
1706
1707    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1708        let inode = self.inode_for_existing_path(path, "lstat", false)?;
1709        Ok(self.build_stat(inode))
1710    }
1711
1712    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1713        let ino = self.inode_id_for_existing_path(old_path, "link", true)?;
1714        let inode = self
1715            .inodes
1716            .get(&ino)
1717            .expect("path index should always point at a valid inode");
1718        if !matches!(inode.kind, InodeKind::File { .. }) {
1719            return Err(VfsError::permission_denied("link", old_path));
1720        }
1721
1722        let normalized = self.resolve_exact_path(new_path)?;
1723        if self.path_index.contains_key(&normalized) {
1724            return Err(VfsError::already_exists("link", new_path));
1725        }
1726
1727        self.assert_directory_path(&dirname(&normalized), "link")?;
1728        self.path_index.insert(normalized, ino);
1729        let inode = self
1730            .inodes
1731            .get_mut(&ino)
1732            .expect("path index should always point at a valid inode");
1733        inode.metadata.nlink += 1;
1734        inode.metadata.ctime_ms = now_ms();
1735        Ok(())
1736    }
1737
1738    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1739        let inode = self.inode_mut_for_existing_path(path, "chmod", true)?;
1740        let type_bits = if mode & 0o170000 == 0 {
1741            inode.metadata.mode & 0o170000
1742        } else {
1743            mode & 0o170000
1744        };
1745        inode.metadata.mode = type_bits | (mode & 0o7777);
1746        inode.metadata.ctime_ms = now_ms();
1747        Ok(())
1748    }
1749
1750    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1751        let inode = self.inode_mut_for_existing_path(path, "chown", true)?;
1752        inode.metadata.uid = uid;
1753        inode.metadata.gid = gid;
1754        inode.metadata.ctime_ms = now_ms();
1755        Ok(())
1756    }
1757
1758    fn chown_spec(
1759        &mut self,
1760        path: &str,
1761        uid: u32,
1762        gid: u32,
1763        follow_symlinks: bool,
1764    ) -> VfsResult<()> {
1765        let inode = self.inode_mut_for_existing_path(path, "chown", follow_symlinks)?;
1766        inode.metadata.uid = uid;
1767        inode.metadata.gid = gid;
1768        inode.metadata.ctime_ms = now_ms();
1769        Ok(())
1770    }
1771
1772    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1773        let inode = self.inode_mut_for_existing_path(path, "lchown", false)?;
1774        inode.metadata.uid = uid;
1775        inode.metadata.gid = gid;
1776        inode.metadata.ctime_ms = now_ms();
1777        Ok(())
1778    }
1779
1780    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
1781        validate_xattr_name(name)?;
1782        self.inode_for_existing_path(path, "getxattr", follow_symlinks)?
1783            .metadata
1784            .xattrs
1785            .get(name)
1786            .cloned()
1787            .ok_or_else(|| {
1788                VfsError::new(
1789                    "ENODATA",
1790                    format!("extended attribute does not exist: {name}"),
1791                )
1792            })
1793    }
1794
1795    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
1796        Ok(self
1797            .inode_for_existing_path(path, "listxattr", follow_symlinks)?
1798            .metadata
1799            .xattrs
1800            .keys()
1801            .cloned()
1802            .collect())
1803    }
1804
1805    fn set_xattr(
1806        &mut self,
1807        path: &str,
1808        name: &str,
1809        value: Vec<u8>,
1810        flags: u32,
1811        follow_symlinks: bool,
1812    ) -> VfsResult<()> {
1813        let inode = self.inode_mut_for_existing_path(path, "setxattr", follow_symlinks)?;
1814        set_xattr_value(&mut inode.metadata.xattrs, name, &value, flags)?;
1815        inode.metadata.ctime_ms = now_ms();
1816        Ok(())
1817    }
1818
1819    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
1820        validate_xattr_name(name)?;
1821        let inode = self.inode_mut_for_existing_path(path, "removexattr", follow_symlinks)?;
1822        if inode.metadata.xattrs.remove(name).is_none() {
1823            return Err(VfsError::new(
1824                "ENODATA",
1825                format!("extended attribute does not exist: {name}"),
1826            ));
1827        }
1828        inode.metadata.ctime_ms = now_ms();
1829        Ok(())
1830    }
1831
1832    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1833        let inode = self.inode_mut_for_existing_path(path, "utimes", true)?;
1834        inode.metadata.atime_ms = atime_ms;
1835        inode.metadata.atime_nsec = 0;
1836        inode.metadata.mtime_ms = mtime_ms;
1837        inode.metadata.mtime_nsec = 0;
1838        inode.metadata.ctime_ms = now_ms();
1839        inode.metadata.ctime_nsec = 0;
1840        Ok(())
1841    }
1842
1843    fn utimes_spec(
1844        &mut self,
1845        path: &str,
1846        atime: VirtualUtimeSpec,
1847        mtime: VirtualUtimeSpec,
1848        follow_symlinks: bool,
1849    ) -> VfsResult<()> {
1850        let stat = if follow_symlinks {
1851            self.stat(path)?
1852        } else {
1853            self.lstat(path)?
1854        };
1855        let inode = self.inode_mut_for_existing_path(path, "utimes", follow_symlinks)?;
1856        let now = now_time_spec();
1857        let atime = resolve_utime_spec(
1858            atime,
1859            now,
1860            VirtualTimeSpec {
1861                sec: (stat.atime_ms / 1_000) as i64,
1862                nsec: stat.atime_nsec,
1863            },
1864        )?;
1865        let mtime = resolve_utime_spec(
1866            mtime,
1867            now,
1868            VirtualTimeSpec {
1869                sec: (stat.mtime_ms / 1_000) as i64,
1870                nsec: stat.mtime_nsec,
1871            },
1872        )?;
1873        inode.metadata.atime_ms = atime.to_truncated_millis()?;
1874        inode.metadata.atime_nsec = atime.nsec;
1875        inode.metadata.mtime_ms = mtime.to_truncated_millis()?;
1876        inode.metadata.mtime_nsec = mtime.nsec;
1877        let ctime = now_time_spec();
1878        inode.metadata.ctime_ms = ctime.to_truncated_millis()?;
1879        inode.metadata.ctime_nsec = ctime.nsec;
1880        Ok(())
1881    }
1882
1883    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1884        let inode = self.inode_mut_for_existing_path(path, "truncate", true)?;
1885        let now = now_ms();
1886        match &mut inode.kind {
1887            InodeKind::File { data } => {
1888                resize_file_data(data, checked_file_len(length, "truncate length")?)?;
1889                truncate_allocation(&mut inode.metadata.allocated_extents, length);
1890                truncate_allocation(&mut inode.metadata.unwritten_extents, length);
1891                inode.metadata.mtime_ms = now;
1892                inode.metadata.ctime_ms = now;
1893                Ok(())
1894            }
1895            InodeKind::Directory => Err(VfsError::is_directory("truncate", path)),
1896            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("truncate", path)),
1897            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1898                Err(VfsError::new(
1899                    "ENXIO",
1900                    format!("cannot truncate character device: {path}"),
1901                ))
1902            }
1903        }
1904    }
1905
1906    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1907        let end = offset
1908            .checked_add(length)
1909            .ok_or_else(|| VfsError::new("EINVAL", "allocation range overflows"))?;
1910        if length == 0 {
1911            return Ok(());
1912        }
1913        let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1914        match &mut inode.kind {
1915            InodeKind::File { data } => {
1916                let end_len = checked_file_len(end, "allocation range end")?;
1917                if end_len > data.len() {
1918                    resize_file_data(data, end_len)?;
1919                }
1920                allocate_unwritten_holes(
1921                    &mut inode.metadata.unwritten_extents,
1922                    &inode.metadata.allocated_extents,
1923                    offset,
1924                    length,
1925                );
1926                allocate_range(&mut inode.metadata.allocated_extents, offset, length);
1927                let now = now_ms();
1928                inode.metadata.mtime_ms = now;
1929                inode.metadata.ctime_ms = now;
1930                Ok(())
1931            }
1932            InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
1933            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
1934            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1935                Err(VfsError::new(
1936                    "ENXIO",
1937                    format!("cannot allocate character device: {path}"),
1938                ))
1939            }
1940        }
1941    }
1942
1943    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1944        validate_shift_range(offset, length)?;
1945        let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1946        match &mut inode.kind {
1947            InodeKind::File { data } => {
1948                let start = checked_file_len(offset, "insert range offset")?;
1949                let insert_len = checked_file_len(length, "insert range length")?;
1950                if start >= data.len() {
1951                    return Err(VfsError::new(
1952                        "EINVAL",
1953                        "insert range offset must be before EOF",
1954                    ));
1955                }
1956                data.splice(start..start, std::iter::repeat_n(0, insert_len));
1957                inode.metadata.allocated_extents =
1958                    allocation_after_insert(&inode.metadata.allocated_extents, offset, length);
1959                inode.metadata.unwritten_extents =
1960                    allocation_after_insert(&inode.metadata.unwritten_extents, offset, length);
1961                let now = now_ms();
1962                inode.metadata.mtime_ms = now;
1963                inode.metadata.ctime_ms = now;
1964                Ok(())
1965            }
1966            InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
1967            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
1968            _ => Err(VfsError::new(
1969                "ENXIO",
1970                "cannot insert range on special inode",
1971            )),
1972        }
1973    }
1974
1975    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1976        validate_shift_range(offset, length)?;
1977        let end = offset
1978            .checked_add(length)
1979            .ok_or_else(|| VfsError::new("EINVAL", "collapse range overflows"))?;
1980        let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1981        match &mut inode.kind {
1982            InodeKind::File { data } => {
1983                if end >= data.len() as u64 {
1984                    return Err(VfsError::new(
1985                        "EINVAL",
1986                        "collapse range must end before EOF",
1987                    ));
1988                }
1989                let start = checked_file_len(offset, "collapse range offset")?;
1990                let end = checked_file_len(end, "collapse range end")?;
1991                data.drain(start..end);
1992                inode.metadata.allocated_extents =
1993                    allocation_after_collapse(&inode.metadata.allocated_extents, offset, length);
1994                inode.metadata.unwritten_extents =
1995                    allocation_after_collapse(&inode.metadata.unwritten_extents, offset, length);
1996                let now = now_ms();
1997                inode.metadata.mtime_ms = now;
1998                inode.metadata.ctime_ms = now;
1999                Ok(())
2000            }
2001            InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2002            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2003            _ => Err(VfsError::new(
2004                "ENXIO",
2005                "cannot collapse range on special inode",
2006            )),
2007        }
2008    }
2009
2010    fn zero_range(
2011        &mut self,
2012        path: &str,
2013        offset: u64,
2014        length: u64,
2015        keep_size: bool,
2016    ) -> VfsResult<()> {
2017        let end = offset
2018            .checked_add(length)
2019            .ok_or_else(|| VfsError::new("EINVAL", "zero range overflows"))?;
2020        if length == 0 {
2021            return Err(VfsError::new("EINVAL", "zero range length must be nonzero"));
2022        }
2023        let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
2024        match &mut inode.kind {
2025            InodeKind::File { data } => {
2026                let original_size = data.len() as u64;
2027                let zero_end = if keep_size {
2028                    end.min(original_size)
2029                } else {
2030                    end
2031                };
2032                if !keep_size {
2033                    let end_len = checked_file_len(end, "zero range end")?;
2034                    if end_len > data.len() {
2035                        resize_file_data(data, end_len)?;
2036                    }
2037                }
2038                let start = checked_file_len(offset.min(zero_end), "zero range offset")?;
2039                let zero_end = checked_file_len(zero_end, "zero range end")?;
2040                data[start..zero_end].fill(0);
2041                mark_zero_unwritten(
2042                    &mut inode.metadata.unwritten_extents,
2043                    &inode.metadata.allocated_extents,
2044                    offset,
2045                    length,
2046                );
2047                allocate_range(&mut inode.metadata.allocated_extents, offset, length);
2048                let now = now_ms();
2049                inode.metadata.mtime_ms = now;
2050                inode.metadata.ctime_ms = now;
2051                Ok(())
2052            }
2053            InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2054            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2055            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2056                Err(VfsError::new("ENXIO", format!("cannot zero range: {path}")))
2057            }
2058        }
2059    }
2060
2061    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2062        let requested_end = offset
2063            .checked_add(length)
2064            .ok_or_else(|| VfsError::new("EINVAL", "hole-punch range overflows"))?;
2065        let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
2066        match &mut inode.kind {
2067            InodeKind::File { data } => {
2068                let start = checked_file_len(offset.min(data.len() as u64), "hole-punch offset")?;
2069                let end =
2070                    checked_file_len(requested_end.min(data.len() as u64), "hole-punch range end")?;
2071                data[start..end].fill(0);
2072                punch_allocation(&mut inode.metadata.allocated_extents, offset, length);
2073                remove_extent_range(&mut inode.metadata.unwritten_extents, offset, length);
2074                let now = now_ms();
2075                inode.metadata.mtime_ms = now;
2076                inode.metadata.ctime_ms = now;
2077                Ok(())
2078            }
2079            InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2080            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2081            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2082                Err(VfsError::new(
2083                    "ENXIO",
2084                    format!("cannot punch character device: {path}"),
2085                ))
2086            }
2087        }
2088    }
2089
2090    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2091        let inode = self.inode_mut_for_existing_path(path, "fiemap", true)?;
2092        match &inode.kind {
2093            InodeKind::File { data } => Ok(allocation_byte_ranges(
2094                &inode.metadata.allocated_extents,
2095                data.len() as u64,
2096            )),
2097            InodeKind::Directory => Err(VfsError::is_directory("fiemap", path)),
2098            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fiemap", path)),
2099            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2100                Ok(Vec::new())
2101            }
2102        }
2103    }
2104
2105    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2106        let inode = self.inode_mut_for_existing_path(path, "fiemap", true)?;
2107        match &inode.kind {
2108            InodeKind::File { data } => Ok(allocation_byte_ranges(
2109                &inode.metadata.unwritten_extents,
2110                data.len() as u64,
2111            )),
2112            InodeKind::Directory => Err(VfsError::is_directory("fiemap", path)),
2113            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fiemap", path)),
2114            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2115                Ok(Vec::new())
2116            }
2117        }
2118    }
2119
2120    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
2121        let inode = self.inode_mut_for_existing_path(path, "open", true)?;
2122        match &mut inode.kind {
2123            InodeKind::File { data } => {
2124                inode.metadata.atime_ms = now_ms();
2125                let start = offset as usize;
2126                if start >= data.len() {
2127                    return Ok(Vec::new());
2128                }
2129                let end = start.saturating_add(length).min(data.len());
2130                Ok(data[start..end].to_vec())
2131            }
2132            InodeKind::Directory => Err(VfsError::is_directory("open", path)),
2133            InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
2134            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2135                Err(VfsError::new(
2136                    "ENXIO",
2137                    format!("device I/O requires kernel dispatch: {path}"),
2138                ))
2139            }
2140        }
2141    }
2142
2143    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
2144        let content = content.into();
2145        let inode = self.inode_mut_for_existing_path(path, "open", true)?;
2146        let data = match &mut inode.kind {
2147            InodeKind::File { data } => data,
2148            InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
2149            InodeKind::SymbolicLink { .. } => {
2150                return Err(VfsError::not_found("open", path));
2151            }
2152            InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2153                return Err(VfsError::new(
2154                    "ENXIO",
2155                    format!("device I/O requires kernel dispatch: {path}"),
2156                ));
2157            }
2158        };
2159        let start = checked_file_len(offset, "pwrite offset")?;
2160        let end = start.checked_add(content.len()).ok_or_else(|| {
2161            VfsError::new(
2162                "ENOMEM",
2163                format!(
2164                    "pwrite result length overflows addressable memory: offset {offset}, content length {}",
2165                    content.len()
2166                ),
2167            )
2168        })?;
2169        if end > data.len() {
2170            resize_file_data(data, end)?;
2171        }
2172        data[start..end].copy_from_slice(&content);
2173        allocate_range(
2174            &mut inode.metadata.allocated_extents,
2175            offset,
2176            content.len() as u64,
2177        );
2178        remove_extent_range(
2179            &mut inode.metadata.unwritten_extents,
2180            offset,
2181            content.len() as u64,
2182        );
2183        let now = now_ms();
2184        inode.metadata.mtime_ms = now;
2185        inode.metadata.ctime_ms = now;
2186        Ok(())
2187    }
2188}
2189
2190impl Default for MemoryFileSystem {
2191    fn default() -> Self {
2192        Self::new()
2193    }
2194}
2195
2196fn resolve_utime_spec(
2197    spec: VirtualUtimeSpec,
2198    now: VirtualTimeSpec,
2199    existing: VirtualTimeSpec,
2200) -> VfsResult<VirtualTimeSpec> {
2201    match spec {
2202        VirtualUtimeSpec::Set(spec) => Ok(spec),
2203        VirtualUtimeSpec::Now => Ok(now),
2204        VirtualUtimeSpec::Omit => Ok(existing),
2205    }
2206}
2207
2208fn resolve_utime_millis(
2209    spec: VirtualUtimeSpec,
2210    now_ms: u64,
2211    existing: Option<VirtualTimeSpec>,
2212) -> VfsResult<u64> {
2213    match spec {
2214        VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis(),
2215        VirtualUtimeSpec::Now => Ok(now_ms),
2216        VirtualUtimeSpec::Omit => existing
2217            .ok_or_else(|| VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata"))?
2218            .to_truncated_millis(),
2219    }
2220}
2221
2222pub fn validate_path(path: &str) -> VfsResult<()> {
2223    if path.as_bytes().contains(&0) {
2224        return Err(VfsError::invalid_input("path contains NUL byte"));
2225    }
2226    let normalized = normalize_path(path);
2227    if normalized.len() > MAX_PATH_LENGTH {
2228        return Err(VfsError::path_too_long(path));
2229    }
2230    Ok(())
2231}
2232
2233pub fn normalize_path(path: &str) -> String {
2234    if path.is_empty() {
2235        return String::from("/");
2236    }
2237
2238    let candidate = if path.starts_with('/') {
2239        path.to_owned()
2240    } else {
2241        format!("/{path}")
2242    };
2243
2244    let mut resolved = Vec::new();
2245    for part in candidate.split('/') {
2246        match part {
2247            "" | "." => {}
2248            ".." => {
2249                resolved.pop();
2250            }
2251            component => resolved.push(component),
2252        }
2253    }
2254
2255    if resolved.is_empty() {
2256        String::from("/")
2257    } else {
2258        format!("/{}", resolved.join("/"))
2259    }
2260}
2261
2262fn dense_allocation(size: u64) -> Vec<(u64, u64)> {
2263    if size == 0 {
2264        Vec::new()
2265    } else {
2266        vec![(0, size.div_ceil(512))]
2267    }
2268}
2269
2270fn allocate_range(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2271    if length == 0 {
2272        return;
2273    }
2274    let start = offset / 512;
2275    let end = offset.saturating_add(length).div_ceil(512);
2276    let mut merged = Vec::with_capacity(extents.len() + 1);
2277    let mut pending = (start, end);
2278    for &(extent_start, extent_end) in extents.iter() {
2279        if extent_end < pending.0 {
2280            merged.push((extent_start, extent_end));
2281        } else if pending.1 < extent_start {
2282            merged.push(pending);
2283            pending = (extent_start, extent_end);
2284        } else {
2285            pending.0 = pending.0.min(extent_start);
2286            pending.1 = pending.1.max(extent_end);
2287        }
2288    }
2289    merged.push(pending);
2290    *extents = merged;
2291}
2292
2293fn remove_extent_range(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2294    if length == 0 {
2295        return;
2296    }
2297    let start = offset / 512;
2298    let end = offset.saturating_add(length).div_ceil(512);
2299    *extents = extents
2300        .iter()
2301        .flat_map(|&(extent_start, extent_end)| {
2302            [
2303                (extent_start, extent_end.min(start)),
2304                (extent_start.max(end), extent_end),
2305            ]
2306            .into_iter()
2307            .filter(|(part_start, part_end)| part_start < part_end)
2308        })
2309        .collect();
2310}
2311
2312fn allocate_unwritten_holes(
2313    unwritten: &mut Vec<(u64, u64)>,
2314    allocated: &[(u64, u64)],
2315    offset: u64,
2316    length: u64,
2317) {
2318    if length == 0 {
2319        return;
2320    }
2321    let start = offset / 512;
2322    let end = offset.saturating_add(length).div_ceil(512);
2323    let mut cursor = start;
2324    for &(allocated_start, allocated_end) in allocated {
2325        if allocated_end <= cursor || allocated_start >= end {
2326            continue;
2327        }
2328        if cursor < allocated_start {
2329            allocate_range(
2330                unwritten,
2331                cursor.saturating_mul(512),
2332                (allocated_start.min(end) - cursor).saturating_mul(512),
2333            );
2334        }
2335        cursor = cursor.max(allocated_end).min(end);
2336        if cursor == end {
2337            return;
2338        }
2339    }
2340    if cursor < end {
2341        allocate_range(
2342            unwritten,
2343            cursor.saturating_mul(512),
2344            (end - cursor).saturating_mul(512),
2345        );
2346    }
2347}
2348
2349fn mark_zero_unwritten(
2350    unwritten: &mut Vec<(u64, u64)>,
2351    allocated: &[(u64, u64)],
2352    offset: u64,
2353    length: u64,
2354) {
2355    allocate_unwritten_holes(unwritten, allocated, offset, length);
2356    let end = offset.saturating_add(length);
2357    let first_full_block = offset.div_ceil(4096);
2358    let past_full_block = end / 4096;
2359    if first_full_block < past_full_block {
2360        allocate_range(
2361            unwritten,
2362            first_full_block * 4096,
2363            (past_full_block - first_full_block) * 4096,
2364        );
2365    }
2366}
2367
2368fn truncate_allocation(extents: &mut Vec<(u64, u64)>, size: u64) {
2369    let end = size.div_ceil(512);
2370    extents.retain_mut(|(start, extent_end)| {
2371        *extent_end = (*extent_end).min(end);
2372        *start < *extent_end
2373    });
2374}
2375
2376fn punch_allocation(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2377    let start = offset.div_ceil(512);
2378    let end = offset.saturating_add(length) / 512;
2379    if start >= end {
2380        return;
2381    }
2382    *extents = extents
2383        .iter()
2384        .flat_map(|&(extent_start, extent_end)| {
2385            let left = (extent_start, extent_end.min(start));
2386            let right = (extent_start.max(end), extent_end);
2387            [left, right]
2388                .into_iter()
2389                .filter(|(part_start, part_end)| part_start < part_end)
2390        })
2391        .collect();
2392}
2393
2394fn validate_shift_range(offset: u64, length: u64) -> VfsResult<()> {
2395    if length == 0 || !offset.is_multiple_of(512) || !length.is_multiple_of(512) {
2396        return Err(VfsError::new(
2397            "EINVAL",
2398            "insert/collapse range requires a nonzero 512-byte-aligned range",
2399        ));
2400    }
2401    Ok(())
2402}
2403
2404fn allocation_after_insert(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
2405    let start = offset / 512;
2406    let shift = length / 512;
2407    normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
2408        if extent_end <= start {
2409            vec![(extent_start, extent_end)]
2410        } else if extent_start >= start {
2411            vec![(extent_start + shift, extent_end + shift)]
2412        } else {
2413            vec![(extent_start, start), (start + shift, extent_end + shift)]
2414        }
2415    }))
2416}
2417
2418fn allocation_after_collapse(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
2419    let start = offset / 512;
2420    let end = start + length / 512;
2421    normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
2422        let mut parts = Vec::with_capacity(2);
2423        if extent_start < start {
2424            parts.push((extent_start, extent_end.min(start)));
2425        }
2426        if extent_end > end {
2427            parts.push((
2428                extent_start.max(end) - (end - start),
2429                extent_end - (end - start),
2430            ));
2431        }
2432        parts
2433    }))
2434}
2435
2436fn normalize_extents(extents: impl IntoIterator<Item = (u64, u64)>) -> Vec<(u64, u64)> {
2437    let mut merged = Vec::<(u64, u64)>::new();
2438    for (start, end) in extents.into_iter().filter(|(start, end)| start < end) {
2439        if let Some(last) = merged.last_mut().filter(|last| start <= last.1) {
2440            last.1 = last.1.max(end);
2441        } else {
2442            merged.push((start, end));
2443        }
2444    }
2445    merged
2446}
2447
2448fn allocated_block_count(extents: &[(u64, u64)]) -> u64 {
2449    extents
2450        .iter()
2451        .map(|(start, end)| end.saturating_sub(*start))
2452        .sum()
2453}
2454
2455fn allocation_byte_ranges(extents: &[(u64, u64)], size: u64) -> Vec<(u64, u64)> {
2456    extents
2457        .iter()
2458        .filter_map(|&(start, end)| {
2459            let start = start.saturating_mul(512).min(size);
2460            let end = end.saturating_mul(512).min(size);
2461            (start < end).then_some((start, end))
2462        })
2463        .collect()
2464}
2465
2466fn checked_file_len(value: u64, description: &'static str) -> VfsResult<usize> {
2467    usize::try_from(value).map_err(|_| {
2468        VfsError::new(
2469            "EINVAL",
2470            format!("{description} exceeds addressable memory: {value}"),
2471        )
2472    })
2473}
2474
2475fn reserve_file_growth(data: &mut Vec<u8>, additional: usize) -> VfsResult<()> {
2476    data.try_reserve(additional).map_err(|error| {
2477        VfsError::new(
2478            "ENOMEM",
2479            format!(
2480                "file growth exceeds addressable memory: current length {}, additional {additional}: {error}",
2481                data.len()
2482            ),
2483        )
2484    })
2485}
2486
2487fn resize_file_data(data: &mut Vec<u8>, new_len: usize) -> VfsResult<()> {
2488    if new_len > data.len() {
2489        reserve_file_growth(data, new_len - data.len())?;
2490    }
2491    data.resize(new_len, 0);
2492    Ok(())
2493}
2494
2495fn dirname(path: &str) -> String {
2496    let normalized = normalize_path(path);
2497    let Some((head, _)) = normalized.rsplit_once('/') else {
2498        return String::from("/");
2499    };
2500
2501    if head.is_empty() {
2502        String::from("/")
2503    } else {
2504        String::from(head)
2505    }
2506}
2507
2508fn now_ms() -> u64 {
2509    SystemTime::now()
2510        .duration_since(UNIX_EPOCH)
2511        .unwrap_or_default()
2512        .as_millis() as u64
2513}
2514
2515fn now_time_spec() -> VirtualTimeSpec {
2516    let now = SystemTime::now()
2517        .duration_since(UNIX_EPOCH)
2518        .unwrap_or_default();
2519    VirtualTimeSpec {
2520        sec: now.as_secs() as i64,
2521        nsec: now.subsec_nanos(),
2522    }
2523}