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