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