Skip to main content

secure_exec_vfs_core/posix/
mount_table.rs

1use super::root_fs::RootFileSystem;
2use super::usage::FileSystemUsage;
3use super::vfs::{
4    VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualUtimeSpec,
5};
6use std::any::Any;
7use std::collections::BTreeSet;
8use std::path::{Component, Path};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11pub trait MountedFileSystem: Any {
12    fn as_any(&self) -> &dyn Any;
13    fn as_any_mut(&mut self) -> &mut dyn Any;
14    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
15    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
16    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
17        let entries = self.read_dir(path)?;
18        if entries.len() > max_entries {
19            return Err(VfsError::new(
20                "ENOMEM",
21                format!(
22                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
23                ),
24            ));
25        }
26        Ok(entries)
27    }
28    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
29    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()>;
30    fn write_file_with_mode(
31        &mut self,
32        path: &str,
33        content: Vec<u8>,
34        mode: Option<u32>,
35    ) -> VfsResult<()> {
36        let _ = mode;
37        self.write_file(path, content)
38    }
39    fn create_file_exclusive(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
40        if self.exists(path) {
41            return Err(VfsError::new(
42                "EEXIST",
43                format!("file already exists, open '{path}'"),
44            ));
45        }
46        self.write_file(path, content)
47    }
48    fn create_file_exclusive_with_mode(
49        &mut self,
50        path: &str,
51        content: Vec<u8>,
52        mode: Option<u32>,
53    ) -> VfsResult<()> {
54        let _ = mode;
55        self.create_file_exclusive(path, content)
56    }
57    fn append_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<u64> {
58        let mut existing = self.read_file(path)?;
59        existing.extend_from_slice(&content);
60        let new_len = existing.len() as u64;
61        self.write_file(path, existing)?;
62        Ok(new_len)
63    }
64    fn create_dir(&mut self, path: &str) -> VfsResult<()>;
65    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
66        let _ = mode;
67        self.create_dir(path)
68    }
69    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
70    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
71        let _ = mode;
72        self.mkdir(path, recursive)
73    }
74    fn exists(&self, path: &str) -> bool;
75    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
76    fn remove_file(&mut self, path: &str) -> VfsResult<()>;
77    fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
78    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
79    fn realpath(&self, path: &str) -> VfsResult<String>;
80    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
81    fn read_link(&self, path: &str) -> VfsResult<String>;
82    fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
83    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
84    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
85    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
86    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
87    fn utimes_spec(
88        &mut self,
89        path: &str,
90        atime: VirtualUtimeSpec,
91        mtime: VirtualUtimeSpec,
92        follow_symlinks: bool,
93    ) -> VfsResult<()> {
94        if !follow_symlinks {
95            return Err(VfsError::unsupported(format!(
96                "lutimes is not supported for mount path '{path}'"
97            )));
98        }
99        let existing = match (atime, mtime) {
100            (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
101            _ => None,
102        };
103        let now_ms = SystemTime::now()
104            .duration_since(UNIX_EPOCH)
105            .unwrap_or_default()
106            .as_millis() as u64;
107        let atime_ms = match atime {
108            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
109            VirtualUtimeSpec::Now => now_ms,
110            VirtualUtimeSpec::Omit => {
111                existing
112                    .as_ref()
113                    .ok_or_else(|| {
114                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
115                    })?
116                    .atime_ms
117            }
118        };
119        let mtime_ms = match mtime {
120            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
121            VirtualUtimeSpec::Now => now_ms,
122            VirtualUtimeSpec::Omit => {
123                existing
124                    .as_ref()
125                    .ok_or_else(|| {
126                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
127                    })?
128                    .mtime_ms
129            }
130        };
131        self.utimes(path, atime_ms, mtime_ms)
132    }
133    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
134    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
135    fn shutdown(&mut self) -> VfsResult<()> {
136        Ok(())
137    }
138}
139
140pub struct MountedVirtualFileSystem<F> {
141    inner: F,
142}
143
144impl<F> MountedVirtualFileSystem<F> {
145    pub fn new(inner: F) -> Self {
146        Self { inner }
147    }
148
149    pub fn inner(&self) -> &F {
150        &self.inner
151    }
152
153    pub fn inner_mut(&mut self) -> &mut F {
154        &mut self.inner
155    }
156}
157
158impl<F> MountedFileSystem for MountedVirtualFileSystem<F>
159where
160    F: VirtualFileSystem + 'static,
161{
162    fn as_any(&self) -> &dyn Any {
163        self
164    }
165
166    fn as_any_mut(&mut self) -> &mut dyn Any {
167        self
168    }
169
170    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
171        VirtualFileSystem::read_file(&mut self.inner, path)
172    }
173
174    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
175        VirtualFileSystem::read_dir(&mut self.inner, path)
176    }
177
178    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
179        VirtualFileSystem::read_dir_limited(&mut self.inner, path, max_entries)
180    }
181
182    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
183        VirtualFileSystem::read_dir_with_types(&mut self.inner, path)
184    }
185
186    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
187        VirtualFileSystem::write_file(&mut self.inner, path, content)
188    }
189
190    fn write_file_with_mode(
191        &mut self,
192        path: &str,
193        content: Vec<u8>,
194        mode: Option<u32>,
195    ) -> VfsResult<()> {
196        VirtualFileSystem::write_file_with_mode(&mut self.inner, path, content, mode)
197    }
198
199    fn create_file_exclusive(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
200        VirtualFileSystem::create_file_exclusive(&mut self.inner, path, content)
201    }
202
203    fn create_file_exclusive_with_mode(
204        &mut self,
205        path: &str,
206        content: Vec<u8>,
207        mode: Option<u32>,
208    ) -> VfsResult<()> {
209        VirtualFileSystem::create_file_exclusive_with_mode(&mut self.inner, path, content, mode)
210    }
211
212    fn append_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<u64> {
213        VirtualFileSystem::append_file(&mut self.inner, path, content)
214    }
215
216    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
217        VirtualFileSystem::create_dir(&mut self.inner, path)
218    }
219
220    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
221        VirtualFileSystem::create_dir_with_mode(&mut self.inner, path, mode)
222    }
223
224    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
225        VirtualFileSystem::mkdir(&mut self.inner, path, recursive)
226    }
227
228    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
229        VirtualFileSystem::mkdir_with_mode(&mut self.inner, path, recursive, mode)
230    }
231
232    fn exists(&self, path: &str) -> bool {
233        VirtualFileSystem::exists(&self.inner, path)
234    }
235
236    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
237        VirtualFileSystem::stat(&mut self.inner, path)
238    }
239
240    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
241        VirtualFileSystem::remove_file(&mut self.inner, path)
242    }
243
244    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
245        VirtualFileSystem::remove_dir(&mut self.inner, path)
246    }
247
248    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
249        VirtualFileSystem::rename(&mut self.inner, old_path, new_path)
250    }
251
252    fn realpath(&self, path: &str) -> VfsResult<String> {
253        VirtualFileSystem::realpath(&self.inner, path)
254    }
255
256    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
257        VirtualFileSystem::symlink(&mut self.inner, target, link_path)
258    }
259
260    fn read_link(&self, path: &str) -> VfsResult<String> {
261        VirtualFileSystem::read_link(&self.inner, path)
262    }
263
264    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
265        VirtualFileSystem::lstat(&self.inner, path)
266    }
267
268    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
269        VirtualFileSystem::link(&mut self.inner, old_path, new_path)
270    }
271
272    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
273        VirtualFileSystem::chmod(&mut self.inner, path, mode)
274    }
275
276    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
277        VirtualFileSystem::chown(&mut self.inner, path, uid, gid)
278    }
279
280    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
281        VirtualFileSystem::utimes(&mut self.inner, path, atime_ms, mtime_ms)
282    }
283
284    fn utimes_spec(
285        &mut self,
286        path: &str,
287        atime: VirtualUtimeSpec,
288        mtime: VirtualUtimeSpec,
289        follow_symlinks: bool,
290    ) -> VfsResult<()> {
291        VirtualFileSystem::utimes_spec(&mut self.inner, path, atime, mtime, follow_symlinks)
292    }
293
294    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
295        VirtualFileSystem::truncate(&mut self.inner, path, length)
296    }
297
298    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
299        VirtualFileSystem::pread(&mut self.inner, path, offset, length)
300    }
301}
302
303impl<T> MountedFileSystem for Box<T>
304where
305    T: MountedFileSystem + ?Sized + 'static,
306{
307    fn as_any(&self) -> &dyn Any {
308        (**self).as_any()
309    }
310
311    fn as_any_mut(&mut self) -> &mut dyn Any {
312        (**self).as_any_mut()
313    }
314
315    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
316        (**self).read_file(path)
317    }
318
319    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
320        (**self).read_dir(path)
321    }
322
323    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
324        (**self).read_dir_limited(path, max_entries)
325    }
326
327    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
328        (**self).read_dir_with_types(path)
329    }
330
331    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
332        (**self).write_file(path, content)
333    }
334
335    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
336        (**self).create_dir(path)
337    }
338
339    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
340        (**self).mkdir(path, recursive)
341    }
342
343    fn exists(&self, path: &str) -> bool {
344        (**self).exists(path)
345    }
346
347    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
348        (**self).stat(path)
349    }
350
351    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
352        (**self).remove_file(path)
353    }
354
355    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
356        (**self).remove_dir(path)
357    }
358
359    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
360        (**self).rename(old_path, new_path)
361    }
362
363    fn realpath(&self, path: &str) -> VfsResult<String> {
364        (**self).realpath(path)
365    }
366
367    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
368        (**self).symlink(target, link_path)
369    }
370
371    fn read_link(&self, path: &str) -> VfsResult<String> {
372        (**self).read_link(path)
373    }
374
375    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
376        (**self).lstat(path)
377    }
378
379    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
380        (**self).link(old_path, new_path)
381    }
382
383    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
384        (**self).chmod(path, mode)
385    }
386
387    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
388        (**self).chown(path, uid, gid)
389    }
390
391    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
392        (**self).utimes(path, atime_ms, mtime_ms)
393    }
394
395    fn utimes_spec(
396        &mut self,
397        path: &str,
398        atime: VirtualUtimeSpec,
399        mtime: VirtualUtimeSpec,
400        follow_symlinks: bool,
401    ) -> VfsResult<()> {
402        (**self).utimes_spec(path, atime, mtime, follow_symlinks)
403    }
404
405    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
406        (**self).truncate(path, length)
407    }
408
409    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
410        (**self).pread(path, offset, length)
411    }
412
413    fn shutdown(&mut self) -> VfsResult<()> {
414        (**self).shutdown()
415    }
416}
417
418pub struct ReadOnlyFileSystem<F> {
419    inner: F,
420}
421
422impl<F> ReadOnlyFileSystem<F> {
423    pub fn new(inner: F) -> Self {
424        Self { inner }
425    }
426}
427
428impl<F> MountedFileSystem for ReadOnlyFileSystem<F>
429where
430    F: MountedFileSystem + 'static,
431{
432    fn as_any(&self) -> &dyn Any {
433        self
434    }
435
436    fn as_any_mut(&mut self) -> &mut dyn Any {
437        self
438    }
439
440    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
441        self.inner.read_file(path)
442    }
443
444    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
445        self.inner.read_dir(path)
446    }
447
448    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
449        self.inner.read_dir_limited(path, max_entries)
450    }
451
452    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
453        self.inner.read_dir_with_types(path)
454    }
455
456    fn write_file(&mut self, path: &str, _content: Vec<u8>) -> VfsResult<()> {
457        Err(VfsError::new(
458            "EROFS",
459            format!("read-only filesystem: {path}"),
460        ))
461    }
462
463    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
464        Err(VfsError::new(
465            "EROFS",
466            format!("read-only filesystem: {path}"),
467        ))
468    }
469
470    fn mkdir(&mut self, path: &str, _recursive: bool) -> VfsResult<()> {
471        Err(VfsError::new(
472            "EROFS",
473            format!("read-only filesystem: {path}"),
474        ))
475    }
476
477    fn exists(&self, path: &str) -> bool {
478        self.inner.exists(path)
479    }
480
481    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
482        self.inner.stat(path)
483    }
484
485    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
486        Err(VfsError::new(
487            "EROFS",
488            format!("read-only filesystem: {path}"),
489        ))
490    }
491
492    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
493        Err(VfsError::new(
494            "EROFS",
495            format!("read-only filesystem: {path}"),
496        ))
497    }
498
499    fn rename(&mut self, old_path: &str, _new_path: &str) -> VfsResult<()> {
500        Err(VfsError::new(
501            "EROFS",
502            format!("read-only filesystem: {old_path}"),
503        ))
504    }
505
506    fn realpath(&self, path: &str) -> VfsResult<String> {
507        self.inner.realpath(path)
508    }
509
510    fn symlink(&mut self, _target: &str, link_path: &str) -> VfsResult<()> {
511        Err(VfsError::new(
512            "EROFS",
513            format!("read-only filesystem: {link_path}"),
514        ))
515    }
516
517    fn read_link(&self, path: &str) -> VfsResult<String> {
518        self.inner.read_link(path)
519    }
520
521    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
522        self.inner.lstat(path)
523    }
524
525    fn link(&mut self, _old_path: &str, new_path: &str) -> VfsResult<()> {
526        Err(VfsError::new(
527            "EROFS",
528            format!("read-only filesystem: {new_path}"),
529        ))
530    }
531
532    fn chmod(&mut self, path: &str, _mode: u32) -> VfsResult<()> {
533        Err(VfsError::new(
534            "EROFS",
535            format!("read-only filesystem: {path}"),
536        ))
537    }
538
539    fn chown(&mut self, path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
540        Err(VfsError::new(
541            "EROFS",
542            format!("read-only filesystem: {path}"),
543        ))
544    }
545
546    fn utimes(&mut self, path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
547        Err(VfsError::new(
548            "EROFS",
549            format!("read-only filesystem: {path}"),
550        ))
551    }
552
553    fn utimes_spec(
554        &mut self,
555        path: &str,
556        _atime: VirtualUtimeSpec,
557        _mtime: VirtualUtimeSpec,
558        _follow_symlinks: bool,
559    ) -> VfsResult<()> {
560        Err(VfsError::new(
561            "EROFS",
562            format!("read-only filesystem: {path}"),
563        ))
564    }
565
566    fn truncate(&mut self, path: &str, _length: u64) -> VfsResult<()> {
567        Err(VfsError::new(
568            "EROFS",
569            format!("read-only filesystem: {path}"),
570        ))
571    }
572
573    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
574        self.inner.pread(path, offset, length)
575    }
576
577    fn shutdown(&mut self) -> VfsResult<()> {
578        self.inner.shutdown()
579    }
580}
581
582#[derive(Debug, Clone, PartialEq, Eq)]
583pub struct MountEntry {
584    pub path: String,
585    pub plugin_id: String,
586    pub read_only: bool,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct MountOptions {
591    pub plugin_id: String,
592    pub read_only: bool,
593}
594
595impl MountOptions {
596    pub fn new(plugin_id: impl Into<String>) -> Self {
597        Self {
598            plugin_id: plugin_id.into(),
599            read_only: false,
600        }
601    }
602
603    pub fn read_only(mut self, read_only: bool) -> Self {
604        self.read_only = read_only;
605        self
606    }
607}
608
609struct MountRegistration {
610    path: String,
611    plugin_id: String,
612    read_only: bool,
613    filesystem: Box<dyn MountedFileSystem>,
614}
615
616pub struct MountTable {
617    mounts: Vec<MountRegistration>,
618}
619
620impl MountTable {
621    pub fn new(root_fs: impl VirtualFileSystem + 'static) -> Self {
622        Self {
623            mounts: vec![MountRegistration {
624                path: String::from("/"),
625                plugin_id: String::from("root"),
626                read_only: false,
627                filesystem: Box::new(MountedVirtualFileSystem::new(root_fs)),
628            }],
629        }
630    }
631
632    pub fn new_boxed_root(filesystem: Box<dyn MountedFileSystem>, options: MountOptions) -> Self {
633        let filesystem = if options.read_only {
634            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
635        } else {
636            filesystem
637        };
638
639        Self {
640            mounts: vec![MountRegistration {
641                path: String::from("/"),
642                plugin_id: options.plugin_id,
643                read_only: options.read_only,
644                filesystem,
645            }],
646        }
647    }
648
649    pub fn mount(
650        &mut self,
651        path: &str,
652        filesystem: impl VirtualFileSystem + 'static,
653        options: MountOptions,
654    ) -> VfsResult<()> {
655        self.mount_boxed(
656            path,
657            Box::new(MountedVirtualFileSystem::new(filesystem)),
658            options,
659        )
660    }
661
662    pub fn mount_boxed(
663        &mut self,
664        path: &str,
665        mut filesystem: Box<dyn MountedFileSystem>,
666        options: MountOptions,
667    ) -> VfsResult<()> {
668        let normalized = normalize_path(path);
669        if normalized == "/" {
670            return Err(VfsError::new("EINVAL", "cannot mount over root"));
671        }
672        if self.mounts.iter().any(|mount| mount.path == normalized) {
673            return Err(VfsError::new(
674                "EEXIST",
675                format!("already mounted at {normalized}"),
676            ));
677        }
678
679        let (parent_index, relative_path) = self.resolve_index(&normalized)?;
680        let parent_mount = &mut self.mounts[parent_index];
681        if !parent_mount.filesystem.exists(&relative_path) {
682            // Materializing the mountpoint directory on the parent is
683            // cosmetic: child mounts resolve by path prefix before the parent
684            // is consulted. A read-only parent (for example a read-only
685            // module-access mount hosting nested package mounts) cannot
686            // materialize the entry, but the mount must still succeed.
687            if let Err(error) = parent_mount.filesystem.mkdir(&relative_path, true) {
688                if error.code() != "EROFS" {
689                    if let Err(shutdown_error) = filesystem.shutdown() {
690                        return Err(VfsError::new(
691                            shutdown_error.code(),
692                            format!(
693                                "failed to shut down filesystem after mount failure ({error}): {}",
694                                shutdown_error.message()
695                            ),
696                        ));
697                    }
698
699                    return Err(error);
700                }
701            }
702        }
703
704        let filesystem = if options.read_only {
705            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
706        } else {
707            filesystem
708        };
709
710        self.mounts.push(MountRegistration {
711            path: normalized,
712            plugin_id: options.plugin_id,
713            read_only: options.read_only,
714            filesystem,
715        });
716        self.mounts
717            .sort_by_key(|mount| std::cmp::Reverse(mount.path.len()));
718        Ok(())
719    }
720
721    pub fn unmount(&mut self, path: &str) -> VfsResult<()> {
722        let normalized = normalize_path(path);
723        if normalized == "/" {
724            return Err(VfsError::new("EINVAL", "cannot unmount root"));
725        }
726
727        let child_mount_prefix = format!("{normalized}/");
728        if self
729            .mounts
730            .iter()
731            .any(|mount| mount.path.starts_with(&child_mount_prefix))
732        {
733            return Err(VfsError::new(
734                "EBUSY",
735                format!("mount point has child mounts: {normalized}"),
736            ));
737        }
738
739        let Some(index) = self
740            .mounts
741            .iter()
742            .position(|mount| mount.path == normalized)
743        else {
744            return Err(VfsError::new(
745                "EINVAL",
746                format!("not a mount point: {normalized}"),
747            ));
748        };
749
750        let mut mount = self.mounts.remove(index);
751        mount.filesystem.shutdown()?;
752        Ok(())
753    }
754
755    pub fn get_mounts(&self) -> Vec<MountEntry> {
756        self.mounts
757            .iter()
758            .map(|mount| MountEntry {
759                path: mount.path.clone(),
760                plugin_id: mount.plugin_id.clone(),
761                read_only: mount.read_only,
762            })
763            .collect()
764    }
765
766    pub fn root_virtual_filesystem_mut<T: VirtualFileSystem + 'static>(
767        &mut self,
768    ) -> Option<&mut T> {
769        let root = self.mounts.iter_mut().find(|mount| mount.path == "/")?;
770        root.filesystem
771            .as_any_mut()
772            .downcast_mut::<MountedVirtualFileSystem<T>>()
773            .map(MountedVirtualFileSystem::inner_mut)
774    }
775
776    pub fn check_rename_copy_up_limits(
777        &mut self,
778        old_path: &str,
779        new_path: &str,
780        max_bytes: Option<u64>,
781        max_inodes: Option<usize>,
782    ) -> VfsResult<()> {
783        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
784        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
785        if old_index != new_index {
786            return Ok(());
787        }
788
789        let filesystem = &mut self.mounts[old_index].filesystem;
790        if let Some(root) = filesystem
791            .as_any_mut()
792            .downcast_mut::<MountedVirtualFileSystem<RootFileSystem>>()
793        {
794            root.inner_mut().check_rename_copy_up_limits(
795                &old_relative_path,
796                &new_relative_path,
797                max_bytes,
798                max_inodes,
799            )?;
800        }
801
802        Ok(())
803    }
804
805    pub fn root_usage(&mut self) -> VfsResult<FileSystemUsage> {
806        let root = self
807            .mounts
808            .iter_mut()
809            .find(|mount| mount.path == "/")
810            .ok_or_else(|| VfsError::new("ENOENT", "missing root mount"))?;
811        measure_mounted_filesystem_usage(root.filesystem.as_mut(), "/", &mut BTreeSet::new())
812    }
813
814    fn resolve_index(&self, full_path: &str) -> VfsResult<(usize, String)> {
815        let normalized = normalize_path(full_path);
816        for (index, mount) in self.mounts.iter().enumerate() {
817            if mount.path == "/" {
818                return Ok((index, normalized));
819            }
820            if normalized == mount.path {
821                return Ok((index, String::from("/")));
822            }
823            let mount_prefix = format!("{}/", mount.path);
824            if let Some(suffix) = normalized.strip_prefix(&mount_prefix) {
825                // Strip exactly the mount prefix once. `trim_start_matches` would
826                // strip every leading repetition of `mount.path`, so a path like
827                // `/data/data/file` under mount `/data` would alias to `/file`
828                // instead of `/data/file`, routing reads/writes to the wrong file.
829                return Ok((index, format!("/{suffix}")));
830            }
831        }
832
833        Err(VfsError::new(
834            "ENOENT",
835            format!("no such file or directory, resolve '{full_path}'"),
836        ))
837    }
838
839    fn child_mount_basenames(&self, path: &str) -> Vec<String> {
840        let normalized = normalize_path(path);
841        let mut basenames = BTreeSet::new();
842        for mount in &self.mounts {
843            if mount.path == "/" || mount.path == normalized {
844                continue;
845            }
846
847            if parent_path(&mount.path) == normalized {
848                basenames.insert(basename(&mount.path));
849            }
850        }
851        basenames.into_iter().collect()
852    }
853}
854
855fn measure_mounted_filesystem_usage(
856    filesystem: &mut dyn MountedFileSystem,
857    path: &str,
858    visited: &mut BTreeSet<u64>,
859) -> VfsResult<FileSystemUsage> {
860    let stat = filesystem.lstat(path)?;
861    let mut usage = FileSystemUsage::default();
862
863    if visited.insert(stat.ino) {
864        usage.inode_count += 1;
865        if !stat.is_directory {
866            usage.total_bytes = usage.total_bytes.saturating_add(stat.size);
867        }
868    }
869
870    if !stat.is_directory || stat.is_symbolic_link {
871        return Ok(usage);
872    }
873
874    for entry in filesystem.read_dir_with_types(path)? {
875        if matches!(entry.name.as_str(), "." | "..") {
876            continue;
877        }
878
879        let child_path = if path == "/" {
880            format!("/{}", entry.name)
881        } else {
882            format!("{path}/{}", entry.name)
883        };
884        let child_usage = measure_mounted_filesystem_usage(filesystem, &child_path, visited)?;
885        usage.total_bytes = usage.total_bytes.saturating_add(child_usage.total_bytes);
886        usage.inode_count = usage.inode_count.saturating_add(child_usage.inode_count);
887    }
888
889    Ok(usage)
890}
891
892impl Drop for MountTable {
893    fn drop(&mut self) {
894        for mount in self.mounts.iter_mut().rev() {
895            let _ = mount.filesystem.shutdown();
896        }
897    }
898}
899
900impl VirtualFileSystem for MountTable {
901    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
902        let (index, relative_path) = self.resolve_index(path)?;
903        self.mounts[index].filesystem.read_file(&relative_path)
904    }
905
906    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
907        let normalized = normalize_path(path);
908        let (index, relative_path) = self.resolve_index(&normalized)?;
909        let mut entries = self.mounts[index].filesystem.read_dir(&relative_path)?;
910        let child_mounts = self.child_mount_basenames(&normalized);
911        if child_mounts.is_empty() {
912            return Ok(entries);
913        }
914
915        let mut merged = BTreeSet::new();
916        merged.extend(entries.drain(..));
917        merged.extend(child_mounts);
918        Ok(merged.into_iter().collect())
919    }
920
921    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
922        let normalized = normalize_path(path);
923        let (index, relative_path) = self.resolve_index(&normalized)?;
924        let mut entries = self.mounts[index]
925            .filesystem
926            .read_dir_limited(&relative_path, max_entries)?;
927        let child_mounts = self.child_mount_basenames(&normalized);
928        if child_mounts.is_empty() {
929            return Ok(entries);
930        }
931
932        let mut merged = BTreeSet::new();
933        merged.extend(entries.drain(..));
934        merged.extend(child_mounts);
935        if merged.len() > max_entries {
936            return Err(VfsError::new(
937                "ENOMEM",
938                format!(
939                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
940                ),
941            ));
942        }
943        Ok(merged.into_iter().collect())
944    }
945
946    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
947        let normalized = normalize_path(path);
948        let (index, relative_path) = self.resolve_index(&normalized)?;
949        let mut entries = self.mounts[index]
950            .filesystem
951            .read_dir_with_types(&relative_path)?;
952        let child_mounts = self.child_mount_basenames(&normalized);
953        if child_mounts.is_empty() {
954            return Ok(entries);
955        }
956
957        let existing = entries
958            .iter()
959            .map(|entry| entry.name.clone())
960            .collect::<BTreeSet<_>>();
961        for mount_name in child_mounts {
962            if existing.contains(&mount_name) {
963                continue;
964            }
965            entries.push(VirtualDirEntry {
966                name: mount_name,
967                is_directory: true,
968                is_symbolic_link: false,
969            });
970        }
971        Ok(entries)
972    }
973
974    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
975        let (index, relative_path) = self.resolve_index(path)?;
976        self.mounts[index]
977            .filesystem
978            .write_file(&relative_path, content.into())
979    }
980
981    fn write_file_with_mode(
982        &mut self,
983        path: &str,
984        content: impl Into<Vec<u8>>,
985        mode: Option<u32>,
986    ) -> VfsResult<()> {
987        let (index, relative_path) = self.resolve_index(path)?;
988        self.mounts[index]
989            .filesystem
990            .write_file_with_mode(&relative_path, content.into(), mode)
991    }
992
993    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
994        let (index, relative_path) = self.resolve_index(path)?;
995        self.mounts[index]
996            .filesystem
997            .create_file_exclusive(&relative_path, content.into())
998    }
999
1000    fn create_file_exclusive_with_mode(
1001        &mut self,
1002        path: &str,
1003        content: impl Into<Vec<u8>>,
1004        mode: Option<u32>,
1005    ) -> VfsResult<()> {
1006        let (index, relative_path) = self.resolve_index(path)?;
1007        self.mounts[index]
1008            .filesystem
1009            .create_file_exclusive_with_mode(&relative_path, content.into(), mode)
1010    }
1011
1012    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1013        let (index, relative_path) = self.resolve_index(path)?;
1014        self.mounts[index]
1015            .filesystem
1016            .append_file(&relative_path, content.into())
1017    }
1018
1019    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1020        let (index, relative_path) = self.resolve_index(path)?;
1021        self.mounts[index].filesystem.create_dir(&relative_path)
1022    }
1023
1024    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
1025        let (index, relative_path) = self.resolve_index(path)?;
1026        self.mounts[index]
1027            .filesystem
1028            .create_dir_with_mode(&relative_path, mode)
1029    }
1030
1031    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1032        let (index, relative_path) = self.resolve_index(path)?;
1033        self.mounts[index]
1034            .filesystem
1035            .mkdir(&relative_path, recursive)
1036    }
1037
1038    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
1039        let (index, relative_path) = self.resolve_index(path)?;
1040        self.mounts[index]
1041            .filesystem
1042            .mkdir_with_mode(&relative_path, recursive, mode)
1043    }
1044
1045    fn exists(&self, path: &str) -> bool {
1046        self.resolve_index(path)
1047            .map(|(index, relative_path)| self.mounts[index].filesystem.exists(&relative_path))
1048            .unwrap_or(false)
1049    }
1050
1051    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1052        let (index, relative_path) = self.resolve_index(path)?;
1053        self.mounts[index].filesystem.stat(&relative_path)
1054    }
1055
1056    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1057        let (index, relative_path) = self.resolve_index(path)?;
1058        self.mounts[index].filesystem.remove_file(&relative_path)
1059    }
1060
1061    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1062        let (index, relative_path) = self.resolve_index(path)?;
1063        self.mounts[index].filesystem.remove_dir(&relative_path)
1064    }
1065
1066    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1067        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
1068        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
1069        if old_index != new_index {
1070            return Err(VfsError::new(
1071                "EXDEV",
1072                format!("rename across mounts: {old_path} -> {new_path}"),
1073            ));
1074        }
1075
1076        self.mounts[old_index]
1077            .filesystem
1078            .rename(&old_relative_path, &new_relative_path)
1079    }
1080
1081    fn realpath(&self, path: &str) -> VfsResult<String> {
1082        let (index, relative_path) = self.resolve_index(path)?;
1083        let mount = &self.mounts[index];
1084        let resolved = mount.filesystem.realpath(&relative_path)?;
1085        if mount.path == "/" {
1086            return Ok(resolved);
1087        }
1088        if resolved == "/" {
1089            return Ok(mount.path.clone());
1090        }
1091        Ok(format!(
1092            "{}/{}",
1093            mount.path.trim_end_matches('/'),
1094            resolved.trim_start_matches('/')
1095        ))
1096    }
1097
1098    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1099        let normalized_link_path = normalize_path(link_path);
1100        let link_parent = parent_path(&normalized_link_path);
1101        let absolute_target = if target.starts_with('/') {
1102            normalize_path(target)
1103        } else {
1104            normalize_path(&format!("{link_parent}/{target}"))
1105        };
1106
1107        let (index, relative_path) = self.resolve_index(&normalized_link_path)?;
1108        let (target_index, _) = self.resolve_index(&absolute_target)?;
1109        if index != target_index {
1110            return Err(VfsError::new(
1111                "EXDEV",
1112                format!("symlink across mounts: {link_path} -> {target}"),
1113            ));
1114        }
1115
1116        self.mounts[index]
1117            .filesystem
1118            .symlink(target, &relative_path)
1119    }
1120
1121    fn read_link(&self, path: &str) -> VfsResult<String> {
1122        let (index, relative_path) = self.resolve_index(path)?;
1123        self.mounts[index].filesystem.read_link(&relative_path)
1124    }
1125
1126    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1127        let (index, relative_path) = self.resolve_index(path)?;
1128        self.mounts[index].filesystem.lstat(&relative_path)
1129    }
1130
1131    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1132        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
1133        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
1134        if old_index != new_index {
1135            return Err(VfsError::new(
1136                "EXDEV",
1137                format!("link across mounts: {old_path} -> {new_path}"),
1138            ));
1139        }
1140
1141        self.mounts[old_index]
1142            .filesystem
1143            .link(&old_relative_path, &new_relative_path)
1144    }
1145
1146    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1147        let (index, relative_path) = self.resolve_index(path)?;
1148        self.mounts[index].filesystem.chmod(&relative_path, mode)
1149    }
1150
1151    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1152        let (index, relative_path) = self.resolve_index(path)?;
1153        self.mounts[index]
1154            .filesystem
1155            .chown(&relative_path, uid, gid)
1156    }
1157
1158    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1159        let (index, relative_path) = self.resolve_index(path)?;
1160        self.mounts[index]
1161            .filesystem
1162            .utimes(&relative_path, atime_ms, mtime_ms)
1163    }
1164
1165    fn utimes_spec(
1166        &mut self,
1167        path: &str,
1168        atime: VirtualUtimeSpec,
1169        mtime: VirtualUtimeSpec,
1170        follow_symlinks: bool,
1171    ) -> VfsResult<()> {
1172        let (index, relative_path) = self.resolve_index(path)?;
1173        self.mounts[index]
1174            .filesystem
1175            .utimes_spec(&relative_path, atime, mtime, follow_symlinks)
1176    }
1177
1178    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1179        let (index, relative_path) = self.resolve_index(path)?;
1180        self.mounts[index]
1181            .filesystem
1182            .truncate(&relative_path, length)
1183    }
1184
1185    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1186        let (index, relative_path) = self.resolve_index(path)?;
1187        self.mounts[index]
1188            .filesystem
1189            .pread(&relative_path, offset, length)
1190    }
1191}
1192
1193fn normalize_path(path: &str) -> String {
1194    let mut segments = Vec::new();
1195    for component in Path::new(path).components() {
1196        match component {
1197            Component::RootDir => segments.clear(),
1198            Component::ParentDir => {
1199                segments.pop();
1200            }
1201            Component::CurDir => {}
1202            Component::Normal(value) => segments.push(value.to_string_lossy().into_owned()),
1203            Component::Prefix(prefix) => {
1204                segments.push(prefix.as_os_str().to_string_lossy().into_owned());
1205            }
1206        }
1207    }
1208
1209    if segments.is_empty() {
1210        String::from("/")
1211    } else {
1212        format!("/{}", segments.join("/"))
1213    }
1214}
1215
1216fn parent_path(path: &str) -> String {
1217    let normalized = normalize_path(path);
1218    let parent = Path::new(&normalized)
1219        .parent()
1220        .unwrap_or_else(|| Path::new("/"));
1221    let value = parent.to_string_lossy();
1222    if value.is_empty() {
1223        String::from("/")
1224    } else {
1225        value.into_owned()
1226    }
1227}
1228
1229fn basename(path: &str) -> String {
1230    let normalized = normalize_path(path);
1231    Path::new(&normalized)
1232        .file_name()
1233        .map(|name| name.to_string_lossy().into_owned())
1234        .unwrap_or_else(|| String::from("/"))
1235}