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