1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use anyhow::{Context, Error};
8use futures::future::BoxFuture;
9use tokio::runtime::Handle;
10use virtual_fs::{FileSystem, FsError, OverlayFileSystem, RootFileSystemBuilder, TmpFileSystem};
11use webc::metadata::annotations::Wasi as WasiAnnotation;
12
13use crate::{
14 WasiEnvBuilder,
15 bin_factory::BinaryPackage,
16 capabilities::Capabilities,
17 journal::{DynJournal, DynReadableJournal, SnapshotTrigger},
18};
19
20pub const MAPPED_CURRENT_DIR_DEFAULT_PATH: &str = "/home";
21
22#[derive(Debug, Clone)]
23pub struct MappedCommand {
24 pub alias: String,
26 pub target: String,
28}
29
30#[derive(Debug, Default, Clone)]
31pub(crate) struct CommonWasiOptions {
32 pub(crate) entry_function: Option<String>,
33 pub(crate) args: Vec<String>,
34 pub(crate) env: HashMap<String, String>,
35 pub(crate) forward_host_env: bool,
36 pub(crate) mapped_host_commands: Vec<MappedCommand>,
37 pub(crate) mounts: Vec<MountedDirectory>,
38 pub(crate) is_home_mapped: bool,
39 pub(crate) injected_packages: Vec<BinaryPackage>,
40 pub(crate) capabilities: Capabilities,
41 pub(crate) read_only_journals: Vec<Arc<DynReadableJournal>>,
42 pub(crate) writable_journals: Vec<Arc<DynJournal>>,
43 pub(crate) snapshot_on: Vec<SnapshotTrigger>,
44 pub(crate) snapshot_interval: Option<std::time::Duration>,
45 pub(crate) stop_running_after_snapshot: bool,
46 pub(crate) skip_stdio_during_bootstrap: bool,
47 pub(crate) current_dir: Option<PathBuf>,
48}
49
50impl CommonWasiOptions {
51 pub(crate) fn prepare_webc_env(
52 &self,
53 builder: &mut WasiEnvBuilder,
54 container_fs: Option<Arc<dyn FileSystem + Send + Sync>>,
55 wasi: &WasiAnnotation,
56 root_fs: Option<TmpFileSystem>,
57 ) -> Result<(), anyhow::Error> {
58 if let Some(ref entry_function) = self.entry_function {
59 builder.set_entry_function(entry_function);
60 }
61
62 let root_fs = root_fs.unwrap_or_else(|| {
63 let mapped_dirs = self
64 .mounts
65 .iter()
66 .map(|d| d.guest.as_str())
67 .collect::<Vec<_>>();
68 RootFileSystemBuilder::default().build_ext(&mapped_dirs)
69 });
70 let fs = prepare_filesystem(root_fs, &self.mounts, container_fs)?;
71
72 if self.mounts.iter().all(|m| m.guest != ".") {
74 let path = builder.get_current_dir().unwrap_or(PathBuf::from("/"));
76 builder.add_map_dir(".", path)?;
77 }
78
79 builder.add_preopen_dir("/")?;
80
81 builder.set_fs(Box::new(fs));
82
83 for pkg in &self.injected_packages {
84 builder.add_webc(pkg.clone());
85 }
86
87 let mapped_cmds = self
88 .mapped_host_commands
89 .iter()
90 .map(|c| (c.alias.as_str(), c.target.as_str()));
91 builder.add_mapped_commands(mapped_cmds);
92
93 self.populate_env(wasi, builder);
94 self.populate_args(wasi, builder);
95
96 *builder.capabilities_mut() = self.capabilities.clone();
97
98 #[cfg(feature = "journal")]
99 {
100 for journal in &self.read_only_journals {
101 builder.add_read_only_journal(journal.clone());
102 }
103 for journal in &self.writable_journals {
104 builder.add_writable_journal(journal.clone());
105 }
106 for trigger in &self.snapshot_on {
107 builder.add_snapshot_trigger(*trigger);
108 }
109 if let Some(interval) = self.snapshot_interval {
110 builder.with_snapshot_interval(interval);
111 }
112 builder.with_stop_running_after_snapshot(self.stop_running_after_snapshot);
113 }
114
115 Ok(())
116 }
117
118 fn populate_env(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
119 for item in wasi.env.as_deref().unwrap_or_default() {
120 match item.split_once('=') {
124 Some((k, v)) => {
125 builder.add_env(k, v);
126 }
127 None => {
128 builder.add_env(item, String::new());
129 }
130 }
131 }
132
133 if self.forward_host_env {
134 builder.add_envs(std::env::vars());
135 }
136
137 builder.add_envs(self.env.clone());
138 }
139
140 fn populate_args(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
141 if let Some(main_args) = &wasi.main_args {
142 builder.add_args(main_args);
143 }
144
145 builder.add_args(&self.args);
146 }
147}
148
149fn build_directory_mappings(
153 root_fs: &mut TmpFileSystem,
154 mounted_dirs: &[MountedDirectory],
155) -> Result<(), anyhow::Error> {
156 for dir in mounted_dirs {
157 let MountedDirectory {
158 guest: guest_path,
159 fs,
160 } = dir;
161 let mut guest_path = PathBuf::from(guest_path);
162 tracing::debug!(
163 guest=%guest_path.display(),
164 "Mounting",
165 );
166
167 if guest_path.is_relative() {
168 guest_path = apply_relative_path_mounting_hack(&guest_path);
169 }
170
171 let guest_path = root_fs
172 .canonicalize_unchecked(&guest_path)
173 .with_context(|| {
174 format!(
175 "Unable to canonicalize guest path '{}'",
176 guest_path.display()
177 )
178 })?;
179
180 if guest_path == Path::new("/") {
181 root_fs
182 .mount_directory_entries(&guest_path, fs, "/".as_ref())
183 .context("Unable to mount to root")?;
184 } else {
185 if let Some(parent) = guest_path.parent() {
186 create_dir_all(&*root_fs, parent).with_context(|| {
187 format!("Unable to create the \"{}\" directory", parent.display())
188 })?;
189 }
190
191 TmpFileSystem::mount(root_fs, guest_path.clone(), fs, "/".into())
192 .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?;
193 }
194 }
195
196 Ok(())
197}
198
199fn prepare_filesystem(
200 mut root_fs: TmpFileSystem,
201 mounted_dirs: &[MountedDirectory],
202 container_fs: Option<Arc<dyn FileSystem + Send + Sync>>,
203) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
204 if !mounted_dirs.is_empty() {
205 build_directory_mappings(&mut root_fs, mounted_dirs)?;
206 }
207
208 let fs = if let Some(container) = container_fs {
218 let container = RelativeOrAbsolutePathHack(container);
219 let fs = OverlayFileSystem::new(root_fs, [container]);
220 Box::new(fs) as Box<dyn FileSystem + Send + Sync>
221 } else {
222 let fs = RelativeOrAbsolutePathHack(root_fs);
223 Box::new(fs) as Box<dyn FileSystem + Send + Sync>
224 };
225
226 Ok(fs)
227}
228
229fn apply_relative_path_mounting_hack(original: &Path) -> PathBuf {
241 debug_assert!(original.is_relative());
242
243 let root = Path::new("/");
244 let mapped_path = if original == Path::new(".") {
245 root.to_path_buf()
246 } else {
247 root.join(original)
248 };
249
250 tracing::debug!(
251 original_path=%original.display(),
252 remapped_path=%mapped_path.display(),
253 "Remapping a relative path"
254 );
255
256 mapped_path
257}
258
259fn create_dir_all(fs: &dyn FileSystem, path: &Path) -> Result<(), Error> {
260 if fs.metadata(path).is_ok() {
261 return Ok(());
262 }
263
264 if let Some(parent) = path.parent() {
265 create_dir_all(fs, parent)?;
266 }
267
268 fs.create_dir(path)?;
269
270 Ok(())
271}
272
273#[derive(Debug, Clone)]
274pub struct MountedDirectory {
275 pub guest: String,
276 pub fs: Arc<dyn FileSystem + Send + Sync>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
288pub struct MappedDirectory {
289 pub host: std::path::PathBuf,
291 pub guest: String,
294}
295
296impl From<MappedDirectory> for MountedDirectory {
297 fn from(value: MappedDirectory) -> Self {
298 cfg_if::cfg_if! {
299 if #[cfg(feature = "host-fs")] {
300 let MappedDirectory { host, guest } = value;
301 let fs: Arc<dyn FileSystem + Send + Sync> =
302 Arc::new(virtual_fs::host_fs::FileSystem::new(Handle::current(), host).unwrap());
303
304 MountedDirectory { guest, fs }
305 } else {
306 unreachable!("The `host-fs` feature needs to be enabled to map {value:?}")
307 }
308 }
309 }
310}
311
312#[derive(Debug)]
313struct RelativeOrAbsolutePathHack<F>(F);
314
315impl<F: FileSystem> RelativeOrAbsolutePathHack<F> {
316 fn execute<Func, Ret>(&self, path: &Path, operation: Func) -> Result<Ret, FsError>
317 where
318 Func: Fn(&F, &Path) -> Result<Ret, FsError>,
319 {
320 let result = operation(&self.0, path);
322
323 if result.is_err() && !path.is_absolute() {
324 let path = Path::new("/").join(path);
327 operation(&self.0, &path)
328 } else {
329 result
330 }
331 }
332}
333
334impl<F: FileSystem> virtual_fs::FileSystem for RelativeOrAbsolutePathHack<F> {
335 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
336 self.execute(path, |fs, p| fs.readlink(p))
337 }
338
339 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
340 self.execute(path, |fs, p| fs.read_dir(p))
341 }
342
343 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
344 self.execute(path, |fs, p| fs.create_dir(p))
345 }
346
347 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
348 self.execute(path, |fs, p| fs.remove_dir(p))
349 }
350
351 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
352 let from = from.to_owned();
353 let to = to.to_owned();
354 Box::pin(async move { self.0.rename(&from, &to).await })
355 }
356
357 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
358 self.execute(path, |fs, p| fs.metadata(p))
359 }
360
361 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
362 self.execute(path, |fs, p| fs.symlink_metadata(p))
363 }
364
365 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
366 self.execute(path, |fs, p| fs.remove_file(p))
367 }
368
369 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
370 virtual_fs::OpenOptions::new(self)
371 }
372
373 fn mount(
374 &self,
375 name: String,
376 path: &Path,
377 fs: Box<dyn FileSystem + Send + Sync>,
378 ) -> virtual_fs::Result<()> {
379 let name_ref = &name;
380 let f_ref = &Arc::new(fs);
381 self.execute(path, move |f, p| {
382 f.mount(name_ref.clone(), p, Box::new(f_ref.clone()))
383 })
384 }
385}
386
387impl<F: FileSystem> virtual_fs::FileOpener for RelativeOrAbsolutePathHack<F> {
388 fn open(
389 &self,
390 path: &Path,
391 conf: &virtual_fs::OpenOptionsConfig,
392 ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
393 self.execute(path, |fs, p| {
394 fs.new_open_options().options(conf.clone()).open(p)
395 })
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use std::time::SystemTime;
402
403 use tempfile::TempDir;
404 use virtual_fs::{DirEntry, FileType, Metadata, WebcVolumeFileSystem};
405 use wasmer_package::utils::from_bytes;
406
407 use super::*;
408
409 const PYTHON: &[u8] = include_bytes!("../../../c-api/examples/assets/python-0.1.0.wasmer");
410
411 #[tokio::test]
413 async fn mix_args_from_the_webc_and_user() {
414 let args = CommonWasiOptions {
415 args: vec!["extra".to_string(), "args".to_string()],
416 ..Default::default()
417 };
418 let mut builder = WasiEnvBuilder::new("program-name");
419 let fs = Arc::new(virtual_fs::EmptyFileSystem::default());
420 let mut annotations = WasiAnnotation::new("some-atom");
421 annotations.main_args = Some(vec![
422 "hard".to_string(),
423 "coded".to_string(),
424 "args".to_string(),
425 ]);
426
427 args.prepare_webc_env(&mut builder, Some(fs), &annotations, None)
428 .unwrap();
429
430 assert_eq!(
431 builder.get_args(),
432 [
433 "program-name",
435 "hard",
437 "coded",
438 "args",
439 "extra",
441 "args",
442 ]
443 );
444 }
445
446 #[tokio::test]
447 async fn mix_env_vars_from_the_webc_and_user() {
448 let args = CommonWasiOptions {
449 env: vec![("EXTRA".to_string(), "envs".to_string())]
450 .into_iter()
451 .collect(),
452 ..Default::default()
453 };
454 let mut builder = WasiEnvBuilder::new("python");
455 let fs = Arc::new(virtual_fs::EmptyFileSystem::default());
456 let mut annotations = WasiAnnotation::new("python");
457 annotations.env = Some(vec!["HARD_CODED=env-vars".to_string()]);
458
459 args.prepare_webc_env(&mut builder, Some(fs), &annotations, None)
460 .unwrap();
461
462 assert_eq!(
463 builder.get_env(),
464 [
465 ("HARD_CODED".to_string(), b"env-vars".to_vec()),
466 ("EXTRA".to_string(), b"envs".to_vec()),
467 ]
468 );
469 }
470
471 #[tokio::test]
472 #[cfg_attr(not(feature = "host-fs"), ignore)]
473 async fn python_use_case() {
474 let temp = TempDir::new().unwrap();
475 let sub_dir = temp.path().join("path").join("to");
476 std::fs::create_dir_all(&sub_dir).unwrap();
477 std::fs::write(sub_dir.join("file.txt"), b"Hello, World!").unwrap();
478 let mapping = [MountedDirectory::from(MappedDirectory {
479 guest: "/home".to_string(),
480 host: sub_dir,
481 })];
482 let container = from_bytes(PYTHON).unwrap();
483 let webc_fs = WebcVolumeFileSystem::mount_all(&container);
484
485 let root_fs = RootFileSystemBuilder::default().build();
486 let fs = prepare_filesystem(root_fs, &mapping, Some(Arc::new(webc_fs))).unwrap();
487
488 assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file());
489 assert!(fs.metadata("lib".as_ref()).unwrap().is_dir());
490 assert!(
491 fs.metadata("lib/python3.6/collections/__init__.py".as_ref())
492 .unwrap()
493 .is_file()
494 );
495 assert!(
496 fs.metadata("lib/python3.6/encodings/__init__.py".as_ref())
497 .unwrap()
498 .is_file()
499 );
500 }
501
502 fn unix_timestamp_nanos(instant: SystemTime) -> Option<u64> {
503 let duration = instant.duration_since(SystemTime::UNIX_EPOCH).ok()?;
504 Some(duration.as_nanos() as u64)
505 }
506
507 #[tokio::test]
508 #[cfg_attr(not(feature = "host-fs"), ignore)]
509 async fn convert_mapped_directory_to_mounted_directory() {
510 let temp = TempDir::new().unwrap();
511 let dir = MappedDirectory {
512 guest: "/mnt/dir".to_string(),
513 host: temp.path().to_path_buf(),
514 };
515 let contents = "Hello, World!";
516 let file_txt = temp.path().join("file.txt");
517 std::fs::write(&file_txt, contents).unwrap();
518 let metadata = std::fs::metadata(&file_txt).unwrap();
519
520 let got = MountedDirectory::from(dir);
521
522 let directory_contents: Vec<_> = got
523 .fs
524 .read_dir("/".as_ref())
525 .unwrap()
526 .map(|entry| entry.unwrap())
527 .collect();
528 assert_eq!(
529 directory_contents,
530 vec![DirEntry {
531 path: PathBuf::from("/file.txt"),
532 metadata: Ok(Metadata {
533 ft: FileType::new_file(),
534 accessed: metadata
537 .accessed()
538 .ok()
539 .and_then(unix_timestamp_nanos)
540 .unwrap_or(0),
541 created: metadata
542 .created()
543 .ok()
544 .and_then(unix_timestamp_nanos)
545 .unwrap_or(0),
546 modified: metadata
547 .modified()
548 .ok()
549 .and_then(unix_timestamp_nanos)
550 .unwrap_or(0),
551 len: contents.len() as u64,
552 })
553 }]
554 );
555 }
556}