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 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 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 fn mount_is_symlink_leaf(&self, index: usize) -> bool {
858 self.mounts[index]
863 .filesystem
864 .lstat("/")
865 .map(|stat| stat.is_symbolic_link)
866 .unwrap_or(false)
867 }
868
869 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 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 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 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 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(¤t, &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}