Skip to main content

vfs/posix/
mount_table.rs

1use super::root_fs::RootFileSystem;
2use super::usage::{FileSystemStats, FileSystemUsage};
3use super::vfs::{
4    VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualUtimeSpec,
5};
6use std::any::Any;
7use std::collections::VecDeque;
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Component, Path};
10use web_time::{SystemTime, UNIX_EPOCH};
11
12const MAX_REALPATH_SYMLINKS: usize = 40;
13
14pub trait MountedFileSystem: Any {
15    fn as_any(&self) -> &dyn Any;
16    fn as_any_mut(&mut self) -> &mut dyn Any;
17    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
18    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
19    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
20        let entries = self.read_dir(path)?;
21        if entries.len() > max_entries {
22            return Err(VfsError::new(
23                "ENOMEM",
24                format!(
25                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
26                ),
27            ));
28        }
29        Ok(entries)
30    }
31    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
32    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()>;
33    fn write_file_with_mode(
34        &mut self,
35        path: &str,
36        content: Vec<u8>,
37        mode: Option<u32>,
38    ) -> VfsResult<()> {
39        let _ = mode;
40        self.write_file(path, content)
41    }
42    fn create_file_exclusive(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
43        if self.exists(path) {
44            return Err(VfsError::new(
45                "EEXIST",
46                format!("file already exists, open '{path}'"),
47            ));
48        }
49        self.write_file(path, content)
50    }
51    fn create_file_exclusive_with_mode(
52        &mut self,
53        path: &str,
54        content: Vec<u8>,
55        mode: Option<u32>,
56    ) -> VfsResult<()> {
57        let _ = mode;
58        self.create_file_exclusive(path, content)
59    }
60    fn append_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<u64> {
61        let mut existing = self.read_file(path)?;
62        existing.extend_from_slice(&content);
63        let new_len = existing.len() as u64;
64        self.write_file(path, existing)?;
65        Ok(new_len)
66    }
67    fn create_dir(&mut self, path: &str) -> VfsResult<()>;
68    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
69        let _ = mode;
70        self.create_dir(path)
71    }
72    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
73    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
74        let _ = (mode, rdev);
75        Err(VfsError::new(
76            "EOPNOTSUPP",
77            format!("special inode creation is not supported for mount path '{path}'"),
78        ))
79    }
80    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
81        let _ = mode;
82        self.mkdir(path, recursive)
83    }
84    fn exists(&self, path: &str) -> bool;
85    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
86    fn remove_file(&mut self, path: &str) -> VfsResult<()>;
87    fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
88    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
89    fn realpath(&self, path: &str) -> VfsResult<String>;
90    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
91    fn read_link(&self, path: &str) -> VfsResult<String>;
92    fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
93    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
94    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
95    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
96    fn chown_spec(
97        &mut self,
98        path: &str,
99        uid: u32,
100        gid: u32,
101        follow_symlinks: bool,
102    ) -> VfsResult<()> {
103        if !follow_symlinks {
104            return Err(VfsError::unsupported(format!(
105                "lchown is not supported for mount path '{path}'"
106            )));
107        }
108        self.chown(path, uid, gid)
109    }
110
111    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
112        self.chown(path, uid, gid)
113    }
114    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
115        let _ = (name, follow_symlinks);
116        Err(VfsError::new(
117            "EOPNOTSUPP",
118            format!("extended attributes are not supported for mount path '{path}'"),
119        ))
120    }
121    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
122        let _ = follow_symlinks;
123        Err(VfsError::new(
124            "EOPNOTSUPP",
125            format!("extended attributes are not supported for mount path '{path}'"),
126        ))
127    }
128    fn set_xattr(
129        &mut self,
130        path: &str,
131        name: &str,
132        value: Vec<u8>,
133        flags: u32,
134        follow_symlinks: bool,
135    ) -> VfsResult<()> {
136        let _ = (name, value, flags, follow_symlinks);
137        Err(VfsError::new(
138            "EOPNOTSUPP",
139            format!("extended attributes are not supported for mount path '{path}'"),
140        ))
141    }
142    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
143        let _ = (name, follow_symlinks);
144        Err(VfsError::new(
145            "EOPNOTSUPP",
146            format!("extended attributes are not supported for mount path '{path}'"),
147        ))
148    }
149    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
150    fn set_atime(&mut self, path: &str, atime_ms: u64) -> VfsResult<()> {
151        let mtime_ms = self.stat(path)?.mtime_ms;
152        self.utimes(path, atime_ms, mtime_ms)
153    }
154    fn utimes_spec(
155        &mut self,
156        path: &str,
157        atime: VirtualUtimeSpec,
158        mtime: VirtualUtimeSpec,
159        follow_symlinks: bool,
160    ) -> VfsResult<()> {
161        if !follow_symlinks {
162            return Err(VfsError::unsupported(format!(
163                "lutimes is not supported for mount path '{path}'"
164            )));
165        }
166        let existing = match (atime, mtime) {
167            (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
168            _ => None,
169        };
170        let now_ms = SystemTime::now()
171            .duration_since(UNIX_EPOCH)
172            .unwrap_or_default()
173            .as_millis() as u64;
174        let atime_ms = match atime {
175            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
176            VirtualUtimeSpec::Now => now_ms,
177            VirtualUtimeSpec::Omit => {
178                existing
179                    .as_ref()
180                    .ok_or_else(|| {
181                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
182                    })?
183                    .atime_ms
184            }
185        };
186        let mtime_ms = match mtime {
187            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
188            VirtualUtimeSpec::Now => now_ms,
189            VirtualUtimeSpec::Omit => {
190                existing
191                    .as_ref()
192                    .ok_or_else(|| {
193                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
194                    })?
195                    .mtime_ms
196            }
197        };
198        self.utimes(path, atime_ms, mtime_ms)
199    }
200    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
201    fn sync(&mut self, _path: &str) -> VfsResult<()> {
202        Ok(())
203    }
204    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
205        let end = offset
206            .checked_add(length)
207            .ok_or_else(|| VfsError::new("EINVAL", "allocation range overflows"))?;
208        if length == 0 {
209            return Ok(());
210        }
211        let stat = self.stat(path)?;
212        if end > stat.size {
213            self.truncate(path, end)?;
214        }
215        let mut cursor = offset;
216        while cursor < end {
217            let chunk_len = (end - cursor).min(64 * 1024) as usize;
218            let mut bytes = self.pread(path, cursor, chunk_len)?;
219            bytes.resize(chunk_len, 0);
220            self.pwrite(path, bytes, cursor)?;
221            cursor += chunk_len as u64;
222        }
223        Ok(())
224    }
225    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()>;
226    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()>;
227    fn zero_range(
228        &mut self,
229        path: &str,
230        offset: u64,
231        length: u64,
232        keep_size: bool,
233    ) -> VfsResult<()> {
234        let end = offset
235            .checked_add(length)
236            .ok_or_else(|| VfsError::new("EINVAL", "zero range overflows"))?;
237        if length == 0 {
238            return Err(VfsError::new("EINVAL", "zero range length must be nonzero"));
239        }
240        let original_size = self.stat(path)?.size;
241        self.allocate(path, offset, length)?;
242        let zero_end = if keep_size {
243            end.min(original_size)
244        } else {
245            end
246        };
247        let mut cursor = offset.min(zero_end);
248        while cursor < zero_end {
249            let chunk_len = (zero_end - cursor).min(64 * 1024) as usize;
250            self.pwrite(path, vec![0; chunk_len], cursor)?;
251            cursor += chunk_len as u64;
252        }
253        if keep_size && self.stat(path)?.size != original_size {
254            self.truncate(path, original_size)?;
255        }
256        Ok(())
257    }
258    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
259        let requested_end = offset
260            .checked_add(length)
261            .ok_or_else(|| VfsError::new("EINVAL", "hole-punch range overflows"))?;
262        let size = self.stat(path)?.size;
263        let end = requested_end.min(size);
264        let mut cursor = offset.min(size);
265        while cursor < end {
266            let chunk_len = (end - cursor).min(64 * 1024) as usize;
267            self.pwrite(path, vec![0; chunk_len], cursor)?;
268            cursor += chunk_len as u64;
269        }
270        Ok(())
271    }
272    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
273        Err(VfsError::new(
274            "EOPNOTSUPP",
275            format!("extent mapping is not supported for {path}"),
276        ))
277    }
278    fn unwritten_ranges(&mut self, _path: &str) -> VfsResult<Vec<(u64, u64)>> {
279        Ok(Vec::new())
280    }
281    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
282    fn pwrite(&mut self, path: &str, content: Vec<u8>, offset: u64) -> VfsResult<()> {
283        let mut existing = self.read_file(path)?;
284        let start = usize::try_from(offset).map_err(|_| {
285            VfsError::new("EINVAL", format!("pwrite offset is too large: {offset}"))
286        })?;
287        if start > existing.len() {
288            existing.resize(start, 0);
289        }
290        let end = start.checked_add(content.len()).ok_or_else(|| {
291            VfsError::new(
292                "EINVAL",
293                format!(
294                    "pwrite range overflows usize: offset={offset}, length={}",
295                    content.len()
296                ),
297            )
298        })?;
299        if end > existing.len() {
300            existing.resize(end, 0);
301        }
302        existing[start..end].copy_from_slice(&content);
303        self.write_file(path, existing)
304    }
305    fn shutdown(&mut self) -> VfsResult<()> {
306        Ok(())
307    }
308}
309
310pub struct MountedVirtualFileSystem<F> {
311    inner: F,
312}
313
314impl<F> MountedVirtualFileSystem<F> {
315    pub fn new(inner: F) -> Self {
316        Self { inner }
317    }
318
319    pub fn inner(&self) -> &F {
320        &self.inner
321    }
322
323    pub fn inner_mut(&mut self) -> &mut F {
324        &mut self.inner
325    }
326}
327
328impl<F> MountedFileSystem for MountedVirtualFileSystem<F>
329where
330    F: VirtualFileSystem + 'static,
331{
332    fn as_any(&self) -> &dyn Any {
333        self
334    }
335
336    fn as_any_mut(&mut self) -> &mut dyn Any {
337        self
338    }
339
340    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
341        VirtualFileSystem::read_file(&mut self.inner, path)
342    }
343
344    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
345        VirtualFileSystem::read_dir(&mut self.inner, path)
346    }
347
348    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
349        VirtualFileSystem::read_dir_limited(&mut self.inner, path, max_entries)
350    }
351
352    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
353        VirtualFileSystem::read_dir_with_types(&mut self.inner, path)
354    }
355
356    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
357        VirtualFileSystem::write_file(&mut self.inner, path, content)
358    }
359
360    fn write_file_with_mode(
361        &mut self,
362        path: &str,
363        content: Vec<u8>,
364        mode: Option<u32>,
365    ) -> VfsResult<()> {
366        VirtualFileSystem::write_file_with_mode(&mut self.inner, path, content, mode)
367    }
368
369    fn create_file_exclusive(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
370        VirtualFileSystem::create_file_exclusive(&mut self.inner, path, content)
371    }
372
373    fn create_file_exclusive_with_mode(
374        &mut self,
375        path: &str,
376        content: Vec<u8>,
377        mode: Option<u32>,
378    ) -> VfsResult<()> {
379        VirtualFileSystem::create_file_exclusive_with_mode(&mut self.inner, path, content, mode)
380    }
381
382    fn append_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<u64> {
383        VirtualFileSystem::append_file(&mut self.inner, path, content)
384    }
385
386    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
387        VirtualFileSystem::create_dir(&mut self.inner, path)
388    }
389
390    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
391        VirtualFileSystem::create_dir_with_mode(&mut self.inner, path, mode)
392    }
393
394    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
395        VirtualFileSystem::mkdir(&mut self.inner, path, recursive)
396    }
397
398    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
399        VirtualFileSystem::mknod(&mut self.inner, path, mode, rdev)
400    }
401
402    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
403        VirtualFileSystem::mkdir_with_mode(&mut self.inner, path, recursive, mode)
404    }
405
406    fn exists(&self, path: &str) -> bool {
407        VirtualFileSystem::exists(&self.inner, path)
408    }
409
410    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
411        VirtualFileSystem::stat(&mut self.inner, path)
412    }
413
414    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
415        VirtualFileSystem::remove_file(&mut self.inner, path)
416    }
417
418    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
419        VirtualFileSystem::remove_dir(&mut self.inner, path)
420    }
421
422    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
423        VirtualFileSystem::rename(&mut self.inner, old_path, new_path)
424    }
425
426    fn realpath(&self, path: &str) -> VfsResult<String> {
427        VirtualFileSystem::realpath(&self.inner, path)
428    }
429
430    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
431        VirtualFileSystem::symlink(&mut self.inner, target, link_path)
432    }
433
434    fn read_link(&self, path: &str) -> VfsResult<String> {
435        VirtualFileSystem::read_link(&self.inner, path)
436    }
437
438    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
439        VirtualFileSystem::lstat(&self.inner, path)
440    }
441
442    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
443        VirtualFileSystem::link(&mut self.inner, old_path, new_path)
444    }
445
446    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
447        VirtualFileSystem::chmod(&mut self.inner, path, mode)
448    }
449
450    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
451        VirtualFileSystem::chown(&mut self.inner, path, uid, gid)
452    }
453
454    fn chown_spec(
455        &mut self,
456        path: &str,
457        uid: u32,
458        gid: u32,
459        follow_symlinks: bool,
460    ) -> VfsResult<()> {
461        VirtualFileSystem::chown_spec(&mut self.inner, path, uid, gid, follow_symlinks)
462    }
463
464    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
465        VirtualFileSystem::lchown(&mut self.inner, path, uid, gid)
466    }
467
468    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
469        VirtualFileSystem::get_xattr(&mut self.inner, path, name, follow_symlinks)
470    }
471
472    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
473        VirtualFileSystem::list_xattrs(&mut self.inner, path, follow_symlinks)
474    }
475
476    fn set_xattr(
477        &mut self,
478        path: &str,
479        name: &str,
480        value: Vec<u8>,
481        flags: u32,
482        follow_symlinks: bool,
483    ) -> VfsResult<()> {
484        VirtualFileSystem::set_xattr(&mut self.inner, path, name, value, flags, follow_symlinks)
485    }
486
487    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
488        VirtualFileSystem::remove_xattr(&mut self.inner, path, name, follow_symlinks)
489    }
490
491    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
492        VirtualFileSystem::utimes(&mut self.inner, path, atime_ms, mtime_ms)
493    }
494
495    fn utimes_spec(
496        &mut self,
497        path: &str,
498        atime: VirtualUtimeSpec,
499        mtime: VirtualUtimeSpec,
500        follow_symlinks: bool,
501    ) -> VfsResult<()> {
502        VirtualFileSystem::utimes_spec(&mut self.inner, path, atime, mtime, follow_symlinks)
503    }
504
505    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
506        VirtualFileSystem::truncate(&mut self.inner, path, length)
507    }
508
509    fn sync(&mut self, path: &str) -> VfsResult<()> {
510        VirtualFileSystem::sync(&mut self.inner, path)
511    }
512
513    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
514        VirtualFileSystem::allocate(&mut self.inner, path, offset, length)
515    }
516
517    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
518        VirtualFileSystem::insert_range(&mut self.inner, path, offset, length)
519    }
520
521    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
522        VirtualFileSystem::collapse_range(&mut self.inner, path, offset, length)
523    }
524
525    fn zero_range(
526        &mut self,
527        path: &str,
528        offset: u64,
529        length: u64,
530        keep_size: bool,
531    ) -> VfsResult<()> {
532        VirtualFileSystem::zero_range(&mut self.inner, path, offset, length, keep_size)
533    }
534
535    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
536        VirtualFileSystem::punch_hole(&mut self.inner, path, offset, length)
537    }
538
539    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
540        VirtualFileSystem::allocated_ranges(&mut self.inner, path)
541    }
542
543    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
544        VirtualFileSystem::unwritten_ranges(&mut self.inner, path)
545    }
546
547    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
548        VirtualFileSystem::pread(&mut self.inner, path, offset, length)
549    }
550
551    fn pwrite(&mut self, path: &str, content: Vec<u8>, offset: u64) -> VfsResult<()> {
552        VirtualFileSystem::pwrite(&mut self.inner, path, content, offset)
553    }
554}
555
556impl<T> MountedFileSystem for Box<T>
557where
558    T: MountedFileSystem + ?Sized + 'static,
559{
560    fn as_any(&self) -> &dyn Any {
561        (**self).as_any()
562    }
563
564    fn as_any_mut(&mut self) -> &mut dyn Any {
565        (**self).as_any_mut()
566    }
567
568    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
569        (**self).read_file(path)
570    }
571
572    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
573        (**self).read_dir(path)
574    }
575
576    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
577        (**self).read_dir_limited(path, max_entries)
578    }
579
580    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
581        (**self).read_dir_with_types(path)
582    }
583
584    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
585        (**self).write_file(path, content)
586    }
587
588    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
589        (**self).create_dir(path)
590    }
591
592    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
593        (**self).mkdir(path, recursive)
594    }
595
596    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
597        (**self).mknod(path, mode, rdev)
598    }
599
600    fn exists(&self, path: &str) -> bool {
601        (**self).exists(path)
602    }
603
604    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
605        (**self).stat(path)
606    }
607
608    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
609        (**self).remove_file(path)
610    }
611
612    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
613        (**self).remove_dir(path)
614    }
615
616    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
617        (**self).rename(old_path, new_path)
618    }
619
620    fn realpath(&self, path: &str) -> VfsResult<String> {
621        (**self).realpath(path)
622    }
623
624    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
625        (**self).symlink(target, link_path)
626    }
627
628    fn read_link(&self, path: &str) -> VfsResult<String> {
629        (**self).read_link(path)
630    }
631
632    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
633        (**self).lstat(path)
634    }
635
636    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
637        (**self).link(old_path, new_path)
638    }
639
640    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
641        (**self).chmod(path, mode)
642    }
643
644    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
645        (**self).chown(path, uid, gid)
646    }
647
648    fn chown_spec(
649        &mut self,
650        path: &str,
651        uid: u32,
652        gid: u32,
653        follow_symlinks: bool,
654    ) -> VfsResult<()> {
655        (**self).chown_spec(path, uid, gid, follow_symlinks)
656    }
657
658    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
659        (**self).lchown(path, uid, gid)
660    }
661
662    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
663        (**self).get_xattr(path, name, follow_symlinks)
664    }
665
666    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
667        (**self).list_xattrs(path, follow_symlinks)
668    }
669
670    fn set_xattr(
671        &mut self,
672        path: &str,
673        name: &str,
674        value: Vec<u8>,
675        flags: u32,
676        follow_symlinks: bool,
677    ) -> VfsResult<()> {
678        (**self).set_xattr(path, name, value, flags, follow_symlinks)
679    }
680
681    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
682        (**self).remove_xattr(path, name, follow_symlinks)
683    }
684
685    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
686        (**self).utimes(path, atime_ms, mtime_ms)
687    }
688
689    fn set_atime(&mut self, path: &str, atime_ms: u64) -> VfsResult<()> {
690        (**self).set_atime(path, atime_ms)
691    }
692
693    fn utimes_spec(
694        &mut self,
695        path: &str,
696        atime: VirtualUtimeSpec,
697        mtime: VirtualUtimeSpec,
698        follow_symlinks: bool,
699    ) -> VfsResult<()> {
700        (**self).utimes_spec(path, atime, mtime, follow_symlinks)
701    }
702
703    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
704        (**self).truncate(path, length)
705    }
706
707    fn sync(&mut self, path: &str) -> VfsResult<()> {
708        (**self).sync(path)
709    }
710
711    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
712        (**self).allocate(path, offset, length)
713    }
714
715    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
716        (**self).insert_range(path, offset, length)
717    }
718
719    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
720        (**self).collapse_range(path, offset, length)
721    }
722
723    fn zero_range(
724        &mut self,
725        path: &str,
726        offset: u64,
727        length: u64,
728        keep_size: bool,
729    ) -> VfsResult<()> {
730        (**self).zero_range(path, offset, length, keep_size)
731    }
732
733    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
734        (**self).punch_hole(path, offset, length)
735    }
736
737    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
738        (**self).allocated_ranges(path)
739    }
740
741    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
742        (**self).unwritten_ranges(path)
743    }
744
745    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
746        (**self).pread(path, offset, length)
747    }
748
749    fn pwrite(&mut self, path: &str, content: Vec<u8>, offset: u64) -> VfsResult<()> {
750        (**self).pwrite(path, content, offset)
751    }
752
753    fn shutdown(&mut self) -> VfsResult<()> {
754        (**self).shutdown()
755    }
756}
757
758pub struct ReadOnlyFileSystem<F> {
759    inner: F,
760}
761
762impl<F> ReadOnlyFileSystem<F> {
763    pub fn new(inner: F) -> Self {
764        Self { inner }
765    }
766}
767
768impl<F> MountedFileSystem for ReadOnlyFileSystem<F>
769where
770    F: MountedFileSystem + 'static,
771{
772    fn as_any(&self) -> &dyn Any {
773        self
774    }
775
776    fn as_any_mut(&mut self) -> &mut dyn Any {
777        self
778    }
779
780    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
781        self.inner.read_file(path)
782    }
783
784    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
785        self.inner.read_dir(path)
786    }
787
788    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
789        self.inner.read_dir_limited(path, max_entries)
790    }
791
792    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
793        self.inner.read_dir_with_types(path)
794    }
795
796    fn write_file(&mut self, path: &str, _content: Vec<u8>) -> VfsResult<()> {
797        Err(VfsError::new(
798            "EROFS",
799            format!("read-only filesystem: {path}"),
800        ))
801    }
802
803    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
804        Err(VfsError::new(
805            "EROFS",
806            format!("read-only filesystem: {path}"),
807        ))
808    }
809
810    fn mkdir(&mut self, path: &str, _recursive: bool) -> VfsResult<()> {
811        Err(VfsError::new(
812            "EROFS",
813            format!("read-only filesystem: {path}"),
814        ))
815    }
816
817    fn mknod(&mut self, path: &str, _mode: u32, _rdev: u64) -> VfsResult<()> {
818        Err(VfsError::new(
819            "EROFS",
820            format!("read-only filesystem: {path}"),
821        ))
822    }
823
824    fn exists(&self, path: &str) -> bool {
825        self.inner.exists(path)
826    }
827
828    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
829        self.inner.stat(path)
830    }
831
832    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
833        Err(VfsError::new(
834            "EROFS",
835            format!("read-only filesystem: {path}"),
836        ))
837    }
838
839    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
840        Err(VfsError::new(
841            "EROFS",
842            format!("read-only filesystem: {path}"),
843        ))
844    }
845
846    fn rename(&mut self, old_path: &str, _new_path: &str) -> VfsResult<()> {
847        Err(VfsError::new(
848            "EROFS",
849            format!("read-only filesystem: {old_path}"),
850        ))
851    }
852
853    fn realpath(&self, path: &str) -> VfsResult<String> {
854        self.inner.realpath(path)
855    }
856
857    fn symlink(&mut self, _target: &str, link_path: &str) -> VfsResult<()> {
858        Err(VfsError::new(
859            "EROFS",
860            format!("read-only filesystem: {link_path}"),
861        ))
862    }
863
864    fn read_link(&self, path: &str) -> VfsResult<String> {
865        self.inner.read_link(path)
866    }
867
868    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
869        self.inner.lstat(path)
870    }
871
872    fn link(&mut self, _old_path: &str, new_path: &str) -> VfsResult<()> {
873        Err(VfsError::new(
874            "EROFS",
875            format!("read-only filesystem: {new_path}"),
876        ))
877    }
878
879    fn chmod(&mut self, path: &str, _mode: u32) -> VfsResult<()> {
880        Err(VfsError::new(
881            "EROFS",
882            format!("read-only filesystem: {path}"),
883        ))
884    }
885
886    fn chown(&mut self, path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
887        Err(VfsError::new(
888            "EROFS",
889            format!("read-only filesystem: {path}"),
890        ))
891    }
892
893    fn chown_spec(
894        &mut self,
895        path: &str,
896        _uid: u32,
897        _gid: u32,
898        _follow_symlinks: bool,
899    ) -> VfsResult<()> {
900        Err(VfsError::new(
901            "EROFS",
902            format!("read-only filesystem: {path}"),
903        ))
904    }
905
906    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
907        self.inner.get_xattr(path, name, follow_symlinks)
908    }
909
910    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
911        self.inner.list_xattrs(path, follow_symlinks)
912    }
913
914    fn set_xattr(
915        &mut self,
916        path: &str,
917        _name: &str,
918        _value: Vec<u8>,
919        _flags: u32,
920        _follow_symlinks: bool,
921    ) -> VfsResult<()> {
922        Err(VfsError::new(
923            "EROFS",
924            format!("read-only filesystem: {path}"),
925        ))
926    }
927
928    fn remove_xattr(&mut self, path: &str, _name: &str, _follow_symlinks: bool) -> VfsResult<()> {
929        Err(VfsError::new(
930            "EROFS",
931            format!("read-only filesystem: {path}"),
932        ))
933    }
934
935    fn utimes(&mut self, path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
936        Err(VfsError::new(
937            "EROFS",
938            format!("read-only filesystem: {path}"),
939        ))
940    }
941
942    fn set_atime(&mut self, _path: &str, _atime_ms: u64) -> VfsResult<()> {
943        Ok(())
944    }
945
946    fn utimes_spec(
947        &mut self,
948        path: &str,
949        _atime: VirtualUtimeSpec,
950        _mtime: VirtualUtimeSpec,
951        _follow_symlinks: bool,
952    ) -> VfsResult<()> {
953        Err(VfsError::new(
954            "EROFS",
955            format!("read-only filesystem: {path}"),
956        ))
957    }
958
959    fn truncate(&mut self, path: &str, _length: u64) -> VfsResult<()> {
960        Err(VfsError::new(
961            "EROFS",
962            format!("read-only filesystem: {path}"),
963        ))
964    }
965
966    fn sync(&mut self, path: &str) -> VfsResult<()> {
967        self.inner.sync(path)
968    }
969
970    fn allocate(&mut self, path: &str, _offset: u64, _length: u64) -> VfsResult<()> {
971        Err(VfsError::new(
972            "EROFS",
973            format!("read-only filesystem: {path}"),
974        ))
975    }
976
977    fn insert_range(&mut self, path: &str, _offset: u64, _length: u64) -> VfsResult<()> {
978        Err(VfsError::new(
979            "EROFS",
980            format!("read-only filesystem: {path}"),
981        ))
982    }
983
984    fn collapse_range(&mut self, path: &str, _offset: u64, _length: u64) -> VfsResult<()> {
985        Err(VfsError::new(
986            "EROFS",
987            format!("read-only filesystem: {path}"),
988        ))
989    }
990
991    fn zero_range(
992        &mut self,
993        path: &str,
994        _offset: u64,
995        _length: u64,
996        _keep_size: bool,
997    ) -> VfsResult<()> {
998        Err(VfsError::new(
999            "EROFS",
1000            format!("read-only filesystem: {path}"),
1001        ))
1002    }
1003
1004    fn punch_hole(&mut self, path: &str, _offset: u64, _length: u64) -> VfsResult<()> {
1005        Err(VfsError::new(
1006            "EROFS",
1007            format!("read-only filesystem: {path}"),
1008        ))
1009    }
1010
1011    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
1012        self.inner.allocated_ranges(path)
1013    }
1014
1015    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
1016        self.inner.unwritten_ranges(path)
1017    }
1018
1019    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1020        self.inner.pread(path, offset, length)
1021    }
1022
1023    fn pwrite(&mut self, path: &str, _content: Vec<u8>, _offset: u64) -> VfsResult<()> {
1024        Err(VfsError::new(
1025            "EROFS",
1026            format!("read-only filesystem: {path}"),
1027        ))
1028    }
1029
1030    fn shutdown(&mut self) -> VfsResult<()> {
1031        self.inner.shutdown()
1032    }
1033}
1034
1035#[derive(Debug, Clone, PartialEq, Eq)]
1036pub enum AccessTimePolicy {
1037    Relatime,
1038    NoAtime,
1039    StrictAtime,
1040}
1041
1042impl AccessTimePolicy {
1043    pub fn option_name(&self) -> &'static str {
1044        match self {
1045            Self::Relatime => "relatime",
1046            Self::NoAtime => "noatime",
1047            Self::StrictAtime => "strictatime",
1048        }
1049    }
1050}
1051
1052impl MountEntry {
1053    pub fn option_string(&self) -> String {
1054        let mut options = vec![
1055            if self.read_only { "ro" } else { "rw" },
1056            self.access_time.option_name(),
1057        ];
1058        if self.no_dir_atime {
1059            options.push("nodiratime");
1060        }
1061        options.join(",")
1062    }
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Eq)]
1066pub struct MountEntry {
1067    pub path: String,
1068    pub plugin_id: String,
1069    pub guest_source: String,
1070    pub guest_fstype: String,
1071    pub read_only: bool,
1072    pub access_time: AccessTimePolicy,
1073    pub no_dir_atime: bool,
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq)]
1077pub struct MountOptions {
1078    pub plugin_id: String,
1079    pub guest_source: String,
1080    pub guest_fstype: String,
1081    pub read_only: bool,
1082    pub access_time: AccessTimePolicy,
1083    pub no_dir_atime: bool,
1084    pub max_bytes: Option<u64>,
1085    pub max_inodes: Option<usize>,
1086}
1087
1088impl MountOptions {
1089    pub fn new(plugin_id: impl Into<String>) -> Self {
1090        let plugin_id = plugin_id.into();
1091        Self {
1092            guest_source: plugin_id.clone(),
1093            guest_fstype: plugin_id.clone(),
1094            plugin_id,
1095            read_only: false,
1096            access_time: AccessTimePolicy::Relatime,
1097            no_dir_atime: false,
1098            max_bytes: None,
1099            max_inodes: None,
1100        }
1101    }
1102
1103    pub fn guest_source(mut self, guest_source: impl Into<String>) -> Self {
1104        self.guest_source = guest_source.into();
1105        self
1106    }
1107
1108    pub fn guest_fstype(mut self, guest_fstype: impl Into<String>) -> Self {
1109        self.guest_fstype = guest_fstype.into();
1110        self
1111    }
1112
1113    pub fn read_only(mut self, read_only: bool) -> Self {
1114        self.read_only = read_only;
1115        self
1116    }
1117
1118    pub fn access_time(mut self, access_time: AccessTimePolicy) -> Self {
1119        self.access_time = access_time;
1120        self
1121    }
1122
1123    pub fn no_dir_atime(mut self, no_dir_atime: bool) -> Self {
1124        self.no_dir_atime = no_dir_atime;
1125        self
1126    }
1127
1128    pub fn max_bytes(mut self, max_bytes: Option<u64>) -> Self {
1129        self.max_bytes = max_bytes;
1130        self
1131    }
1132
1133    pub fn max_inodes(mut self, max_inodes: Option<usize>) -> Self {
1134        self.max_inodes = max_inodes;
1135        self
1136    }
1137}
1138
1139struct MountRegistration {
1140    path: String,
1141    plugin_id: String,
1142    guest_source: String,
1143    guest_fstype: String,
1144    read_only: bool,
1145    access_time: AccessTimePolicy,
1146    no_dir_atime: bool,
1147    max_bytes: Option<u64>,
1148    max_inodes: Option<usize>,
1149    cached_usage: Option<FileSystemUsage>,
1150    filesystem: Box<dyn MountedFileSystem>,
1151}
1152
1153pub struct MountTable {
1154    mounts: Vec<MountRegistration>,
1155    mount_indices: BTreeMap<String, usize>,
1156}
1157
1158impl MountTable {
1159    pub fn new(root_fs: impl VirtualFileSystem + 'static) -> Self {
1160        Self {
1161            mounts: vec![MountRegistration {
1162                path: String::from("/"),
1163                plugin_id: String::from("root"),
1164                guest_source: String::from("root"),
1165                guest_fstype: String::from("root"),
1166                read_only: false,
1167                access_time: AccessTimePolicy::Relatime,
1168                no_dir_atime: false,
1169                max_bytes: None,
1170                max_inodes: None,
1171                cached_usage: None,
1172                filesystem: Box::new(MountedVirtualFileSystem::new(root_fs)),
1173            }],
1174            mount_indices: BTreeMap::from([(String::from("/"), 0)]),
1175        }
1176    }
1177
1178    pub fn new_boxed_root(filesystem: Box<dyn MountedFileSystem>, options: MountOptions) -> Self {
1179        let filesystem = if options.read_only {
1180            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
1181        } else {
1182            filesystem
1183        };
1184
1185        Self {
1186            mounts: vec![MountRegistration {
1187                path: String::from("/"),
1188                plugin_id: options.plugin_id,
1189                guest_source: options.guest_source,
1190                guest_fstype: options.guest_fstype,
1191                read_only: options.read_only,
1192                access_time: options.access_time,
1193                no_dir_atime: options.no_dir_atime,
1194                max_bytes: options.max_bytes,
1195                max_inodes: options.max_inodes,
1196                cached_usage: None,
1197                filesystem,
1198            }],
1199            mount_indices: BTreeMap::from([(String::from("/"), 0)]),
1200        }
1201    }
1202
1203    pub fn mount(
1204        &mut self,
1205        path: &str,
1206        filesystem: impl VirtualFileSystem + 'static,
1207        options: MountOptions,
1208    ) -> VfsResult<()> {
1209        self.mount_boxed(
1210            path,
1211            Box::new(MountedVirtualFileSystem::new(filesystem)),
1212            options,
1213        )
1214    }
1215
1216    pub fn mount_boxed(
1217        &mut self,
1218        path: &str,
1219        mut filesystem: Box<dyn MountedFileSystem>,
1220        options: MountOptions,
1221    ) -> VfsResult<()> {
1222        let normalized = normalize_path(path);
1223        if normalized == "/" {
1224            return Err(VfsError::new("EINVAL", "cannot mount over root"));
1225        }
1226        if self.mounts.iter().any(|mount| mount.path == normalized) {
1227            return Err(VfsError::new(
1228                "EEXIST",
1229                format!("already mounted at {normalized}"),
1230            ));
1231        }
1232
1233        let (parent_index, relative_path) = self.resolve_index(&normalized)?;
1234        let parent_mount = &mut self.mounts[parent_index];
1235        if !parent_mount.filesystem.exists(&relative_path) {
1236            // Materializing the mountpoint directory on the parent is
1237            // cosmetic: child mounts resolve by path prefix before the parent
1238            // is consulted. A read-only parent (for example a read-only
1239            // module-access mount hosting nested package mounts) cannot
1240            // materialize the entry, but the mount must still succeed.
1241            if let Err(error) = parent_mount.filesystem.mkdir(&relative_path, true) {
1242                if error.code() != "EROFS" {
1243                    if let Err(shutdown_error) = filesystem.shutdown() {
1244                        return Err(VfsError::new(
1245                            shutdown_error.code(),
1246                            format!(
1247                                "failed to shut down filesystem after mount failure ({error}): {}",
1248                                shutdown_error.message()
1249                            ),
1250                        ));
1251                    }
1252
1253                    return Err(error);
1254                }
1255            }
1256        }
1257
1258        let filesystem = if options.read_only {
1259            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
1260        } else {
1261            filesystem
1262        };
1263
1264        self.mounts.push(MountRegistration {
1265            path: normalized,
1266            plugin_id: options.plugin_id,
1267            guest_source: options.guest_source,
1268            guest_fstype: options.guest_fstype,
1269            read_only: options.read_only,
1270            access_time: options.access_time,
1271            no_dir_atime: options.no_dir_atime,
1272            max_bytes: options.max_bytes,
1273            max_inodes: options.max_inodes,
1274            cached_usage: None,
1275            filesystem,
1276        });
1277        self.mounts
1278            .sort_by_key(|mount| std::cmp::Reverse(mount.path.len()));
1279        self.rebuild_mount_indices();
1280        Ok(())
1281    }
1282
1283    pub fn unmount(&mut self, path: &str) -> VfsResult<()> {
1284        let normalized = normalize_path(path);
1285        if normalized == "/" {
1286            return Err(VfsError::new("EINVAL", "cannot unmount root"));
1287        }
1288
1289        let child_mount_prefix = format!("{normalized}/");
1290        if self
1291            .mounts
1292            .iter()
1293            .any(|mount| mount.path.starts_with(&child_mount_prefix))
1294        {
1295            return Err(VfsError::new(
1296                "EBUSY",
1297                format!("mount point has child mounts: {normalized}"),
1298            ));
1299        }
1300
1301        let Some(index) = self
1302            .mounts
1303            .iter()
1304            .position(|mount| mount.path == normalized)
1305        else {
1306            return Err(VfsError::new(
1307                "EINVAL",
1308                format!("not a mount point: {normalized}"),
1309            ));
1310        };
1311
1312        let mut mount = self.mounts.remove(index);
1313        self.rebuild_mount_indices();
1314        mount.filesystem.shutdown()?;
1315        Ok(())
1316    }
1317
1318    pub fn remount(&mut self, path: &str, options: &str) -> VfsResult<()> {
1319        let normalized = normalize_path(path);
1320        let mount = self
1321            .mounts
1322            .iter_mut()
1323            .find(|mount| mount.path == normalized)
1324            .ok_or_else(|| VfsError::new("EINVAL", format!("not a mount point: {normalized}")))?;
1325
1326        let mut read_only = mount.read_only;
1327        let mut access_time = mount.access_time.clone();
1328        let mut no_dir_atime = mount.no_dir_atime;
1329        let mut max_bytes = mount.max_bytes;
1330        let mut max_inodes = mount.max_inodes;
1331        for option in options
1332            .split(',')
1333            .map(str::trim)
1334            .filter(|value| !value.is_empty())
1335        {
1336            match option {
1337                "remount" => {}
1338                "ro" => read_only = true,
1339                "rw" => read_only = false,
1340                "relatime" => access_time = AccessTimePolicy::Relatime,
1341                "noatime" => access_time = AccessTimePolicy::NoAtime,
1342                "strictatime" => access_time = AccessTimePolicy::StrictAtime,
1343                "nodiratime" => no_dir_atime = true,
1344                "diratime" => no_dir_atime = false,
1345                value if value.starts_with("size=") => {
1346                    max_bytes = Some(parse_mount_limit(value, "size")?);
1347                }
1348                value if value.starts_with("inodes=") => {
1349                    max_inodes = Some(
1350                        usize::try_from(parse_mount_limit(value, "inodes")?).map_err(|_| {
1351                            VfsError::new("EINVAL", "mount inode limit exceeds usize")
1352                        })?,
1353                    );
1354                }
1355                unsupported => {
1356                    return Err(VfsError::new(
1357                        "EINVAL",
1358                        format!("unsupported mount option: {unsupported}"),
1359                    ));
1360                }
1361            }
1362        }
1363        let usage =
1364            measure_mounted_filesystem_usage(mount.filesystem.as_mut(), "/", &mut BTreeSet::new())?;
1365        check_usage_limits(&usage, max_bytes, max_inodes)?;
1366        mount.read_only = read_only;
1367        mount.access_time = access_time;
1368        mount.no_dir_atime = no_dir_atime;
1369        mount.max_bytes = max_bytes;
1370        mount.max_inodes = max_inodes;
1371        mount.cached_usage = Some(usage);
1372        Ok(())
1373    }
1374
1375    pub fn get_mounts(&self) -> Vec<MountEntry> {
1376        self.mounts
1377            .iter()
1378            .map(|mount| MountEntry {
1379                path: mount.path.clone(),
1380                plugin_id: mount.plugin_id.clone(),
1381                guest_source: mount.guest_source.clone(),
1382                guest_fstype: mount.guest_fstype.clone(),
1383                read_only: mount.read_only,
1384                access_time: mount.access_time.clone(),
1385                no_dir_atime: mount.no_dir_atime,
1386            })
1387            .collect()
1388    }
1389
1390    pub fn root_virtual_filesystem_mut<T: VirtualFileSystem + 'static>(
1391        &mut self,
1392    ) -> Option<&mut T> {
1393        let root = self.mounts.iter_mut().find(|mount| mount.path == "/")?;
1394        root.filesystem
1395            .as_any_mut()
1396            .downcast_mut::<MountedVirtualFileSystem<T>>()
1397            .map(MountedVirtualFileSystem::inner_mut)
1398    }
1399
1400    pub fn check_rename_copy_up_limits(
1401        &mut self,
1402        old_path: &str,
1403        new_path: &str,
1404        max_bytes: Option<u64>,
1405        max_inodes: Option<usize>,
1406    ) -> VfsResult<()> {
1407        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
1408        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
1409        if old_index != new_index {
1410            return Ok(());
1411        }
1412
1413        let filesystem = &mut self.mounts[old_index].filesystem;
1414        if let Some(root) = filesystem
1415            .as_any_mut()
1416            .downcast_mut::<MountedVirtualFileSystem<RootFileSystem>>()
1417        {
1418            root.inner_mut().check_rename_copy_up_limits(
1419                &old_relative_path,
1420                &new_relative_path,
1421                max_bytes,
1422                max_inodes,
1423            )?;
1424        }
1425
1426        Ok(())
1427    }
1428
1429    pub fn root_usage(&mut self) -> VfsResult<FileSystemUsage> {
1430        let root = self
1431            .mounts
1432            .iter_mut()
1433            .find(|mount| mount.path == "/")
1434            .ok_or_else(|| VfsError::new("ENOENT", "missing root mount"))?;
1435        measure_mounted_filesystem_usage(root.filesystem.as_mut(), "/", &mut BTreeSet::new())
1436    }
1437
1438    pub fn path_stats(
1439        &mut self,
1440        path: &str,
1441        root_max_bytes: Option<u64>,
1442        root_max_inodes: Option<usize>,
1443    ) -> VfsResult<FileSystemStats> {
1444        let (index, _) = self.resolve_content_index(path)?;
1445        let mount = &mut self.mounts[index];
1446        let usage =
1447            measure_mounted_filesystem_usage(mount.filesystem.as_mut(), "/", &mut BTreeSet::new())?;
1448        mount.cached_usage = Some(usage.clone());
1449        let max_bytes = mount
1450            .max_bytes
1451            .or_else(|| (mount.path == "/").then_some(root_max_bytes).flatten())
1452            .unwrap_or(usage.total_bytes);
1453        let used_bytes = usage.total_bytes.min(max_bytes);
1454        let max_inodes = mount
1455            .max_inodes
1456            .or_else(|| (mount.path == "/").then_some(root_max_inodes).flatten())
1457            .map(|value| value as u64)
1458            .unwrap_or(usage.inode_count as u64);
1459        let used_inodes = (usage.inode_count as u64).min(max_inodes);
1460        Ok(FileSystemStats {
1461            total_bytes: max_bytes,
1462            used_bytes,
1463            available_bytes: max_bytes.saturating_sub(used_bytes),
1464            total_inodes: max_inodes,
1465            free_inodes: max_inodes.saturating_sub(used_inodes),
1466        })
1467    }
1468
1469    fn cached_usage(&mut self, index: usize) -> VfsResult<FileSystemUsage> {
1470        if let Some(usage) = self.mounts[index].cached_usage.clone() {
1471            return Ok(usage);
1472        }
1473        let usage = measure_mounted_filesystem_usage(
1474            self.mounts[index].filesystem.as_mut(),
1475            "/",
1476            &mut BTreeSet::new(),
1477        )?;
1478        self.mounts[index].cached_usage = Some(usage.clone());
1479        Ok(usage)
1480    }
1481
1482    fn update_cached_path_usage(
1483        &mut self,
1484        index: usize,
1485        before: Option<VirtualStat>,
1486        relative_path: &str,
1487    ) {
1488        let after = self.mounts[index].filesystem.lstat(relative_path).ok();
1489        let Some(usage) = self.mounts[index].cached_usage.as_mut() else {
1490            return;
1491        };
1492        match (before, after) {
1493            (None, Some(after)) => {
1494                usage.inode_count = usage.inode_count.saturating_add(1);
1495                if !after.is_directory {
1496                    usage.total_bytes = usage.total_bytes.saturating_add(after.size);
1497                }
1498            }
1499            (Some(before), None) => {
1500                if before.is_directory || before.nlink <= 1 {
1501                    usage.inode_count = usage.inode_count.saturating_sub(1);
1502                    if !before.is_directory {
1503                        usage.total_bytes = usage.total_bytes.saturating_sub(before.size);
1504                    }
1505                }
1506            }
1507            (Some(before), Some(after))
1508                if (before.dev, before.ino) == (after.dev, after.ino) && !before.is_directory =>
1509            {
1510                usage.total_bytes = usage
1511                    .total_bytes
1512                    .saturating_sub(before.size)
1513                    .saturating_add(after.size);
1514            }
1515            _ => {}
1516        }
1517    }
1518
1519    fn check_file_growth(
1520        &mut self,
1521        index: usize,
1522        relative_path: &str,
1523        new_size: u64,
1524        exclusive: bool,
1525    ) -> VfsResult<()> {
1526        if self.mounts[index].max_bytes.is_none() && self.mounts[index].max_inodes.is_none() {
1527            return Ok(());
1528        }
1529        let usage = self.cached_usage(index)?;
1530        let mount = &mut self.mounts[index];
1531        let existing = mount.filesystem.lstat(relative_path).ok();
1532        let existing_size = existing
1533            .as_ref()
1534            .filter(|stat| !stat.is_directory)
1535            .map_or(0, |stat| stat.size);
1536        let resulting = FileSystemUsage {
1537            total_bytes: usage
1538                .total_bytes
1539                .saturating_sub(existing_size)
1540                .saturating_add(new_size),
1541            inode_count: usage
1542                .inode_count
1543                .saturating_add(usize::from(existing.is_none())),
1544        };
1545        if exclusive && existing.is_some() {
1546            return Ok(());
1547        }
1548        check_usage_limits(&resulting, mount.max_bytes, mount.max_inodes)
1549    }
1550
1551    fn check_inode_growth(&mut self, index: usize, added: usize) -> VfsResult<()> {
1552        if added == 0
1553            || (self.mounts[index].max_bytes.is_none() && self.mounts[index].max_inodes.is_none())
1554        {
1555            return Ok(());
1556        }
1557        let mut usage = self.cached_usage(index)?;
1558        let mount = &self.mounts[index];
1559        usage.inode_count = usage.inode_count.saturating_add(added);
1560        check_usage_limits(&usage, mount.max_bytes, mount.max_inodes)
1561    }
1562
1563    fn missing_directory_count(&self, index: usize, relative_path: &str) -> usize {
1564        let mut current = String::from("/");
1565        let mut missing = 0usize;
1566        for component in path_components(relative_path) {
1567            current = join_path(&current, &component);
1568            if !self.mounts[index].filesystem.exists(&current) {
1569                missing = missing.saturating_add(1);
1570            }
1571        }
1572        missing
1573    }
1574
1575    pub fn path_uses_root_filesystem(&self, path: &str) -> bool {
1576        self.resolve_index(path)
1577            .is_ok_and(|(index, _)| self.mounts[index].path == "/")
1578    }
1579
1580    fn resolve_index(&self, full_path: &str) -> VfsResult<(usize, String)> {
1581        let normalized = normalize_path(full_path);
1582        let mut candidate = normalized.as_str();
1583        loop {
1584            if let Some(index) = self.mount_indices.get(candidate).copied() {
1585                let relative_path = if candidate == "/" {
1586                    normalized
1587                } else if candidate.len() == normalized.len() {
1588                    String::from("/")
1589                } else {
1590                    // Strip exactly the mount prefix once. `trim_start_matches`
1591                    // would strip repeated path components and alias distinct
1592                    // paths such as `/data/data/file` and `/data/file`.
1593                    format!("/{}", &normalized[candidate.len() + 1..])
1594                };
1595                return Ok((index, relative_path));
1596            }
1597            if candidate == "/" {
1598                break;
1599            }
1600            candidate = candidate
1601                .rfind('/')
1602                .map(|index| if index == 0 { "/" } else { &candidate[..index] })
1603                .unwrap_or("/");
1604        }
1605
1606        Err(VfsError::new(
1607            "ENOENT",
1608            format!("no such file or directory, resolve '{full_path}'"),
1609        ))
1610    }
1611
1612    fn rebuild_mount_indices(&mut self) {
1613        self.mount_indices.clear();
1614        self.mount_indices.extend(
1615            self.mounts
1616                .iter()
1617                .enumerate()
1618                .map(|(index, mount)| (mount.path.clone(), index)),
1619        );
1620    }
1621
1622    fn resolve_writable_index(&self, full_path: &str) -> VfsResult<(usize, String)> {
1623        let (index, relative_path) = self.resolve_index(full_path)?;
1624        self.ensure_writable(index, full_path)?;
1625        Ok((index, relative_path))
1626    }
1627
1628    fn ensure_writable(&self, index: usize, full_path: &str) -> VfsResult<()> {
1629        if self.mounts[index].read_only {
1630            return Err(VfsError::new(
1631                "EROFS",
1632                format!("read-only filesystem: {full_path}"),
1633            ));
1634        }
1635        Ok(())
1636    }
1637
1638    fn atime_snapshot(
1639        &mut self,
1640        index: usize,
1641        relative_path: &str,
1642        is_directory: bool,
1643    ) -> VfsResult<Option<VirtualStat>> {
1644        let mount = &mut self.mounts[index];
1645        if mount.read_only
1646            || mount.access_time == AccessTimePolicy::NoAtime
1647            || (is_directory && mount.no_dir_atime)
1648        {
1649            return Ok(None);
1650        }
1651        mount.filesystem.stat(relative_path).map(Some)
1652    }
1653
1654    fn finish_atime_update(
1655        &mut self,
1656        index: usize,
1657        relative_path: &str,
1658        before: Option<VirtualStat>,
1659    ) -> VfsResult<()> {
1660        let Some(before) = before else {
1661            return Ok(());
1662        };
1663        let mount = &mut self.mounts[index];
1664        let now_ms = SystemTime::now()
1665            .duration_since(UNIX_EPOCH)
1666            .unwrap_or_default()
1667            .as_millis() as u64;
1668        let update = match mount.access_time {
1669            AccessTimePolicy::NoAtime => false,
1670            AccessTimePolicy::StrictAtime => true,
1671            AccessTimePolicy::Relatime => {
1672                timestamp_ns(before.atime_ms, before.atime_nsec)
1673                    <= timestamp_ns(before.mtime_ms, before.mtime_nsec)
1674                    || timestamp_ns(before.atime_ms, before.atime_nsec)
1675                        <= timestamp_ns(before.ctime_ms, before.ctime_nsec)
1676                    || now_ms.saturating_sub(before.atime_ms) >= 24 * 60 * 60 * 1_000
1677            }
1678        };
1679        if update {
1680            mount.filesystem.set_atime(relative_path, now_ms)?;
1681        }
1682        Ok(())
1683    }
1684
1685    /// Resolve a path for a CONTENT operation (read_file/stat/pread/read_dir)
1686    /// that must follow symlinks like POSIX `open()`. `resolve_index` is purely
1687    /// lexical, so a path that descends through a symlink whose target lives in a
1688    /// *different* mount (e.g. `/opt/agentos/pkgs/<pkg>/current -> <version>`,
1689    /// where `current` is its own single-symlink leaf mount) would route into the
1690    /// symlink mount and fail. `realpath` follows those cross-mount symlinks, so
1691    /// resolve it first, then route the resolved path. Falls back to the raw path
1692    /// when realpath can't resolve it (e.g. a genuinely missing file) so callers
1693    /// still receive the mount's own ENOENT.
1694    /// Resolve a path for a LINK-LEAF operation (lstat/readlink) that must
1695    /// follow INTERMEDIATE symlinks but not the final component. When the
1696    /// lexical route has a symlinked parent, resolve that parent across mounts
1697    /// and re-route while keeping the final component unresolved. `relative ==
1698    /// "/"` (a mounted symlink leaf itself) keeps the raw route so
1699    /// `lstat(<pkg>/current)` still reports the symlink.
1700    fn resolve_link_leaf_index(&self, path: &str) -> VfsResult<(usize, String)> {
1701        let normalized = normalize_path(path);
1702        let raw = self.resolve_index(&normalized)?;
1703        if raw.1 == "/" {
1704            return Ok(raw);
1705        }
1706        let parent = parent_path(&normalized);
1707        let leaf = basename(&normalized);
1708        match self.realpath(&parent) {
1709            Ok(resolved_parent) if resolved_parent != parent => {
1710                self.resolve_index(&join_path(&resolved_parent, &leaf))
1711            }
1712            Err(_) => Ok(raw),
1713            Ok(_) => Ok(raw),
1714        }
1715    }
1716
1717    fn resolve_content_index(&self, path: &str) -> VfsResult<(usize, String)> {
1718        let normalized = normalize_path(path);
1719        let raw = self.resolve_index(&normalized)?;
1720        match self.realpath(&normalized) {
1721            Ok(resolved) if resolved != normalized => self.resolve_index(&resolved),
1722            Ok(_) | Err(_) => Ok(raw),
1723        }
1724    }
1725
1726    fn child_mount_basenames(&self, path: &str) -> Vec<String> {
1727        let normalized = normalize_path(path);
1728        let mut basenames = BTreeSet::new();
1729        for mount in &self.mounts {
1730            if mount.path == "/" || mount.path == normalized {
1731                continue;
1732            }
1733
1734            if parent_path(&mount.path) == normalized {
1735                basenames.insert(basename(&mount.path));
1736            }
1737        }
1738        basenames.into_iter().collect()
1739    }
1740
1741    fn realpath_in_mount(&self, index: usize, relative_path: &str) -> VfsResult<String> {
1742        let mount = &self.mounts[index];
1743        let resolved = mount.filesystem.realpath(relative_path)?;
1744        if mount.path == "/" {
1745            return Ok(normalize_path(&resolved));
1746        }
1747        if resolved == "/" {
1748            return Ok(mount.path.clone());
1749        }
1750        Ok(normalize_path(&format!(
1751            "{}/{}",
1752            mount.path,
1753            resolved.trim_start_matches('/')
1754        )))
1755    }
1756}
1757
1758fn measure_mounted_filesystem_usage(
1759    filesystem: &mut dyn MountedFileSystem,
1760    path: &str,
1761    visited: &mut BTreeSet<(u64, u64)>,
1762) -> VfsResult<FileSystemUsage> {
1763    let stat = filesystem.lstat(path)?;
1764    let mut usage = FileSystemUsage::default();
1765
1766    if visited.insert((stat.dev, stat.ino)) {
1767        usage.inode_count += 1;
1768        if !stat.is_directory {
1769            usage.total_bytes = usage.total_bytes.saturating_add(stat.size);
1770        }
1771    }
1772
1773    if !stat.is_directory || stat.is_symbolic_link {
1774        return Ok(usage);
1775    }
1776
1777    for entry in filesystem.read_dir_with_types(path)? {
1778        if matches!(entry.name.as_str(), "." | "..") {
1779            continue;
1780        }
1781
1782        let child_path = if path == "/" {
1783            format!("/{}", entry.name)
1784        } else {
1785            format!("{path}/{}", entry.name)
1786        };
1787        let child_usage = measure_mounted_filesystem_usage(filesystem, &child_path, visited)?;
1788        usage.total_bytes = usage.total_bytes.saturating_add(child_usage.total_bytes);
1789        usage.inode_count = usage.inode_count.saturating_add(child_usage.inode_count);
1790    }
1791
1792    Ok(usage)
1793}
1794
1795impl Drop for MountTable {
1796    fn drop(&mut self) {
1797        for mount in self.mounts.iter_mut().rev() {
1798            if let Err(error) = mount.filesystem.shutdown() {
1799                eprintln!(
1800                    "failed to shut down filesystem mounted at {}: {}: {}",
1801                    mount.path,
1802                    error.code(),
1803                    error.message()
1804                );
1805            }
1806        }
1807    }
1808}
1809
1810impl VirtualFileSystem for MountTable {
1811    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
1812        let (index, relative_path) = self.resolve_content_index(path)?;
1813        let before = self.atime_snapshot(index, &relative_path, false)?;
1814        let content = self.mounts[index].filesystem.read_file(&relative_path)?;
1815        self.finish_atime_update(index, &relative_path, before)?;
1816        Ok(content)
1817    }
1818
1819    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
1820        let normalized = normalize_path(path);
1821        // Directory listings are content ops: a path that descends through a
1822        // symlink-root leaf mount (`<pkg>/current -> <version>`) must be
1823        // followed into the target mount, exactly like read_file/stat. Child
1824        // mounts still merge on the LEXICAL path — mount points attach to the
1825        // caller-visible path, not the resolved target.
1826        let (index, relative_path) = self.resolve_content_index(&normalized)?;
1827        let before = self.atime_snapshot(index, &relative_path, true)?;
1828        let mut entries = self.mounts[index].filesystem.read_dir(&relative_path)?;
1829        self.finish_atime_update(index, &relative_path, before)?;
1830        let child_mounts = self.child_mount_basenames(&normalized);
1831        if child_mounts.is_empty() {
1832            return Ok(entries);
1833        }
1834
1835        let mut merged = BTreeSet::new();
1836        merged.extend(entries.drain(..));
1837        merged.extend(child_mounts);
1838        Ok(merged.into_iter().collect())
1839    }
1840
1841    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
1842        let normalized = normalize_path(path);
1843        let (index, relative_path) = self.resolve_content_index(&normalized)?;
1844        let before = self.atime_snapshot(index, &relative_path, true)?;
1845        let mut entries = self.mounts[index]
1846            .filesystem
1847            .read_dir_limited(&relative_path, max_entries)?;
1848        self.finish_atime_update(index, &relative_path, before)?;
1849        let child_mounts = self.child_mount_basenames(&normalized);
1850        if child_mounts.is_empty() {
1851            return Ok(entries);
1852        }
1853
1854        let mut merged = BTreeSet::new();
1855        merged.extend(entries.drain(..));
1856        merged.extend(child_mounts);
1857        if merged.len() > max_entries {
1858            return Err(VfsError::new(
1859                "ENOMEM",
1860                format!(
1861                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
1862                ),
1863            ));
1864        }
1865        Ok(merged.into_iter().collect())
1866    }
1867
1868    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1869        let normalized = normalize_path(path);
1870        let (index, relative_path) = self.resolve_content_index(&normalized)?;
1871        let before = self.atime_snapshot(index, &relative_path, true)?;
1872        let mut entries = self.mounts[index]
1873            .filesystem
1874            .read_dir_with_types(&relative_path)?;
1875        self.finish_atime_update(index, &relative_path, before)?;
1876        let child_mounts = self.child_mount_basenames(&normalized);
1877        if child_mounts.is_empty() {
1878            return Ok(entries);
1879        }
1880
1881        let existing = entries
1882            .iter()
1883            .map(|entry| entry.name.clone())
1884            .collect::<BTreeSet<_>>();
1885        for mount_name in child_mounts {
1886            if existing.contains(&mount_name) {
1887                continue;
1888            }
1889            entries.push(VirtualDirEntry {
1890                name: mount_name,
1891                is_directory: true,
1892                is_symbolic_link: false,
1893            });
1894        }
1895        Ok(entries)
1896    }
1897
1898    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1899        let content = content.into();
1900        let (index, relative_path) = self.resolve_writable_index(path)?;
1901        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1902        self.check_file_growth(index, &relative_path, content.len() as u64, false)?;
1903        self.mounts[index]
1904            .filesystem
1905            .write_file(&relative_path, content)?;
1906        self.update_cached_path_usage(index, before, &relative_path);
1907        Ok(())
1908    }
1909
1910    fn write_file_with_mode(
1911        &mut self,
1912        path: &str,
1913        content: impl Into<Vec<u8>>,
1914        mode: Option<u32>,
1915    ) -> VfsResult<()> {
1916        let content = content.into();
1917        let (index, relative_path) = self.resolve_writable_index(path)?;
1918        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1919        self.check_file_growth(index, &relative_path, content.len() as u64, false)?;
1920        self.mounts[index]
1921            .filesystem
1922            .write_file_with_mode(&relative_path, content, mode)?;
1923        self.update_cached_path_usage(index, before, &relative_path);
1924        Ok(())
1925    }
1926
1927    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1928        let content = content.into();
1929        let (index, relative_path) = self.resolve_writable_index(path)?;
1930        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1931        self.check_file_growth(index, &relative_path, content.len() as u64, true)?;
1932        self.mounts[index]
1933            .filesystem
1934            .create_file_exclusive(&relative_path, content)?;
1935        self.update_cached_path_usage(index, before, &relative_path);
1936        Ok(())
1937    }
1938
1939    fn create_file_exclusive_with_mode(
1940        &mut self,
1941        path: &str,
1942        content: impl Into<Vec<u8>>,
1943        mode: Option<u32>,
1944    ) -> VfsResult<()> {
1945        let content = content.into();
1946        let (index, relative_path) = self.resolve_writable_index(path)?;
1947        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1948        self.check_file_growth(index, &relative_path, content.len() as u64, true)?;
1949        self.mounts[index]
1950            .filesystem
1951            .create_file_exclusive_with_mode(&relative_path, content, mode)?;
1952        self.update_cached_path_usage(index, before, &relative_path);
1953        Ok(())
1954    }
1955
1956    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1957        let content = content.into();
1958        let (index, relative_path) = self.resolve_writable_index(path)?;
1959        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1960        let current_size = before.as_ref().map_or(0, |stat| stat.size);
1961        self.check_file_growth(
1962            index,
1963            &relative_path,
1964            current_size.saturating_add(content.len() as u64),
1965            false,
1966        )?;
1967        let size = self.mounts[index]
1968            .filesystem
1969            .append_file(&relative_path, content)?;
1970        self.update_cached_path_usage(index, before, &relative_path);
1971        Ok(size)
1972    }
1973
1974    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1975        let (index, relative_path) = self.resolve_writable_index(path)?;
1976        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1977        self.check_inode_growth(
1978            index,
1979            usize::from(!self.mounts[index].filesystem.exists(&relative_path)),
1980        )?;
1981        self.mounts[index].filesystem.create_dir(&relative_path)?;
1982        self.update_cached_path_usage(index, before, &relative_path);
1983        Ok(())
1984    }
1985
1986    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
1987        let (index, relative_path) = self.resolve_writable_index(path)?;
1988        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
1989        self.check_inode_growth(
1990            index,
1991            usize::from(!self.mounts[index].filesystem.exists(&relative_path)),
1992        )?;
1993        self.mounts[index]
1994            .filesystem
1995            .create_dir_with_mode(&relative_path, mode)?;
1996        self.update_cached_path_usage(index, before, &relative_path);
1997        Ok(())
1998    }
1999
2000    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
2001        let (index, relative_path) = self.resolve_writable_index(path)?;
2002        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2003        let added = if recursive {
2004            self.missing_directory_count(index, &relative_path)
2005        } else {
2006            usize::from(before.is_none())
2007        };
2008        self.check_inode_growth(index, added)?;
2009        self.mounts[index]
2010            .filesystem
2011            .mkdir(&relative_path, recursive)?;
2012        if recursive {
2013            if let Some(usage) = self.mounts[index].cached_usage.as_mut() {
2014                usage.inode_count = usage.inode_count.saturating_add(added);
2015            }
2016        } else {
2017            self.update_cached_path_usage(index, before, &relative_path);
2018        }
2019        Ok(())
2020    }
2021
2022    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
2023        let (index, relative_path) = self.resolve_writable_index(path)?;
2024        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2025        self.check_inode_growth(
2026            index,
2027            usize::from(!self.mounts[index].filesystem.exists(&relative_path)),
2028        )?;
2029        self.mounts[index]
2030            .filesystem
2031            .mknod(&relative_path, mode, rdev)?;
2032        self.update_cached_path_usage(index, before, &relative_path);
2033        Ok(())
2034    }
2035
2036    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
2037        let (index, relative_path) = self.resolve_writable_index(path)?;
2038        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2039        let added = if recursive {
2040            self.missing_directory_count(index, &relative_path)
2041        } else {
2042            usize::from(before.is_none())
2043        };
2044        self.check_inode_growth(index, added)?;
2045        self.mounts[index]
2046            .filesystem
2047            .mkdir_with_mode(&relative_path, recursive, mode)?;
2048        if recursive {
2049            if let Some(usage) = self.mounts[index].cached_usage.as_mut() {
2050                usage.inode_count = usage.inode_count.saturating_add(added);
2051            }
2052        } else {
2053            self.update_cached_path_usage(index, before, &relative_path);
2054        }
2055        Ok(())
2056    }
2057
2058    fn exists(&self, path: &str) -> bool {
2059        // `exists` follows symlinks like POSIX access(); route through the
2060        // content resolver so paths under a symlink-root leaf mount resolve.
2061        self.resolve_content_index(path)
2062            .map(|(index, relative_path)| self.mounts[index].filesystem.exists(&relative_path))
2063            .unwrap_or(false)
2064    }
2065
2066    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
2067        let (index, relative_path) = self.resolve_content_index(path)?;
2068        self.mounts[index].filesystem.stat(&relative_path)
2069    }
2070
2071    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
2072        let (index, relative_path) = self.resolve_writable_index(path)?;
2073        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2074        self.mounts[index].filesystem.remove_file(&relative_path)?;
2075        self.update_cached_path_usage(index, before, &relative_path);
2076        Ok(())
2077    }
2078
2079    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
2080        let (index, relative_path) = self.resolve_writable_index(path)?;
2081        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2082        self.mounts[index].filesystem.remove_dir(&relative_path)?;
2083        self.update_cached_path_usage(index, before, &relative_path);
2084        Ok(())
2085    }
2086
2087    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
2088        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
2089        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
2090        if old_index != new_index {
2091            return Err(VfsError::new(
2092                "EXDEV",
2093                format!("rename across mounts: {old_path} -> {new_path}"),
2094            ));
2095        }
2096        self.ensure_writable(old_index, old_path)?;
2097        self.ensure_writable(new_index, new_path)?;
2098        let source = self.mounts[old_index]
2099            .filesystem
2100            .lstat(&old_relative_path)
2101            .ok();
2102        let replaced = self.mounts[new_index]
2103            .filesystem
2104            .lstat(&new_relative_path)
2105            .ok();
2106        self.mounts[old_index]
2107            .filesystem
2108            .rename(&old_relative_path, &new_relative_path)?;
2109        if let (Some(source), Some(replaced), Some(usage)) = (
2110            source,
2111            replaced,
2112            self.mounts[old_index].cached_usage.as_mut(),
2113        ) {
2114            if (source.dev, source.ino) != (replaced.dev, replaced.ino)
2115                && (replaced.is_directory || replaced.nlink <= 1)
2116            {
2117                usage.inode_count = usage.inode_count.saturating_sub(1);
2118                if !replaced.is_directory {
2119                    usage.total_bytes = usage.total_bytes.saturating_sub(replaced.size);
2120                }
2121            }
2122        }
2123        Ok(())
2124    }
2125
2126    fn realpath(&self, path: &str) -> VfsResult<String> {
2127        let normalized = normalize_path(path);
2128        let (index, relative_path) = self.resolve_index(&normalized)?;
2129        let fallback_error = match self.realpath_in_mount(index, &relative_path) {
2130            Ok(resolved) => return Ok(resolved),
2131            // ELOOP and ENOENT may mean the mounted backend encountered a symlink
2132            // target expressed in the guest namespace. Walk components through the
2133            // mount table, but preserve the backend's original ENOENT when the walk
2134            // does not actually encounter a symlink.
2135            Err(error) if error.code() == "ELOOP" => None,
2136            Err(error) if error.code() == "ENOENT" => Some(error),
2137            Err(error) => return Err(error),
2138        };
2139
2140        let mut pending = path_components(&normalized);
2141        let mut current = String::from("/");
2142        let mut followed_symlinks = 0usize;
2143
2144        while let Some(component) = pending.pop_front() {
2145            let candidate = join_path(&current, &component);
2146            let stat = match self.lstat(&candidate) {
2147                Ok(stat) => stat,
2148                Err(error) => return Err(fallback_error.unwrap_or(error)),
2149            };
2150
2151            if stat.is_symbolic_link {
2152                followed_symlinks += 1;
2153                if followed_symlinks > MAX_REALPATH_SYMLINKS {
2154                    return Err(VfsError::new(
2155                        "ELOOP",
2156                        format!("too many levels of symbolic links, '{path}'"),
2157                    ));
2158                }
2159
2160                // Mounted filesystems express absolute symlink targets in their
2161                // own root namespace. Keep the mount index while reading the
2162                // link so the component-walk fallback does not accidentally
2163                // reinterpret `/target` as the VM root after crossing a
2164                // synthetic leaf mount.
2165                let (link_index, relative_path) = self.resolve_link_leaf_index(&candidate)?;
2166                let target = self.mounts[link_index]
2167                    .filesystem
2168                    .read_link(&relative_path)?;
2169                let target_path = if target.starts_with('/') {
2170                    let mount_path = &self.mounts[link_index].path;
2171                    let guest_absolute_target =
2172                        target == *mount_path || target.starts_with(&format!("{mount_path}/"));
2173                    if mount_path == "/" || guest_absolute_target {
2174                        normalize_path(&target)
2175                    } else {
2176                        normalize_path(&format!(
2177                            "{}/{}",
2178                            mount_path,
2179                            target.trim_start_matches('/')
2180                        ))
2181                    }
2182                } else {
2183                    normalize_path(&format!("{}/{}", parent_path(&candidate), target))
2184                };
2185                let mut resolved_target = path_components(&target_path);
2186                resolved_target.extend(pending);
2187                pending = resolved_target;
2188                current = String::from("/");
2189                continue;
2190            }
2191
2192            if !pending.is_empty() && !stat.is_directory {
2193                return Err(VfsError::new(
2194                    "ENOTDIR",
2195                    format!("not a directory, realpath '{candidate}'"),
2196                ));
2197            }
2198
2199            current = candidate;
2200        }
2201
2202        if followed_symlinks == 0 {
2203            if let Some(error) = fallback_error {
2204                return Err(error);
2205            }
2206        }
2207        Ok(current)
2208    }
2209
2210    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
2211        let normalized_link_path = normalize_path(link_path);
2212        let link_parent = parent_path(&normalized_link_path);
2213        let absolute_target = if target.starts_with('/') {
2214            normalize_path(target)
2215        } else {
2216            normalize_path(&format!("{link_parent}/{target}"))
2217        };
2218
2219        let (index, relative_path) = self.resolve_index(&normalized_link_path)?;
2220        let (target_index, _) = self.resolve_index(&absolute_target)?;
2221        if index != target_index {
2222            return Err(VfsError::new(
2223                "EXDEV",
2224                format!("symlink across mounts: {link_path} -> {target}"),
2225            ));
2226        }
2227        self.ensure_writable(index, link_path)?;
2228        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2229        self.check_file_growth(index, &relative_path, target.len() as u64, true)?;
2230
2231        self.mounts[index]
2232            .filesystem
2233            .symlink(target, &relative_path)?;
2234        self.update_cached_path_usage(index, before, &relative_path);
2235        Ok(())
2236    }
2237
2238    fn read_link(&self, path: &str) -> VfsResult<String> {
2239        let (index, relative_path) = self.resolve_link_leaf_index(path)?;
2240        self.mounts[index].filesystem.read_link(&relative_path)
2241    }
2242
2243    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
2244        let (index, relative_path) = self.resolve_link_leaf_index(path)?;
2245        self.mounts[index].filesystem.lstat(&relative_path)
2246    }
2247
2248    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
2249        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
2250        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
2251        if old_index != new_index {
2252            return Err(VfsError::new(
2253                "EXDEV",
2254                format!("link across mounts: {old_path} -> {new_path}"),
2255            ));
2256        }
2257        self.ensure_writable(new_index, new_path)?;
2258
2259        self.mounts[old_index]
2260            .filesystem
2261            .link(&old_relative_path, &new_relative_path)
2262    }
2263
2264    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
2265        let (index, relative_path) = self.resolve_writable_index(path)?;
2266        self.mounts[index].filesystem.chmod(&relative_path, mode)
2267    }
2268
2269    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
2270        self.chown_spec(path, uid, gid, true)
2271    }
2272
2273    fn chown_spec(
2274        &mut self,
2275        path: &str,
2276        uid: u32,
2277        gid: u32,
2278        follow_symlinks: bool,
2279    ) -> VfsResult<()> {
2280        let (index, relative_path) = if follow_symlinks {
2281            self.resolve_index(path)?
2282        } else {
2283            self.resolve_link_leaf_index(path)?
2284        };
2285        self.ensure_writable(index, path)?;
2286        self.mounts[index]
2287            .filesystem
2288            .chown_spec(&relative_path, uid, gid, follow_symlinks)
2289    }
2290
2291    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
2292        let (index, relative_path) = self.resolve_link_leaf_index(path)?;
2293        self.ensure_writable(index, path)?;
2294        self.mounts[index]
2295            .filesystem
2296            .lchown(&relative_path, uid, gid)
2297    }
2298
2299    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
2300        let (index, relative_path) = if follow_symlinks {
2301            self.resolve_content_index(path)?
2302        } else {
2303            self.resolve_link_leaf_index(path)?
2304        };
2305        self.mounts[index]
2306            .filesystem
2307            .get_xattr(&relative_path, name, follow_symlinks)
2308    }
2309
2310    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
2311        let (index, relative_path) = if follow_symlinks {
2312            self.resolve_content_index(path)?
2313        } else {
2314            self.resolve_link_leaf_index(path)?
2315        };
2316        self.mounts[index]
2317            .filesystem
2318            .list_xattrs(&relative_path, follow_symlinks)
2319    }
2320
2321    fn set_xattr(
2322        &mut self,
2323        path: &str,
2324        name: &str,
2325        value: Vec<u8>,
2326        flags: u32,
2327        follow_symlinks: bool,
2328    ) -> VfsResult<()> {
2329        let (index, relative_path) = if follow_symlinks {
2330            self.resolve_content_index(path)?
2331        } else {
2332            self.resolve_link_leaf_index(path)?
2333        };
2334        self.ensure_writable(index, path)?;
2335        self.mounts[index]
2336            .filesystem
2337            .set_xattr(&relative_path, name, value, flags, follow_symlinks)
2338    }
2339
2340    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
2341        let (index, relative_path) = if follow_symlinks {
2342            self.resolve_content_index(path)?
2343        } else {
2344            self.resolve_link_leaf_index(path)?
2345        };
2346        self.ensure_writable(index, path)?;
2347        self.mounts[index]
2348            .filesystem
2349            .remove_xattr(&relative_path, name, follow_symlinks)
2350    }
2351
2352    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
2353        let (index, relative_path) = self.resolve_writable_index(path)?;
2354        self.mounts[index]
2355            .filesystem
2356            .utimes(&relative_path, atime_ms, mtime_ms)
2357    }
2358
2359    fn utimes_spec(
2360        &mut self,
2361        path: &str,
2362        atime: VirtualUtimeSpec,
2363        mtime: VirtualUtimeSpec,
2364        follow_symlinks: bool,
2365    ) -> VfsResult<()> {
2366        let (index, relative_path) = self.resolve_writable_index(path)?;
2367        self.mounts[index]
2368            .filesystem
2369            .utimes_spec(&relative_path, atime, mtime, follow_symlinks)
2370    }
2371
2372    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
2373        let (index, relative_path) = self.resolve_writable_index(path)?;
2374        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2375        self.check_file_growth(index, &relative_path, length, false)?;
2376        self.mounts[index]
2377            .filesystem
2378            .truncate(&relative_path, length)?;
2379        self.update_cached_path_usage(index, before, &relative_path);
2380        Ok(())
2381    }
2382
2383    fn sync(&mut self, path: &str) -> VfsResult<()> {
2384        let (index, relative_path) = self.resolve_content_index(path)?;
2385        self.mounts[index].filesystem.sync(&relative_path)
2386    }
2387
2388    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2389        let (index, relative_path) = self.resolve_content_index(path)?;
2390        self.ensure_writable(index, path)?;
2391        let before = self.mounts[index].filesystem.lstat(&relative_path)?;
2392        let current_size = before.size;
2393        self.check_file_growth(
2394            index,
2395            &relative_path,
2396            current_size.max(offset.saturating_add(length)),
2397            false,
2398        )?;
2399        self.mounts[index]
2400            .filesystem
2401            .allocate(&relative_path, offset, length)?;
2402        self.update_cached_path_usage(index, Some(before), &relative_path);
2403        Ok(())
2404    }
2405
2406    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2407        let (index, relative_path) = self.resolve_content_index(path)?;
2408        self.ensure_writable(index, path)?;
2409        let before = self.mounts[index].filesystem.lstat(&relative_path)?;
2410        let current_size = before.size;
2411        self.check_file_growth(
2412            index,
2413            &relative_path,
2414            current_size.saturating_add(length),
2415            false,
2416        )?;
2417        self.mounts[index]
2418            .filesystem
2419            .insert_range(&relative_path, offset, length)?;
2420        self.update_cached_path_usage(index, Some(before), &relative_path);
2421        Ok(())
2422    }
2423
2424    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2425        let (index, relative_path) = self.resolve_content_index(path)?;
2426        self.ensure_writable(index, path)?;
2427        let before = self.mounts[index].filesystem.lstat(&relative_path)?;
2428        self.mounts[index]
2429            .filesystem
2430            .collapse_range(&relative_path, offset, length)?;
2431        self.update_cached_path_usage(index, Some(before), &relative_path);
2432        Ok(())
2433    }
2434
2435    fn zero_range(
2436        &mut self,
2437        path: &str,
2438        offset: u64,
2439        length: u64,
2440        keep_size: bool,
2441    ) -> VfsResult<()> {
2442        let (index, relative_path) = self.resolve_content_index(path)?;
2443        self.ensure_writable(index, path)?;
2444        let before = self.mounts[index].filesystem.lstat(&relative_path)?;
2445        if !keep_size {
2446            let current_size = before.size;
2447            self.check_file_growth(
2448                index,
2449                &relative_path,
2450                current_size.max(offset.saturating_add(length)),
2451                false,
2452            )?;
2453        }
2454        self.mounts[index]
2455            .filesystem
2456            .zero_range(&relative_path, offset, length, keep_size)?;
2457        self.update_cached_path_usage(index, Some(before), &relative_path);
2458        Ok(())
2459    }
2460
2461    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2462        let (index, relative_path) = self.resolve_content_index(path)?;
2463        self.ensure_writable(index, path)?;
2464        self.mounts[index]
2465            .filesystem
2466            .punch_hole(&relative_path, offset, length)
2467    }
2468
2469    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2470        let (index, relative_path) = self.resolve_content_index(path)?;
2471        self.mounts[index]
2472            .filesystem
2473            .allocated_ranges(&relative_path)
2474    }
2475
2476    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2477        let (index, relative_path) = self.resolve_content_index(path)?;
2478        self.mounts[index]
2479            .filesystem
2480            .unwritten_ranges(&relative_path)
2481    }
2482
2483    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
2484        let (index, relative_path) = self.resolve_content_index(path)?;
2485        let before = self.atime_snapshot(index, &relative_path, false)?;
2486        let content = self.mounts[index]
2487            .filesystem
2488            .pread(&relative_path, offset, length)?;
2489        self.finish_atime_update(index, &relative_path, before)?;
2490        Ok(content)
2491    }
2492
2493    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
2494        let content = content.into();
2495        let (index, relative_path) = self.resolve_content_index(path)?;
2496        self.ensure_writable(index, path)?;
2497        let before = self.mounts[index].filesystem.lstat(&relative_path).ok();
2498        let current_size = before.as_ref().map_or(0, |stat| stat.size);
2499        self.check_file_growth(
2500            index,
2501            &relative_path,
2502            current_size.max(offset.saturating_add(content.len() as u64)),
2503            false,
2504        )?;
2505        self.mounts[index]
2506            .filesystem
2507            .pwrite(&relative_path, content, offset)?;
2508        self.update_cached_path_usage(index, before, &relative_path);
2509        Ok(())
2510    }
2511}
2512
2513fn parse_mount_limit(option: &str, name: &str) -> VfsResult<u64> {
2514    let value = option
2515        .strip_prefix(&format!("{name}="))
2516        .ok_or_else(|| VfsError::new("EINVAL", format!("invalid mount option: {option}")))?;
2517    let parsed = value.parse::<u64>().map_err(|_| {
2518        VfsError::new(
2519            "EINVAL",
2520            format!("mount option {name} requires an unsigned integer"),
2521        )
2522    })?;
2523    if parsed == 0 {
2524        return Err(VfsError::new(
2525            "EINVAL",
2526            format!("mount option {name} must be greater than zero"),
2527        ));
2528    }
2529    Ok(parsed)
2530}
2531
2532fn check_usage_limits(
2533    usage: &FileSystemUsage,
2534    max_bytes: Option<u64>,
2535    max_inodes: Option<usize>,
2536) -> VfsResult<()> {
2537    if max_bytes.is_some_and(|limit| usage.total_bytes > limit) {
2538        return Err(VfsError::new(
2539            "ENOSPC",
2540            format!(
2541                "filesystem byte limit exceeded: {} bytes used",
2542                usage.total_bytes
2543            ),
2544        ));
2545    }
2546    if max_inodes.is_some_and(|limit| usage.inode_count > limit) {
2547        return Err(VfsError::new(
2548            "ENOSPC",
2549            format!(
2550                "filesystem inode limit exceeded: {} inodes used",
2551                usage.inode_count
2552            ),
2553        ));
2554    }
2555    Ok(())
2556}
2557
2558fn normalize_path(path: &str) -> String {
2559    let mut segments = Vec::new();
2560    for component in Path::new(path).components() {
2561        match component {
2562            Component::RootDir => segments.clear(),
2563            Component::ParentDir => {
2564                segments.pop();
2565            }
2566            Component::CurDir => {}
2567            Component::Normal(value) => segments.push(value.to_string_lossy().into_owned()),
2568            Component::Prefix(prefix) => {
2569                segments.push(prefix.as_os_str().to_string_lossy().into_owned());
2570            }
2571        }
2572    }
2573
2574    if segments.is_empty() {
2575        String::from("/")
2576    } else {
2577        format!("/{}", segments.join("/"))
2578    }
2579}
2580
2581fn timestamp_ns(milliseconds: u64, nanoseconds: u32) -> u128 {
2582    u128::from(milliseconds) * 1_000_000 + u128::from(nanoseconds % 1_000_000)
2583}
2584
2585fn path_components(path: &str) -> VecDeque<String> {
2586    normalize_path(path)
2587        .split('/')
2588        .filter(|part| !part.is_empty())
2589        .map(String::from)
2590        .collect()
2591}
2592
2593fn join_path(parent: &str, child: &str) -> String {
2594    if parent == "/" {
2595        format!("/{child}")
2596    } else {
2597        format!("{parent}/{child}")
2598    }
2599}
2600
2601fn parent_path(path: &str) -> String {
2602    let normalized = normalize_path(path);
2603    let parent = Path::new(&normalized)
2604        .parent()
2605        .unwrap_or_else(|| Path::new("/"));
2606    let value = parent.to_string_lossy();
2607    if value.is_empty() {
2608        String::from("/")
2609    } else {
2610        value.into_owned()
2611    }
2612}
2613
2614fn basename(path: &str) -> String {
2615    let normalized = normalize_path(path);
2616    Path::new(&normalized)
2617        .file_name()
2618        .map(|name| name.to_string_lossy().into_owned())
2619        .unwrap_or_else(|| String::from("/"))
2620}