Skip to main content

vfs/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::collections::VecDeque;
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 mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
74        let _ = mode;
75        self.mkdir(path, recursive)
76    }
77    fn exists(&self, path: &str) -> bool;
78    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
79    fn remove_file(&mut self, path: &str) -> VfsResult<()>;
80    fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
81    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
82    fn realpath(&self, path: &str) -> VfsResult<String>;
83    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
84    fn read_link(&self, path: &str) -> VfsResult<String>;
85    fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
86    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
87    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
88    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
89    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
90    fn utimes_spec(
91        &mut self,
92        path: &str,
93        atime: VirtualUtimeSpec,
94        mtime: VirtualUtimeSpec,
95        follow_symlinks: bool,
96    ) -> VfsResult<()> {
97        if !follow_symlinks {
98            return Err(VfsError::unsupported(format!(
99                "lutimes is not supported for mount path '{path}'"
100            )));
101        }
102        let existing = match (atime, mtime) {
103            (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
104            _ => None,
105        };
106        let now_ms = SystemTime::now()
107            .duration_since(UNIX_EPOCH)
108            .unwrap_or_default()
109            .as_millis() as u64;
110        let atime_ms = match atime {
111            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
112            VirtualUtimeSpec::Now => now_ms,
113            VirtualUtimeSpec::Omit => {
114                existing
115                    .as_ref()
116                    .ok_or_else(|| {
117                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
118                    })?
119                    .atime_ms
120            }
121        };
122        let mtime_ms = match mtime {
123            VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis()?,
124            VirtualUtimeSpec::Now => now_ms,
125            VirtualUtimeSpec::Omit => {
126                existing
127                    .as_ref()
128                    .ok_or_else(|| {
129                        VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata")
130                    })?
131                    .mtime_ms
132            }
133        };
134        self.utimes(path, atime_ms, mtime_ms)
135    }
136    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
137    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
138    fn shutdown(&mut self) -> VfsResult<()> {
139        Ok(())
140    }
141}
142
143pub struct MountedVirtualFileSystem<F> {
144    inner: F,
145}
146
147impl<F> MountedVirtualFileSystem<F> {
148    pub fn new(inner: F) -> Self {
149        Self { inner }
150    }
151
152    pub fn inner(&self) -> &F {
153        &self.inner
154    }
155
156    pub fn inner_mut(&mut self) -> &mut F {
157        &mut self.inner
158    }
159}
160
161impl<F> MountedFileSystem for MountedVirtualFileSystem<F>
162where
163    F: VirtualFileSystem + 'static,
164{
165    fn as_any(&self) -> &dyn Any {
166        self
167    }
168
169    fn as_any_mut(&mut self) -> &mut dyn Any {
170        self
171    }
172
173    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
174        VirtualFileSystem::read_file(&mut self.inner, path)
175    }
176
177    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
178        VirtualFileSystem::read_dir(&mut self.inner, path)
179    }
180
181    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
182        VirtualFileSystem::read_dir_limited(&mut self.inner, path, max_entries)
183    }
184
185    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
186        VirtualFileSystem::read_dir_with_types(&mut self.inner, path)
187    }
188
189    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
190        VirtualFileSystem::write_file(&mut self.inner, path, content)
191    }
192
193    fn write_file_with_mode(
194        &mut self,
195        path: &str,
196        content: Vec<u8>,
197        mode: Option<u32>,
198    ) -> VfsResult<()> {
199        VirtualFileSystem::write_file_with_mode(&mut self.inner, path, content, mode)
200    }
201
202    fn create_file_exclusive(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
203        VirtualFileSystem::create_file_exclusive(&mut self.inner, path, content)
204    }
205
206    fn create_file_exclusive_with_mode(
207        &mut self,
208        path: &str,
209        content: Vec<u8>,
210        mode: Option<u32>,
211    ) -> VfsResult<()> {
212        VirtualFileSystem::create_file_exclusive_with_mode(&mut self.inner, path, content, mode)
213    }
214
215    fn append_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<u64> {
216        VirtualFileSystem::append_file(&mut self.inner, path, content)
217    }
218
219    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
220        VirtualFileSystem::create_dir(&mut self.inner, path)
221    }
222
223    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
224        VirtualFileSystem::create_dir_with_mode(&mut self.inner, path, mode)
225    }
226
227    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
228        VirtualFileSystem::mkdir(&mut self.inner, path, recursive)
229    }
230
231    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
232        VirtualFileSystem::mkdir_with_mode(&mut self.inner, path, recursive, mode)
233    }
234
235    fn exists(&self, path: &str) -> bool {
236        VirtualFileSystem::exists(&self.inner, path)
237    }
238
239    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
240        VirtualFileSystem::stat(&mut self.inner, path)
241    }
242
243    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
244        VirtualFileSystem::remove_file(&mut self.inner, path)
245    }
246
247    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
248        VirtualFileSystem::remove_dir(&mut self.inner, path)
249    }
250
251    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
252        VirtualFileSystem::rename(&mut self.inner, old_path, new_path)
253    }
254
255    fn realpath(&self, path: &str) -> VfsResult<String> {
256        VirtualFileSystem::realpath(&self.inner, path)
257    }
258
259    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
260        VirtualFileSystem::symlink(&mut self.inner, target, link_path)
261    }
262
263    fn read_link(&self, path: &str) -> VfsResult<String> {
264        VirtualFileSystem::read_link(&self.inner, path)
265    }
266
267    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
268        VirtualFileSystem::lstat(&self.inner, path)
269    }
270
271    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
272        VirtualFileSystem::link(&mut self.inner, old_path, new_path)
273    }
274
275    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
276        VirtualFileSystem::chmod(&mut self.inner, path, mode)
277    }
278
279    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
280        VirtualFileSystem::chown(&mut self.inner, path, uid, gid)
281    }
282
283    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
284        VirtualFileSystem::utimes(&mut self.inner, path, atime_ms, mtime_ms)
285    }
286
287    fn utimes_spec(
288        &mut self,
289        path: &str,
290        atime: VirtualUtimeSpec,
291        mtime: VirtualUtimeSpec,
292        follow_symlinks: bool,
293    ) -> VfsResult<()> {
294        VirtualFileSystem::utimes_spec(&mut self.inner, path, atime, mtime, follow_symlinks)
295    }
296
297    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
298        VirtualFileSystem::truncate(&mut self.inner, path, length)
299    }
300
301    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
302        VirtualFileSystem::pread(&mut self.inner, path, offset, length)
303    }
304}
305
306impl<T> MountedFileSystem for Box<T>
307where
308    T: MountedFileSystem + ?Sized + 'static,
309{
310    fn as_any(&self) -> &dyn Any {
311        (**self).as_any()
312    }
313
314    fn as_any_mut(&mut self) -> &mut dyn Any {
315        (**self).as_any_mut()
316    }
317
318    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
319        (**self).read_file(path)
320    }
321
322    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
323        (**self).read_dir(path)
324    }
325
326    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
327        (**self).read_dir_limited(path, max_entries)
328    }
329
330    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
331        (**self).read_dir_with_types(path)
332    }
333
334    fn write_file(&mut self, path: &str, content: Vec<u8>) -> VfsResult<()> {
335        (**self).write_file(path, content)
336    }
337
338    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
339        (**self).create_dir(path)
340    }
341
342    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
343        (**self).mkdir(path, recursive)
344    }
345
346    fn exists(&self, path: &str) -> bool {
347        (**self).exists(path)
348    }
349
350    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
351        (**self).stat(path)
352    }
353
354    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
355        (**self).remove_file(path)
356    }
357
358    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
359        (**self).remove_dir(path)
360    }
361
362    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
363        (**self).rename(old_path, new_path)
364    }
365
366    fn realpath(&self, path: &str) -> VfsResult<String> {
367        (**self).realpath(path)
368    }
369
370    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
371        (**self).symlink(target, link_path)
372    }
373
374    fn read_link(&self, path: &str) -> VfsResult<String> {
375        (**self).read_link(path)
376    }
377
378    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
379        (**self).lstat(path)
380    }
381
382    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
383        (**self).link(old_path, new_path)
384    }
385
386    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
387        (**self).chmod(path, mode)
388    }
389
390    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
391        (**self).chown(path, uid, gid)
392    }
393
394    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
395        (**self).utimes(path, atime_ms, mtime_ms)
396    }
397
398    fn utimes_spec(
399        &mut self,
400        path: &str,
401        atime: VirtualUtimeSpec,
402        mtime: VirtualUtimeSpec,
403        follow_symlinks: bool,
404    ) -> VfsResult<()> {
405        (**self).utimes_spec(path, atime, mtime, follow_symlinks)
406    }
407
408    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
409        (**self).truncate(path, length)
410    }
411
412    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
413        (**self).pread(path, offset, length)
414    }
415
416    fn shutdown(&mut self) -> VfsResult<()> {
417        (**self).shutdown()
418    }
419}
420
421pub struct ReadOnlyFileSystem<F> {
422    inner: F,
423}
424
425impl<F> ReadOnlyFileSystem<F> {
426    pub fn new(inner: F) -> Self {
427        Self { inner }
428    }
429}
430
431impl<F> MountedFileSystem for ReadOnlyFileSystem<F>
432where
433    F: MountedFileSystem + 'static,
434{
435    fn as_any(&self) -> &dyn Any {
436        self
437    }
438
439    fn as_any_mut(&mut self) -> &mut dyn Any {
440        self
441    }
442
443    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
444        self.inner.read_file(path)
445    }
446
447    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
448        self.inner.read_dir(path)
449    }
450
451    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
452        self.inner.read_dir_limited(path, max_entries)
453    }
454
455    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
456        self.inner.read_dir_with_types(path)
457    }
458
459    fn write_file(&mut self, path: &str, _content: Vec<u8>) -> VfsResult<()> {
460        Err(VfsError::new(
461            "EROFS",
462            format!("read-only filesystem: {path}"),
463        ))
464    }
465
466    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
467        Err(VfsError::new(
468            "EROFS",
469            format!("read-only filesystem: {path}"),
470        ))
471    }
472
473    fn mkdir(&mut self, path: &str, _recursive: bool) -> VfsResult<()> {
474        Err(VfsError::new(
475            "EROFS",
476            format!("read-only filesystem: {path}"),
477        ))
478    }
479
480    fn exists(&self, path: &str) -> bool {
481        self.inner.exists(path)
482    }
483
484    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
485        self.inner.stat(path)
486    }
487
488    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
489        Err(VfsError::new(
490            "EROFS",
491            format!("read-only filesystem: {path}"),
492        ))
493    }
494
495    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
496        Err(VfsError::new(
497            "EROFS",
498            format!("read-only filesystem: {path}"),
499        ))
500    }
501
502    fn rename(&mut self, old_path: &str, _new_path: &str) -> VfsResult<()> {
503        Err(VfsError::new(
504            "EROFS",
505            format!("read-only filesystem: {old_path}"),
506        ))
507    }
508
509    fn realpath(&self, path: &str) -> VfsResult<String> {
510        self.inner.realpath(path)
511    }
512
513    fn symlink(&mut self, _target: &str, link_path: &str) -> VfsResult<()> {
514        Err(VfsError::new(
515            "EROFS",
516            format!("read-only filesystem: {link_path}"),
517        ))
518    }
519
520    fn read_link(&self, path: &str) -> VfsResult<String> {
521        self.inner.read_link(path)
522    }
523
524    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
525        self.inner.lstat(path)
526    }
527
528    fn link(&mut self, _old_path: &str, new_path: &str) -> VfsResult<()> {
529        Err(VfsError::new(
530            "EROFS",
531            format!("read-only filesystem: {new_path}"),
532        ))
533    }
534
535    fn chmod(&mut self, path: &str, _mode: u32) -> VfsResult<()> {
536        Err(VfsError::new(
537            "EROFS",
538            format!("read-only filesystem: {path}"),
539        ))
540    }
541
542    fn chown(&mut self, path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
543        Err(VfsError::new(
544            "EROFS",
545            format!("read-only filesystem: {path}"),
546        ))
547    }
548
549    fn utimes(&mut self, path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
550        Err(VfsError::new(
551            "EROFS",
552            format!("read-only filesystem: {path}"),
553        ))
554    }
555
556    fn utimes_spec(
557        &mut self,
558        path: &str,
559        _atime: VirtualUtimeSpec,
560        _mtime: VirtualUtimeSpec,
561        _follow_symlinks: bool,
562    ) -> VfsResult<()> {
563        Err(VfsError::new(
564            "EROFS",
565            format!("read-only filesystem: {path}"),
566        ))
567    }
568
569    fn truncate(&mut self, path: &str, _length: u64) -> VfsResult<()> {
570        Err(VfsError::new(
571            "EROFS",
572            format!("read-only filesystem: {path}"),
573        ))
574    }
575
576    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
577        self.inner.pread(path, offset, length)
578    }
579
580    fn shutdown(&mut self) -> VfsResult<()> {
581        self.inner.shutdown()
582    }
583}
584
585#[derive(Debug, Clone, PartialEq, Eq)]
586pub struct MountEntry {
587    pub path: String,
588    pub plugin_id: String,
589    pub read_only: bool,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq)]
593pub struct MountOptions {
594    pub plugin_id: String,
595    pub read_only: bool,
596}
597
598impl MountOptions {
599    pub fn new(plugin_id: impl Into<String>) -> Self {
600        Self {
601            plugin_id: plugin_id.into(),
602            read_only: false,
603        }
604    }
605
606    pub fn read_only(mut self, read_only: bool) -> Self {
607        self.read_only = read_only;
608        self
609    }
610}
611
612struct MountRegistration {
613    path: String,
614    plugin_id: String,
615    read_only: bool,
616    filesystem: Box<dyn MountedFileSystem>,
617}
618
619pub struct MountTable {
620    mounts: Vec<MountRegistration>,
621}
622
623impl MountTable {
624    pub fn new(root_fs: impl VirtualFileSystem + 'static) -> Self {
625        Self {
626            mounts: vec![MountRegistration {
627                path: String::from("/"),
628                plugin_id: String::from("root"),
629                read_only: false,
630                filesystem: Box::new(MountedVirtualFileSystem::new(root_fs)),
631            }],
632        }
633    }
634
635    pub fn new_boxed_root(filesystem: Box<dyn MountedFileSystem>, options: MountOptions) -> Self {
636        let filesystem = if options.read_only {
637            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
638        } else {
639            filesystem
640        };
641
642        Self {
643            mounts: vec![MountRegistration {
644                path: String::from("/"),
645                plugin_id: options.plugin_id,
646                read_only: options.read_only,
647                filesystem,
648            }],
649        }
650    }
651
652    pub fn mount(
653        &mut self,
654        path: &str,
655        filesystem: impl VirtualFileSystem + 'static,
656        options: MountOptions,
657    ) -> VfsResult<()> {
658        self.mount_boxed(
659            path,
660            Box::new(MountedVirtualFileSystem::new(filesystem)),
661            options,
662        )
663    }
664
665    pub fn mount_boxed(
666        &mut self,
667        path: &str,
668        mut filesystem: Box<dyn MountedFileSystem>,
669        options: MountOptions,
670    ) -> VfsResult<()> {
671        let normalized = normalize_path(path);
672        if normalized == "/" {
673            return Err(VfsError::new("EINVAL", "cannot mount over root"));
674        }
675        if self.mounts.iter().any(|mount| mount.path == normalized) {
676            return Err(VfsError::new(
677                "EEXIST",
678                format!("already mounted at {normalized}"),
679            ));
680        }
681
682        let (parent_index, relative_path) = self.resolve_index(&normalized)?;
683        let parent_mount = &mut self.mounts[parent_index];
684        if !parent_mount.filesystem.exists(&relative_path) {
685            // Materializing the mountpoint directory on the parent is
686            // cosmetic: child mounts resolve by path prefix before the parent
687            // is consulted. A read-only parent (for example a read-only
688            // module-access mount hosting nested package mounts) cannot
689            // materialize the entry, but the mount must still succeed.
690            if let Err(error) = parent_mount.filesystem.mkdir(&relative_path, true) {
691                if error.code() != "EROFS" {
692                    if let Err(shutdown_error) = filesystem.shutdown() {
693                        return Err(VfsError::new(
694                            shutdown_error.code(),
695                            format!(
696                                "failed to shut down filesystem after mount failure ({error}): {}",
697                                shutdown_error.message()
698                            ),
699                        ));
700                    }
701
702                    return Err(error);
703                }
704            }
705        }
706
707        let filesystem = if options.read_only {
708            Box::new(ReadOnlyFileSystem::new(filesystem)) as Box<dyn MountedFileSystem>
709        } else {
710            filesystem
711        };
712
713        self.mounts.push(MountRegistration {
714            path: normalized,
715            plugin_id: options.plugin_id,
716            read_only: options.read_only,
717            filesystem,
718        });
719        self.mounts
720            .sort_by_key(|mount| std::cmp::Reverse(mount.path.len()));
721        Ok(())
722    }
723
724    pub fn unmount(&mut self, path: &str) -> VfsResult<()> {
725        let normalized = normalize_path(path);
726        if normalized == "/" {
727            return Err(VfsError::new("EINVAL", "cannot unmount root"));
728        }
729
730        let child_mount_prefix = format!("{normalized}/");
731        if self
732            .mounts
733            .iter()
734            .any(|mount| mount.path.starts_with(&child_mount_prefix))
735        {
736            return Err(VfsError::new(
737                "EBUSY",
738                format!("mount point has child mounts: {normalized}"),
739            ));
740        }
741
742        let Some(index) = self
743            .mounts
744            .iter()
745            .position(|mount| mount.path == normalized)
746        else {
747            return Err(VfsError::new(
748                "EINVAL",
749                format!("not a mount point: {normalized}"),
750            ));
751        };
752
753        let mut mount = self.mounts.remove(index);
754        mount.filesystem.shutdown()?;
755        Ok(())
756    }
757
758    pub fn get_mounts(&self) -> Vec<MountEntry> {
759        self.mounts
760            .iter()
761            .map(|mount| MountEntry {
762                path: mount.path.clone(),
763                plugin_id: mount.plugin_id.clone(),
764                read_only: mount.read_only,
765            })
766            .collect()
767    }
768
769    pub fn root_virtual_filesystem_mut<T: VirtualFileSystem + 'static>(
770        &mut self,
771    ) -> Option<&mut T> {
772        let root = self.mounts.iter_mut().find(|mount| mount.path == "/")?;
773        root.filesystem
774            .as_any_mut()
775            .downcast_mut::<MountedVirtualFileSystem<T>>()
776            .map(MountedVirtualFileSystem::inner_mut)
777    }
778
779    pub fn check_rename_copy_up_limits(
780        &mut self,
781        old_path: &str,
782        new_path: &str,
783        max_bytes: Option<u64>,
784        max_inodes: Option<usize>,
785    ) -> VfsResult<()> {
786        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
787        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
788        if old_index != new_index {
789            return Ok(());
790        }
791
792        let filesystem = &mut self.mounts[old_index].filesystem;
793        if let Some(root) = filesystem
794            .as_any_mut()
795            .downcast_mut::<MountedVirtualFileSystem<RootFileSystem>>()
796        {
797            root.inner_mut().check_rename_copy_up_limits(
798                &old_relative_path,
799                &new_relative_path,
800                max_bytes,
801                max_inodes,
802            )?;
803        }
804
805        Ok(())
806    }
807
808    pub fn root_usage(&mut self) -> VfsResult<FileSystemUsage> {
809        let root = self
810            .mounts
811            .iter_mut()
812            .find(|mount| mount.path == "/")
813            .ok_or_else(|| VfsError::new("ENOENT", "missing root mount"))?;
814        measure_mounted_filesystem_usage(root.filesystem.as_mut(), "/", &mut BTreeSet::new())
815    }
816
817    fn resolve_index(&self, full_path: &str) -> VfsResult<(usize, String)> {
818        let normalized = normalize_path(full_path);
819        for (index, mount) in self.mounts.iter().enumerate() {
820            if mount.path == "/" {
821                return Ok((index, normalized));
822            }
823            if normalized == mount.path {
824                return Ok((index, String::from("/")));
825            }
826            let mount_prefix = format!("{}/", mount.path);
827            if let Some(suffix) = normalized.strip_prefix(&mount_prefix) {
828                // Strip exactly the mount prefix once. `trim_start_matches` would
829                // strip every leading repetition of `mount.path`, so a path like
830                // `/data/data/file` under mount `/data` would alias to `/file`
831                // instead of `/data/file`, routing reads/writes to the wrong file.
832                return Ok((index, format!("/{suffix}")));
833            }
834        }
835
836        Err(VfsError::new(
837            "ENOENT",
838            format!("no such file or directory, resolve '{full_path}'"),
839        ))
840    }
841
842    /// Resolve a path for a CONTENT operation (read_file/stat/pread/read_dir)
843    /// that must follow symlinks like POSIX `open()`. `resolve_index` is purely
844    /// lexical, so a path that descends through a symlink whose target lives in a
845    /// *different* mount (e.g. `/opt/agentos/pkgs/<pkg>/current -> <version>`,
846    /// where `current` is its own single-symlink leaf mount) would route into the
847    /// symlink mount and fail. `realpath` follows those cross-mount symlinks, so
848    /// resolve it first, then route the resolved path. Falls back to the raw path
849    /// when realpath can't resolve it (e.g. a genuinely missing file) so callers
850    /// still receive the mount's own ENOENT.
851    /// True only for the tar-vfs single-symlink-root leaf mount (`<pkg>/current ->
852    /// <version>` and the `bin/<cmd>` links), whose root inode IS a symlink. Only
853    /// these mounts need cross-mount realpath resolution for content ops and the
854    /// ENOENT->component-walk fallback; every other mount serves its own paths and
855    /// MUST keep its native error semantics (e.g. a js_bridge mount's ENOENT/EIO),
856    /// so gating on the concrete leaf type leaves normal mounts untouched.
857    fn mount_is_symlink_leaf(&self, index: usize) -> bool {
858        // Behavioral check (robust through the ReadOnly/MountedVirtual wrappers the
859        // projection applies): a leaf whose root inode is itself a symbolic link.
860        // Only the tar-vfs `<pkg>/current -> <version>` and `bin/<cmd>` single-symlink
861        // mounts satisfy this; every normal mount's root is a directory.
862        self.mounts[index]
863            .filesystem
864            .lstat("/")
865            .map(|stat| stat.is_symbolic_link)
866            .unwrap_or(false)
867    }
868
869    /// Resolve a path for a LINK-LEAF operation (lstat/readlink) that must
870    /// follow INTERMEDIATE symlinks but not the final component. When the
871    /// lexical route lands inside a symlink-root leaf mount at a descendant
872    /// path, the component being operated on is not the mount's own symlink —
873    /// resolve the parent across mounts and re-route, keeping the leaf
874    /// unresolved. `relative == "/"` (the symlink itself) keeps the raw route
875    /// so `lstat(<pkg>/current)` still reports the symlink.
876    fn resolve_link_leaf_index(&self, path: &str) -> VfsResult<(usize, String)> {
877        let normalized = normalize_path(path);
878        let raw = self.resolve_index(&normalized)?;
879        if raw.1 == "/" || !self.mount_is_symlink_leaf(raw.0) {
880            return Ok(raw);
881        }
882        let parent = parent_path(&normalized);
883        let leaf = basename(&normalized);
884        match self.realpath(&parent) {
885            Ok(resolved_parent) => self.resolve_index(&join_path(&resolved_parent, &leaf)),
886            Err(_) => Ok(raw),
887        }
888    }
889
890    fn resolve_content_index(&self, path: &str) -> VfsResult<(usize, String)> {
891        let raw = self.resolve_index(&normalize_path(path))?;
892        if !self.mount_is_symlink_leaf(raw.0) {
893            return Ok(raw);
894        }
895        // The leaf's root is a symlink, so a descendant path must be followed across
896        // the mount boundary to the real version tree before the content op runs.
897        match self.realpath(path) {
898            Ok(resolved) => self.resolve_index(&resolved),
899            Err(_) => Ok(raw),
900        }
901    }
902
903    fn child_mount_basenames(&self, path: &str) -> Vec<String> {
904        let normalized = normalize_path(path);
905        let mut basenames = BTreeSet::new();
906        for mount in &self.mounts {
907            if mount.path == "/" || mount.path == normalized {
908                continue;
909            }
910
911            if parent_path(&mount.path) == normalized {
912                basenames.insert(basename(&mount.path));
913            }
914        }
915        basenames.into_iter().collect()
916    }
917
918    fn realpath_in_mount(&self, index: usize, relative_path: &str) -> VfsResult<String> {
919        let mount = &self.mounts[index];
920        let resolved = mount.filesystem.realpath(relative_path)?;
921        if mount.path == "/" {
922            return Ok(normalize_path(&resolved));
923        }
924        if resolved == "/" {
925            return Ok(mount.path.clone());
926        }
927        Ok(normalize_path(&format!(
928            "{}/{}",
929            mount.path,
930            resolved.trim_start_matches('/')
931        )))
932    }
933}
934
935fn measure_mounted_filesystem_usage(
936    filesystem: &mut dyn MountedFileSystem,
937    path: &str,
938    visited: &mut BTreeSet<u64>,
939) -> VfsResult<FileSystemUsage> {
940    let stat = filesystem.lstat(path)?;
941    let mut usage = FileSystemUsage::default();
942
943    if visited.insert(stat.ino) {
944        usage.inode_count += 1;
945        if !stat.is_directory {
946            usage.total_bytes = usage.total_bytes.saturating_add(stat.size);
947        }
948    }
949
950    if !stat.is_directory || stat.is_symbolic_link {
951        return Ok(usage);
952    }
953
954    for entry in filesystem.read_dir_with_types(path)? {
955        if matches!(entry.name.as_str(), "." | "..") {
956            continue;
957        }
958
959        let child_path = if path == "/" {
960            format!("/{}", entry.name)
961        } else {
962            format!("{path}/{}", entry.name)
963        };
964        let child_usage = measure_mounted_filesystem_usage(filesystem, &child_path, visited)?;
965        usage.total_bytes = usage.total_bytes.saturating_add(child_usage.total_bytes);
966        usage.inode_count = usage.inode_count.saturating_add(child_usage.inode_count);
967    }
968
969    Ok(usage)
970}
971
972impl Drop for MountTable {
973    fn drop(&mut self) {
974        for mount in self.mounts.iter_mut().rev() {
975            let _ = mount.filesystem.shutdown();
976        }
977    }
978}
979
980impl VirtualFileSystem for MountTable {
981    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
982        let (index, relative_path) = self.resolve_content_index(path)?;
983        self.mounts[index].filesystem.read_file(&relative_path)
984    }
985
986    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
987        let normalized = normalize_path(path);
988        // Directory listings are content ops: a path that descends through a
989        // symlink-root leaf mount (`<pkg>/current -> <version>`) must be
990        // followed into the target mount, exactly like read_file/stat. Child
991        // mounts still merge on the LEXICAL path — mount points attach to the
992        // caller-visible path, not the resolved target.
993        let (index, relative_path) = self.resolve_content_index(&normalized)?;
994        let mut entries = self.mounts[index].filesystem.read_dir(&relative_path)?;
995        let child_mounts = self.child_mount_basenames(&normalized);
996        if child_mounts.is_empty() {
997            return Ok(entries);
998        }
999
1000        let mut merged = BTreeSet::new();
1001        merged.extend(entries.drain(..));
1002        merged.extend(child_mounts);
1003        Ok(merged.into_iter().collect())
1004    }
1005
1006    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
1007        let normalized = normalize_path(path);
1008        let (index, relative_path) = self.resolve_content_index(&normalized)?;
1009        let mut entries = self.mounts[index]
1010            .filesystem
1011            .read_dir_limited(&relative_path, max_entries)?;
1012        let child_mounts = self.child_mount_basenames(&normalized);
1013        if child_mounts.is_empty() {
1014            return Ok(entries);
1015        }
1016
1017        let mut merged = BTreeSet::new();
1018        merged.extend(entries.drain(..));
1019        merged.extend(child_mounts);
1020        if merged.len() > max_entries {
1021            return Err(VfsError::new(
1022                "ENOMEM",
1023                format!(
1024                    "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
1025                ),
1026            ));
1027        }
1028        Ok(merged.into_iter().collect())
1029    }
1030
1031    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1032        let normalized = normalize_path(path);
1033        let (index, relative_path) = self.resolve_content_index(&normalized)?;
1034        let mut entries = self.mounts[index]
1035            .filesystem
1036            .read_dir_with_types(&relative_path)?;
1037        let child_mounts = self.child_mount_basenames(&normalized);
1038        if child_mounts.is_empty() {
1039            return Ok(entries);
1040        }
1041
1042        let existing = entries
1043            .iter()
1044            .map(|entry| entry.name.clone())
1045            .collect::<BTreeSet<_>>();
1046        for mount_name in child_mounts {
1047            if existing.contains(&mount_name) {
1048                continue;
1049            }
1050            entries.push(VirtualDirEntry {
1051                name: mount_name,
1052                is_directory: true,
1053                is_symbolic_link: false,
1054            });
1055        }
1056        Ok(entries)
1057    }
1058
1059    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1060        let (index, relative_path) = self.resolve_index(path)?;
1061        self.mounts[index]
1062            .filesystem
1063            .write_file(&relative_path, content.into())
1064    }
1065
1066    fn write_file_with_mode(
1067        &mut self,
1068        path: &str,
1069        content: impl Into<Vec<u8>>,
1070        mode: Option<u32>,
1071    ) -> VfsResult<()> {
1072        let (index, relative_path) = self.resolve_index(path)?;
1073        self.mounts[index]
1074            .filesystem
1075            .write_file_with_mode(&relative_path, content.into(), mode)
1076    }
1077
1078    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1079        let (index, relative_path) = self.resolve_index(path)?;
1080        self.mounts[index]
1081            .filesystem
1082            .create_file_exclusive(&relative_path, content.into())
1083    }
1084
1085    fn create_file_exclusive_with_mode(
1086        &mut self,
1087        path: &str,
1088        content: impl Into<Vec<u8>>,
1089        mode: Option<u32>,
1090    ) -> VfsResult<()> {
1091        let (index, relative_path) = self.resolve_index(path)?;
1092        self.mounts[index]
1093            .filesystem
1094            .create_file_exclusive_with_mode(&relative_path, content.into(), mode)
1095    }
1096
1097    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1098        let (index, relative_path) = self.resolve_index(path)?;
1099        self.mounts[index]
1100            .filesystem
1101            .append_file(&relative_path, content.into())
1102    }
1103
1104    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1105        let (index, relative_path) = self.resolve_index(path)?;
1106        self.mounts[index].filesystem.create_dir(&relative_path)
1107    }
1108
1109    fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
1110        let (index, relative_path) = self.resolve_index(path)?;
1111        self.mounts[index]
1112            .filesystem
1113            .create_dir_with_mode(&relative_path, mode)
1114    }
1115
1116    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1117        let (index, relative_path) = self.resolve_index(path)?;
1118        self.mounts[index]
1119            .filesystem
1120            .mkdir(&relative_path, recursive)
1121    }
1122
1123    fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
1124        let (index, relative_path) = self.resolve_index(path)?;
1125        self.mounts[index]
1126            .filesystem
1127            .mkdir_with_mode(&relative_path, recursive, mode)
1128    }
1129
1130    fn exists(&self, path: &str) -> bool {
1131        // `exists` follows symlinks like POSIX access(); route through the
1132        // content resolver so paths under a symlink-root leaf mount resolve.
1133        self.resolve_content_index(path)
1134            .map(|(index, relative_path)| self.mounts[index].filesystem.exists(&relative_path))
1135            .unwrap_or(false)
1136    }
1137
1138    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1139        let (index, relative_path) = self.resolve_content_index(path)?;
1140        self.mounts[index].filesystem.stat(&relative_path)
1141    }
1142
1143    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1144        let (index, relative_path) = self.resolve_index(path)?;
1145        self.mounts[index].filesystem.remove_file(&relative_path)
1146    }
1147
1148    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1149        let (index, relative_path) = self.resolve_index(path)?;
1150        self.mounts[index].filesystem.remove_dir(&relative_path)
1151    }
1152
1153    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1154        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
1155        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
1156        if old_index != new_index {
1157            return Err(VfsError::new(
1158                "EXDEV",
1159                format!("rename across mounts: {old_path} -> {new_path}"),
1160            ));
1161        }
1162
1163        self.mounts[old_index]
1164            .filesystem
1165            .rename(&old_relative_path, &new_relative_path)
1166    }
1167
1168    fn realpath(&self, path: &str) -> VfsResult<String> {
1169        let normalized = normalize_path(path);
1170        let (index, relative_path) = self.resolve_index(&normalized)?;
1171        match self.realpath_in_mount(index, &relative_path) {
1172            Ok(resolved) => return Ok(resolved),
1173            // Always fall back to the component walk on ELOOP. Fall back on ENOENT
1174            // ONLY for a single-symlink LEAF mount (e.g. `<pkg>/current -> <version>`):
1175            // it cannot resolve a descendant path itself — its root IS the symlink, so
1176            // a subpath resolves into it and ENOENTs; the walk then follows that
1177            // symlink across mounts. For every OTHER mount an ENOENT is a genuine
1178            // "missing file" that must propagate unchanged (walking it would rewrite a
1179            // mount's native ENOENT into a spurious success/EIO — see the js_bridge
1180            // errno-mapping mount). Non-leaf within-mount paths resolve via
1181            // `realpath_in_mount` above and return early, so pnpm resolution is
1182            // unaffected either way.
1183            Err(error) if error.code() == "ELOOP" => {}
1184            Err(error) if error.code() == "ENOENT" && self.mount_is_symlink_leaf(index) => {}
1185            Err(error) => return Err(error),
1186        }
1187
1188        let mut pending = path_components(&normalized);
1189        let mut current = String::from("/");
1190        let mut followed_symlinks = 0usize;
1191
1192        while let Some(component) = pending.pop_front() {
1193            let candidate = join_path(&current, &component);
1194            let stat = self.lstat(&candidate)?;
1195
1196            if stat.is_symbolic_link {
1197                followed_symlinks += 1;
1198                if followed_symlinks > MAX_REALPATH_SYMLINKS {
1199                    return Err(VfsError::new(
1200                        "ELOOP",
1201                        format!("too many levels of symbolic links, '{path}'"),
1202                    ));
1203                }
1204
1205                let target = self.read_link(&candidate)?;
1206                let target_path = if target.starts_with('/') {
1207                    normalize_path(&target)
1208                } else {
1209                    normalize_path(&format!("{}/{}", parent_path(&candidate), target))
1210                };
1211                let mut resolved_target = path_components(&target_path);
1212                resolved_target.extend(pending);
1213                pending = resolved_target;
1214                current = String::from("/");
1215                continue;
1216            }
1217
1218            if !pending.is_empty() && !stat.is_directory {
1219                return Err(VfsError::new(
1220                    "ENOTDIR",
1221                    format!("not a directory, realpath '{candidate}'"),
1222                ));
1223            }
1224
1225            current = candidate;
1226        }
1227
1228        Ok(current)
1229    }
1230
1231    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1232        let normalized_link_path = normalize_path(link_path);
1233        let link_parent = parent_path(&normalized_link_path);
1234        let absolute_target = if target.starts_with('/') {
1235            normalize_path(target)
1236        } else {
1237            normalize_path(&format!("{link_parent}/{target}"))
1238        };
1239
1240        let (index, relative_path) = self.resolve_index(&normalized_link_path)?;
1241        let (target_index, _) = self.resolve_index(&absolute_target)?;
1242        if index != target_index {
1243            return Err(VfsError::new(
1244                "EXDEV",
1245                format!("symlink across mounts: {link_path} -> {target}"),
1246            ));
1247        }
1248
1249        self.mounts[index]
1250            .filesystem
1251            .symlink(target, &relative_path)
1252    }
1253
1254    fn read_link(&self, path: &str) -> VfsResult<String> {
1255        let (index, relative_path) = self.resolve_link_leaf_index(path)?;
1256        self.mounts[index].filesystem.read_link(&relative_path)
1257    }
1258
1259    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1260        let (index, relative_path) = self.resolve_link_leaf_index(path)?;
1261        self.mounts[index].filesystem.lstat(&relative_path)
1262    }
1263
1264    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1265        let (old_index, old_relative_path) = self.resolve_index(old_path)?;
1266        let (new_index, new_relative_path) = self.resolve_index(new_path)?;
1267        if old_index != new_index {
1268            return Err(VfsError::new(
1269                "EXDEV",
1270                format!("link across mounts: {old_path} -> {new_path}"),
1271            ));
1272        }
1273
1274        self.mounts[old_index]
1275            .filesystem
1276            .link(&old_relative_path, &new_relative_path)
1277    }
1278
1279    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1280        let (index, relative_path) = self.resolve_index(path)?;
1281        self.mounts[index].filesystem.chmod(&relative_path, mode)
1282    }
1283
1284    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1285        let (index, relative_path) = self.resolve_index(path)?;
1286        self.mounts[index]
1287            .filesystem
1288            .chown(&relative_path, uid, gid)
1289    }
1290
1291    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1292        let (index, relative_path) = self.resolve_index(path)?;
1293        self.mounts[index]
1294            .filesystem
1295            .utimes(&relative_path, atime_ms, mtime_ms)
1296    }
1297
1298    fn utimes_spec(
1299        &mut self,
1300        path: &str,
1301        atime: VirtualUtimeSpec,
1302        mtime: VirtualUtimeSpec,
1303        follow_symlinks: bool,
1304    ) -> VfsResult<()> {
1305        let (index, relative_path) = self.resolve_index(path)?;
1306        self.mounts[index]
1307            .filesystem
1308            .utimes_spec(&relative_path, atime, mtime, follow_symlinks)
1309    }
1310
1311    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1312        let (index, relative_path) = self.resolve_index(path)?;
1313        self.mounts[index]
1314            .filesystem
1315            .truncate(&relative_path, length)
1316    }
1317
1318    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1319        let (index, relative_path) = self.resolve_content_index(path)?;
1320        self.mounts[index]
1321            .filesystem
1322            .pread(&relative_path, offset, length)
1323    }
1324}
1325
1326fn normalize_path(path: &str) -> String {
1327    let mut segments = Vec::new();
1328    for component in Path::new(path).components() {
1329        match component {
1330            Component::RootDir => segments.clear(),
1331            Component::ParentDir => {
1332                segments.pop();
1333            }
1334            Component::CurDir => {}
1335            Component::Normal(value) => segments.push(value.to_string_lossy().into_owned()),
1336            Component::Prefix(prefix) => {
1337                segments.push(prefix.as_os_str().to_string_lossy().into_owned());
1338            }
1339        }
1340    }
1341
1342    if segments.is_empty() {
1343        String::from("/")
1344    } else {
1345        format!("/{}", segments.join("/"))
1346    }
1347}
1348
1349fn path_components(path: &str) -> VecDeque<String> {
1350    normalize_path(path)
1351        .split('/')
1352        .filter(|part| !part.is_empty())
1353        .map(String::from)
1354        .collect()
1355}
1356
1357fn join_path(parent: &str, child: &str) -> String {
1358    if parent == "/" {
1359        format!("/{child}")
1360    } else {
1361        format!("{parent}/{child}")
1362    }
1363}
1364
1365fn parent_path(path: &str) -> String {
1366    let normalized = normalize_path(path);
1367    let parent = Path::new(&normalized)
1368        .parent()
1369        .unwrap_or_else(|| Path::new("/"));
1370    let value = parent.to_string_lossy();
1371    if value.is_empty() {
1372        String::from("/")
1373    } else {
1374        value.into_owned()
1375    }
1376}
1377
1378fn basename(path: &str) -> String {
1379    let normalized = normalize_path(path);
1380    Path::new(&normalized)
1381        .file_name()
1382        .map(|name| name.to_string_lossy().into_owned())
1383        .unwrap_or_else(|| String::from("/"))
1384}