1use std::{
2 collections::HashMap,
3 ffi::OsString,
4 path::{Component, Path, PathBuf},
5 sync::Arc,
6};
7
8use anyhow::{Context, Error};
9use tokio::runtime::Handle;
10use virtual_fs::{
11 ArcFileSystem, ExactMountConflictMode, FileSystem, MountFileSystem, OverlayFileSystem,
12 RootFileSystemBuilder, TmpFileSystem, limiter::DynFsMemoryLimiter,
13};
14use webc::metadata::annotations::Wasi as WasiAnnotation;
15
16use crate::{
17 WasiEnvBuilder,
18 bin_factory::{BinaryPackage, BinaryPackageMounts},
19 capabilities::Capabilities,
20 fs::WasiFsRoot,
21 journal::{DynJournal, DynReadableJournal, SnapshotTrigger},
22};
23
24pub const MAPPED_CURRENT_DIR_DEFAULT_PATH: &str = "/home";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum ExistingMountConflictBehavior {
28 Fail,
29 #[default]
30 Override,
31}
32
33#[derive(Debug, Clone)]
34pub struct MappedCommand {
35 pub alias: String,
37 pub target: String,
39}
40
41#[derive(Debug, Default, Clone)]
42pub(crate) struct CommonWasiOptions {
43 pub(crate) entry_function: Option<String>,
44 pub(crate) args: Vec<String>,
45 pub(crate) env: HashMap<String, String>,
46 pub(crate) forward_host_env: bool,
47 pub(crate) mapped_host_commands: Vec<MappedCommand>,
48 pub(crate) mounts: Vec<MountedDirectory>,
49 pub(crate) is_home_mapped: bool,
50 pub(crate) injected_packages: Vec<BinaryPackage>,
51 pub(crate) capabilities: Capabilities,
52 pub(crate) read_only_journals: Vec<Arc<DynReadableJournal>>,
53 pub(crate) writable_journals: Vec<Arc<DynJournal>>,
54 pub(crate) snapshot_on: Vec<SnapshotTrigger>,
55 pub(crate) snapshot_interval: Option<std::time::Duration>,
56 pub(crate) stop_running_after_snapshot: bool,
57 pub(crate) skip_stdio_during_bootstrap: bool,
58 pub(crate) current_dir: Option<PathBuf>,
59 pub(crate) existing_mount_conflict_behavior: ExistingMountConflictBehavior,
60}
61
62impl CommonWasiOptions {
63 pub(crate) fn prepare_webc_env(
64 &self,
65 builder: &mut WasiEnvBuilder,
66 container_mounts: Option<&BinaryPackageMounts>,
67 wasi: &WasiAnnotation,
68 root_fs: Option<WasiFsRoot>,
69 ) -> Result<(), anyhow::Error> {
70 if let Some(ref entry_function) = self.entry_function {
71 builder.set_entry_function(entry_function);
72 }
73
74 let root_fs = root_fs.unwrap_or_else(|| {
75 let mapped_dirs = self
76 .mounts
77 .iter()
78 .map(|d| d.guest.as_str())
79 .collect::<Vec<_>>();
80 WasiFsRoot::from_filesystem(Arc::new(
81 RootFileSystemBuilder::default().build_tmp_ext(&mapped_dirs),
82 ))
83 });
84 let fs = prepare_filesystem(
85 root_fs
86 .root()
87 .filesystem_at(Path::new("/"))
88 .context("root fs is missing a / mount")?,
89 root_fs.memory_limiter(),
90 &self.mounts,
91 container_mounts,
92 self.existing_mount_conflict_behavior,
93 )?;
94
95 if self.mounts.iter().all(|m| m.guest != ".") {
97 let path = builder.get_current_dir().unwrap_or(PathBuf::from("/"));
99 builder.add_preopen_build(|p| {
100 p.directory(&path)
101 .alias(".")
102 .read(true)
103 .write(true)
104 .create(true)
105 })?;
106 }
107
108 builder.add_preopen_dir("/")?;
109
110 builder.set_fs_root(fs);
111
112 for pkg in &self.injected_packages {
113 builder.add_webc(pkg.clone());
114 }
115
116 let mapped_cmds = self
117 .mapped_host_commands
118 .iter()
119 .map(|c| (c.alias.as_str(), c.target.as_str()));
120 builder.add_mapped_commands(mapped_cmds);
121
122 self.populate_env(wasi, builder);
123 self.populate_args(wasi, builder);
124
125 *builder.capabilities_mut() = self.capabilities.clone();
126
127 #[cfg(feature = "journal")]
128 {
129 for journal in &self.read_only_journals {
130 builder.add_read_only_journal(journal.clone());
131 }
132 for journal in &self.writable_journals {
133 builder.add_writable_journal(journal.clone());
134 }
135 for trigger in &self.snapshot_on {
136 builder.add_snapshot_trigger(*trigger);
137 }
138 if let Some(interval) = self.snapshot_interval {
139 builder.with_snapshot_interval(interval);
140 }
141 builder.with_stop_running_after_snapshot(self.stop_running_after_snapshot);
142 }
143
144 Ok(())
145 }
146
147 fn populate_env(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
148 for item in wasi.env.as_deref().unwrap_or_default() {
149 match item.split_once('=') {
153 Some((k, v)) => {
154 builder.add_env(k, v);
155 }
156 None => {
157 builder.add_env(item, String::new());
158 }
159 }
160 }
161
162 if self.forward_host_env {
163 builder.add_envs(os_env_vars(std::env::vars_os()));
164 }
165
166 builder.add_envs(self.env.clone());
167 }
168
169 fn populate_args(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
170 if let Some(main_args) = &wasi.main_args {
171 builder.add_args(main_args);
172 }
173
174 builder.add_args(&self.args);
175 }
176}
177
178fn os_env_vars(
186 vars: impl IntoIterator<Item = (OsString, OsString)>,
187) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> {
188 vars.into_iter()
189 .map(|(name, value)| (name.into_encoded_bytes(), value.into_encoded_bytes()))
190}
191
192fn normalized_mount_path(guest_path: &str) -> Result<PathBuf, Error> {
193 let mut guest_path = PathBuf::from(guest_path);
194
195 if guest_path.is_relative() {
196 guest_path = apply_relative_path_mounting_hack(&guest_path);
197 }
198
199 let mut normalized = PathBuf::from("/");
200 for component in guest_path.components() {
201 match component {
202 Component::RootDir => normalized = PathBuf::from("/"),
203 Component::CurDir => {}
204 Component::ParentDir => {
205 if normalized.as_os_str() == "/" {
206 anyhow::bail!(
207 "Invalid guest mount path \"{}\": parent traversal escapes the virtual root",
208 guest_path.display()
209 );
210 }
211 normalized.pop();
212 }
213 Component::Normal(part) => normalized.push(part),
214 Component::Prefix(_) => {
215 anyhow::bail!(
216 "Invalid guest mount path \"{}\": platform-specific prefixes are not supported",
217 guest_path.display()
218 );
219 }
220 }
221 }
222
223 Ok(normalized)
224}
225
226fn prepare_filesystem(
227 base_root: Arc<dyn FileSystem + Send + Sync>,
228 memory_limiter: Option<&DynFsMemoryLimiter>,
229 mounted_dirs: &[MountedDirectory],
230 container_mounts: Option<&BinaryPackageMounts>,
231 conflict_behavior: ExistingMountConflictBehavior,
232) -> Result<WasiFsRoot, Error> {
233 let mut root_layers: Vec<Arc<dyn FileSystem + Send + Sync>> = Vec::new();
234 let mount_fs = MountFileSystem::new();
235
236 for MountedDirectory { guest, fs } in mounted_dirs {
237 let guest_path = normalized_mount_path(guest)?;
238 tracing::debug!(guest=%guest_path.display(), "Mounting");
239
240 if guest_path == Path::new("/") {
241 root_layers.push(fs.clone());
242 } else {
243 match conflict_behavior {
244 ExistingMountConflictBehavior::Fail => mount_fs
245 .mount(&guest_path, fs.clone())
246 .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?,
247 ExistingMountConflictBehavior::Override => mount_fs
248 .set_mount(&guest_path, fs.clone())
249 .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?,
250 }
251 }
252 }
253
254 let Some(container) = container_mounts else {
255 let root_mount: Arc<dyn FileSystem + Send + Sync> = if root_layers.is_empty() {
256 base_root
257 } else {
258 Arc::new(OverlayFileSystem::new(
259 ArcFileSystem::new(base_root),
260 root_layers,
261 ))
262 };
263 mount_fs.mount(Path::new("/"), root_mount)?;
264
265 return Ok(
266 WasiFsRoot::from_mount_fs(mount_fs).with_memory_limiter_opt(memory_limiter.cloned())
267 );
268 };
269
270 if let Some(container_root) = &container.root_layer {
271 root_layers.push(writable_package_mount(
272 container_root.clone(),
273 memory_limiter,
274 ));
275 }
276
277 let root_mount: Arc<dyn FileSystem + Send + Sync> = if root_layers.is_empty() {
278 base_root
279 } else {
280 Arc::new(OverlayFileSystem::new(
281 ArcFileSystem::new(base_root),
282 root_layers,
283 ))
284 };
285
286 mount_fs.mount(Path::new("/"), root_mount)?;
287 let import_mode = match conflict_behavior {
288 ExistingMountConflictBehavior::Fail => ExactMountConflictMode::Fail,
289 ExistingMountConflictBehavior::Override => ExactMountConflictMode::KeepExisting,
290 };
291 let mut skipped_subtree: Option<PathBuf> = None;
292 for mount in &container.mounts {
293 if skipped_subtree
294 .as_ref()
295 .is_some_and(|prefix| mount.guest_path.starts_with(prefix))
296 {
297 continue;
298 }
299
300 match import_mode {
301 ExactMountConflictMode::Fail => {
302 mount_fs
303 .mount_with_source(
304 &mount.guest_path,
305 &mount.source_path,
306 writable_package_mount(mount.fs.clone(), memory_limiter),
307 )
308 .with_context(|| {
309 format!(
310 "Unable to merge container mount \"{}\" into the prepared filesystem",
311 mount.guest_path.display()
312 )
313 })?;
314 }
315 ExactMountConflictMode::KeepExisting => {
316 if mount_fs.filesystem_at(&mount.guest_path).is_some() {
317 skipped_subtree = Some(mount.guest_path.clone());
318 continue;
319 }
320
321 mount_fs
322 .mount_with_source(
323 &mount.guest_path,
324 &mount.source_path,
325 writable_package_mount(mount.fs.clone(), memory_limiter),
326 )
327 .with_context(|| {
328 format!(
329 "Unable to merge container mount \"{}\" into the prepared filesystem",
330 mount.guest_path.display()
331 )
332 })?;
333 }
334 ExactMountConflictMode::ReplaceExisting => unreachable!("not used here"),
335 }
336 }
337
338 Ok(WasiFsRoot::from_mount_fs(mount_fs).with_memory_limiter_opt(memory_limiter.cloned()))
339}
340
341fn writable_package_mount(
342 fs: Arc<dyn FileSystem + Send + Sync>,
343 memory_limiter: Option<&DynFsMemoryLimiter>,
344) -> Arc<dyn FileSystem + Send + Sync> {
345 let upper = TmpFileSystem::new();
346 if let Some(memory_limiter) = memory_limiter {
347 upper.set_memory_limiter(memory_limiter.clone());
348 }
349
350 Arc::new(OverlayFileSystem::new(upper, [ArcFileSystem::new(fs)]))
351}
352
353fn apply_relative_path_mounting_hack(original: &Path) -> PathBuf {
365 debug_assert!(original.is_relative());
366
367 let root = Path::new("/");
368 let mapped_path = if original == Path::new(".") {
369 root.to_path_buf()
370 } else {
371 root.join(original)
372 };
373
374 tracing::debug!(
375 original_path=%original.display(),
376 remapped_path=%mapped_path.display(),
377 "Remapping a relative path"
378 );
379
380 mapped_path
381}
382
383#[derive(Debug, Clone)]
384pub struct MountedDirectory {
385 pub guest: String,
386 pub fs: Arc<dyn FileSystem + Send + Sync>,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
398pub struct MappedDirectory {
399 pub host: std::path::PathBuf,
401 pub guest: String,
404}
405
406impl From<MappedDirectory> for MountedDirectory {
407 fn from(value: MappedDirectory) -> Self {
408 cfg_select! {
409 feature = "host-fs" => {
410 let MappedDirectory { host, guest } = value;
411 let fs: Arc<dyn FileSystem + Send + Sync> =
412 Arc::new(virtual_fs::host_fs::FileSystem::new(Handle::current(), host).unwrap());
413
414 MountedDirectory { guest, fs }
415 }
416 _ => {
417 unreachable!("The `host-fs` feature needs to be enabled to map {value:?}")
418 }
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use std::{
426 sync::{
427 Arc,
428 atomic::{AtomicUsize, Ordering},
429 },
430 time::SystemTime,
431 };
432
433 use tempfile::TempDir;
434 use virtual_fs::TmpFileSystem;
435 use virtual_fs::{DirEntry, FileType, FsError, Metadata, limiter::FsMemoryLimiter};
436
437 use super::*;
438
439 #[cfg(unix)]
441 #[test]
442 fn issue_6835_non_utf8_host_env_vars_are_forwarded_as_raw_bytes() {
443 use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
444
445 let vars = [
446 (
447 OsStr::from_bytes(b"VALID").to_os_string(),
448 OsStr::from_bytes(b"ok").to_os_string(),
449 ),
450 (
451 OsStr::from_bytes(b"INVALID").to_os_string(),
452 OsStr::from_bytes(b"V\xffW").to_os_string(),
453 ),
454 ];
455
456 let mut builder = WasiEnvBuilder::new("test");
457 builder.add_envs(os_env_vars(vars));
458
459 assert_eq!(
460 builder.get_env(),
461 [
462 ("VALID".to_string(), b"ok".to_vec()),
463 ("INVALID".to_string(), b"V\xffW".to_vec()),
464 ]
465 );
466 }
467
468 fn base_root(root_fs: &MountFileSystem) -> Arc<dyn FileSystem + Send + Sync> {
469 root_fs.filesystem_at(Path::new("/")).unwrap()
470 }
471
472 fn package_mounts(fs: MountFileSystem) -> BinaryPackageMounts {
473 BinaryPackageMounts::from_mount_fs(fs)
474 }
475
476 const PYTHON: &[u8] =
477 include_bytes!("../../../../wasmer-test-files/examples/python-0.1.0.wasmer");
478
479 #[derive(Debug)]
480 struct CountingLimiter {
481 used: AtomicUsize,
482 limit: usize,
483 }
484
485 impl CountingLimiter {
486 fn new(limit: usize) -> Self {
487 Self {
488 used: AtomicUsize::new(0),
489 limit,
490 }
491 }
492 }
493
494 impl FsMemoryLimiter for CountingLimiter {
495 fn on_grow(&self, grown_bytes: usize) -> Result<(), FsError> {
496 let new_total = self.used.fetch_add(grown_bytes, Ordering::SeqCst) + grown_bytes;
497 if new_total > self.limit {
498 self.used.fetch_sub(grown_bytes, Ordering::SeqCst);
499 return Err(FsError::StorageFull);
500 }
501
502 Ok(())
503 }
504
505 fn on_shrink(&self, shrunk_bytes: usize) {
506 self.used.fetch_sub(shrunk_bytes, Ordering::SeqCst);
507 }
508 }
509
510 #[tokio::test]
512 async fn mix_args_from_the_webc_and_user() {
513 let args = CommonWasiOptions {
514 args: vec!["extra".to_string(), "args".to_string()],
515 ..Default::default()
516 };
517 let mut builder = WasiEnvBuilder::new("program-name");
518 let mut annotations = WasiAnnotation::new("some-atom");
519 annotations.main_args = Some(vec![
520 "hard".to_string(),
521 "coded".to_string(),
522 "args".to_string(),
523 ]);
524
525 args.prepare_webc_env(&mut builder, None, &annotations, None)
526 .unwrap();
527
528 assert_eq!(
529 builder.get_args(),
530 [
531 "program-name",
533 "hard",
535 "coded",
536 "args",
537 "extra",
539 "args",
540 ]
541 );
542 }
543
544 #[tokio::test]
545 async fn mix_env_vars_from_the_webc_and_user() {
546 let args = CommonWasiOptions {
547 env: vec![("EXTRA".to_string(), "envs".to_string())]
548 .into_iter()
549 .collect(),
550 ..Default::default()
551 };
552 let mut builder = WasiEnvBuilder::new("python");
553 let mut annotations = WasiAnnotation::new("python");
554 annotations.env = Some(vec!["HARD_CODED=env-vars".to_string()]);
555
556 args.prepare_webc_env(&mut builder, None, &annotations, None)
557 .unwrap();
558
559 assert_eq!(
560 builder.get_env(),
561 [
562 ("HARD_CODED".to_string(), b"env-vars".to_vec()),
563 ("EXTRA".to_string(), b"envs".to_vec()),
564 ]
565 );
566 }
567
568 fn unix_timestamp_nanos(instant: SystemTime) -> Option<u64> {
569 let duration = instant.duration_since(SystemTime::UNIX_EPOCH).ok()?;
570 Some(duration.as_nanos() as u64)
571 }
572
573 #[tokio::test]
574 #[cfg_attr(not(feature = "host-fs"), ignore)]
575 async fn python_use_case() {
576 let temp = TempDir::new().unwrap();
577 let sub_dir = temp.path().join("path").join("to");
578 std::fs::create_dir_all(&sub_dir).unwrap();
579 std::fs::write(sub_dir.join("file.txt"), b"Hello, World!").unwrap();
580 let mapping = [MountedDirectory::from(MappedDirectory {
581 guest: "/home".to_string(),
582 host: sub_dir,
583 })];
584 let container = wasmer_package::utils::from_bytes(PYTHON).unwrap();
585 let webc_fs = virtual_fs::WebcVolumeFileSystem::mount_all(&container);
586 let mount_fs = MountFileSystem::new();
587 mount_fs.mount(Path::new("/"), Arc::new(webc_fs)).unwrap();
588
589 let root_fs = RootFileSystemBuilder::default().build();
590 let fs = prepare_filesystem(
591 base_root(&root_fs),
592 None,
593 &mapping,
594 Some(&package_mounts(mount_fs)),
595 ExistingMountConflictBehavior::Override,
596 )
597 .unwrap();
598
599 use virtual_fs::FileSystem;
600 assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file());
601 assert!(fs.metadata("lib".as_ref()).unwrap().is_dir());
602 assert!(
603 fs.metadata("lib/python3.6/collections/__init__.py".as_ref())
604 .unwrap()
605 .is_file()
606 );
607 assert!(
608 fs.metadata("lib/python3.6/encodings/__init__.py".as_ref())
609 .unwrap()
610 .is_file()
611 );
612 }
613
614 #[tokio::test]
615 async fn package_mount_paths_remain_writable() {
616 use virtual_fs::FileSystem;
617
618 let container = wasmer_package::utils::from_bytes(PYTHON).unwrap();
619 let pkg_mount = virtual_fs::WebcVolumeFileSystem::mount_all(&container);
620
621 let mount_fs = MountFileSystem::new();
622 mount_fs
623 .mount(Path::new("/python"), Arc::new(pkg_mount))
624 .unwrap();
625
626 let root_fs = RootFileSystemBuilder::default().build();
627 let fs = prepare_filesystem(
628 base_root(&root_fs),
629 None,
630 &[],
631 Some(&package_mounts(mount_fs)),
632 ExistingMountConflictBehavior::Override,
633 )
634 .unwrap();
635
636 fs.create_dir(Path::new("/python/custom")).unwrap();
637 fs.new_open_options()
638 .create(true)
639 .write(true)
640 .open(Path::new("/python/custom/sitecustomize.py"))
641 .unwrap();
642
643 assert!(
644 fs.metadata(Path::new("/python/custom/sitecustomize.py"))
645 .unwrap()
646 .is_file()
647 );
648 assert!(
649 fs.metadata(Path::new("/python/lib/python3.6/collections/__init__.py"))
650 .unwrap()
651 .is_file()
652 );
653 }
654
655 #[tokio::test]
656 async fn package_mount_symlinks_remain_writable() {
657 use virtual_fs::FileSystem;
658
659 let container = wasmer_package::utils::from_bytes(PYTHON).unwrap();
660 let pkg_mount = virtual_fs::WebcVolumeFileSystem::mount_all(&container);
661
662 let mount_fs = MountFileSystem::new();
663 mount_fs
664 .mount(Path::new("/python"), Arc::new(pkg_mount))
665 .unwrap();
666
667 let root_fs = RootFileSystemBuilder::default().build();
668 let fs = prepare_filesystem(
669 base_root(&root_fs),
670 None,
671 &[],
672 Some(&package_mounts(mount_fs)),
673 ExistingMountConflictBehavior::Override,
674 )
675 .unwrap();
676
677 fs.create_symlink(
678 Path::new("lib/python3.6/collections"),
679 Path::new("/python/collections-link"),
680 )
681 .unwrap();
682
683 assert_eq!(
684 fs.readlink(Path::new("/python/collections-link")).unwrap(),
685 Path::new("lib/python3.6/collections")
686 );
687 assert!(
688 fs.symlink_metadata(Path::new("/python/collections-link"))
689 .unwrap()
690 .ft
691 .is_symlink()
692 );
693 }
694
695 #[tokio::test]
696 async fn user_mounts_override_package_mounts_when_configured() {
697 use virtual_fs::FileSystem;
698
699 let user_mount = TmpFileSystem::new();
700 user_mount
701 .new_open_options()
702 .create(true)
703 .write(true)
704 .open(Path::new("/user.txt"))
705 .unwrap();
706
707 let package_mount = TmpFileSystem::new();
708 package_mount
709 .new_open_options()
710 .create(true)
711 .write(true)
712 .open(Path::new("/pkg.txt"))
713 .unwrap();
714
715 let mounted_dirs = [MountedDirectory {
716 guest: "/python".to_string(),
717 fs: Arc::new(user_mount),
718 }];
719
720 let container_mounts = MountFileSystem::new();
721 container_mounts
722 .mount(Path::new("/python"), Arc::new(package_mount))
723 .unwrap();
724
725 let root_fs = RootFileSystemBuilder::default().build();
726 let fs = prepare_filesystem(
727 base_root(&root_fs),
728 None,
729 &mounted_dirs,
730 Some(&package_mounts(container_mounts)),
731 ExistingMountConflictBehavior::Override,
732 )
733 .unwrap();
734
735 assert!(
736 fs.metadata(Path::new("/python/user.txt"))
737 .unwrap()
738 .is_file()
739 );
740 assert_eq!(
741 fs.metadata(Path::new("/python/pkg.txt")),
742 Err(virtual_fs::FsError::EntryNotFound)
743 );
744 }
745
746 #[tokio::test]
747 async fn conflicting_mounts_fail_when_configured() {
748 let user_mount = TmpFileSystem::new();
749 let package_mount = TmpFileSystem::new();
750
751 let mounted_dirs = [MountedDirectory {
752 guest: "/python".to_string(),
753 fs: Arc::new(user_mount),
754 }];
755
756 let container_mounts = MountFileSystem::new();
757 container_mounts
758 .mount(Path::new("/python"), Arc::new(package_mount))
759 .unwrap();
760
761 let root_fs = RootFileSystemBuilder::default().build();
762 let error = prepare_filesystem(
763 base_root(&root_fs),
764 None,
765 &mounted_dirs,
766 Some(&package_mounts(container_mounts)),
767 ExistingMountConflictBehavior::Fail,
768 )
769 .unwrap_err();
770
771 assert!(
772 error
773 .to_string()
774 .contains("Unable to merge container mount \"/python\""),
775 "{error:#}"
776 );
777 }
778
779 #[tokio::test]
780 async fn root_mounts_are_composed_even_in_fail_mode() {
781 use virtual_fs::FileSystem;
782
783 let root_mount = TmpFileSystem::new();
784 root_mount
785 .new_open_options()
786 .create(true)
787 .write(true)
788 .open(Path::new("/user.txt"))
789 .unwrap();
790
791 let mounted_dirs = [MountedDirectory {
792 guest: "/".to_string(),
793 fs: Arc::new(root_mount),
794 }];
795
796 let container_mounts = MountFileSystem::new();
797 let container_root = TmpFileSystem::new();
798 container_root
799 .new_open_options()
800 .create(true)
801 .write(true)
802 .open(Path::new("/pkg.txt"))
803 .unwrap();
804 container_mounts
805 .mount(Path::new("/"), Arc::new(container_root))
806 .unwrap();
807
808 let root_fs = RootFileSystemBuilder::default().build();
809 let fs = prepare_filesystem(
810 base_root(&root_fs),
811 None,
812 &mounted_dirs,
813 Some(&package_mounts(container_mounts)),
814 ExistingMountConflictBehavior::Fail,
815 )
816 .unwrap();
817
818 assert!(fs.metadata(Path::new("/user.txt")).unwrap().is_file());
819 assert!(fs.metadata(Path::new("/pkg.txt")).unwrap().is_file());
820 }
821
822 #[tokio::test]
823 async fn multiple_root_mounts_are_composed() {
824 use virtual_fs::FileSystem;
825
826 let first_root = TmpFileSystem::new();
827 first_root
828 .new_open_options()
829 .create(true)
830 .write(true)
831 .open(Path::new("/first.txt"))
832 .unwrap();
833
834 let second_root = TmpFileSystem::new();
835 second_root
836 .new_open_options()
837 .create(true)
838 .write(true)
839 .open(Path::new("/second.txt"))
840 .unwrap();
841
842 let mounted_dirs = [
843 MountedDirectory {
844 guest: "/".to_string(),
845 fs: Arc::new(first_root),
846 },
847 MountedDirectory {
848 guest: "/".to_string(),
849 fs: Arc::new(second_root),
850 },
851 ];
852
853 let root_fs = RootFileSystemBuilder::default().build();
854 let fs = prepare_filesystem(
855 base_root(&root_fs),
856 None,
857 &mounted_dirs,
858 None,
859 ExistingMountConflictBehavior::Fail,
860 )
861 .unwrap();
862
863 assert!(fs.metadata(Path::new("/first.txt")).unwrap().is_file());
864 assert!(fs.metadata(Path::new("/second.txt")).unwrap().is_file());
865 }
866
867 #[tokio::test]
868 async fn prepared_filesystem_preserves_root_memory_limiter() {
869 let limiter: virtual_fs::limiter::DynFsMemoryLimiter = Arc::new(CountingLimiter::new(1));
870
871 let package_mount = TmpFileSystem::new();
872 let container_mounts = MountFileSystem::new();
873 container_mounts
874 .mount(Path::new("/python"), Arc::new(package_mount))
875 .unwrap();
876
877 let root_fs = RootFileSystemBuilder::default().build();
878 let fs = prepare_filesystem(
879 base_root(&root_fs),
880 Some(&limiter),
881 &[],
882 Some(&package_mounts(container_mounts)),
883 ExistingMountConflictBehavior::Override,
884 )
885 .unwrap();
886
887 assert!(fs.memory_limiter().is_some());
888 }
889
890 #[test]
891 fn invalid_guest_mount_paths_are_rejected() {
892 let error = normalized_mount_path("../../python").unwrap_err();
893 assert!(
894 error
895 .to_string()
896 .contains("parent traversal escapes the virtual root"),
897 "{error:#}"
898 );
899 }
900
901 #[tokio::test]
902 #[cfg_attr(not(feature = "host-fs"), ignore)]
903 async fn convert_mapped_directory_to_mounted_directory() {
904 let temp = TempDir::new().unwrap();
905 let dir = MappedDirectory {
906 guest: "/mnt/dir".to_string(),
907 host: temp.path().to_path_buf(),
908 };
909 let contents = "Hello, World!";
910 let file_txt = temp.path().join("file.txt");
911 std::fs::write(&file_txt, contents).unwrap();
912 let metadata = std::fs::metadata(&file_txt).unwrap();
913
914 let got = MountedDirectory::from(dir);
915
916 let directory_contents: Vec<_> = got
917 .fs
918 .read_dir("/".as_ref())
919 .unwrap()
920 .map(|entry| entry.unwrap())
921 .collect();
922 assert_eq!(
923 directory_contents,
924 vec![DirEntry {
925 path: PathBuf::from("/file.txt"),
926 metadata: Ok(Metadata {
927 ft: FileType::new_file(),
928 accessed: metadata
931 .accessed()
932 .ok()
933 .and_then(unix_timestamp_nanos)
934 .unwrap_or(0),
935 created: metadata
936 .created()
937 .ok()
938 .and_then(unix_timestamp_nanos)
939 .unwrap_or(0),
940 modified: metadata
941 .modified()
942 .ok()
943 .and_then(unix_timestamp_nanos)
944 .unwrap_or(0),
945 len: contents.len() as u64,
946 })
947 }]
948 );
949 }
950}