wasmer_wasix/state/
builder.rs

1//! Builder system for configuring a [`WasiState`] and creating it.
2
3use std::{
4    collections::{HashMap, HashSet},
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use rand::RngExt;
10use thiserror::Error;
11use virtual_fs::{ArcFile, FileSystem, FsError, TmpFileSystem, VirtualFile};
12use wasmer::{AsStoreMut, Engine, Instance, Module};
13use wasmer_config::package::PackageId;
14
15#[cfg(feature = "journal")]
16use crate::journal::{DynJournal, DynReadableJournal, SnapshotTrigger};
17use crate::{
18    Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiThreadError,
19    bin_factory::{BinFactory, BinaryPackage},
20    capabilities::Capabilities,
21    fs::{WasiFs, WasiFsRoot, WasiInodes},
22    os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane},
23    state::WasiState,
24    syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
25};
26use wasmer_types::ModuleHash;
27use wasmer_wasix_types::wasi::SignalDisposition;
28
29use super::env::WasiEnvInit;
30
31/// Builder API for configuring a [`WasiEnv`] environment needed to run WASI modules.
32///
33/// Usage:
34/// ```no_run
35/// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
36/// # fn main() -> Result<(), WasiStateCreationError> {
37/// let mut state_builder = WasiEnv::builder("wasi-prog-name");
38/// state_builder
39///    .env("ENV_VAR", "ENV_VAL")
40///    .arg("--verbose")
41///    .preopen_dir("src")?
42///    .map_dir("name_wasi_sees", "path/on/host/fs")?
43///    .build_init()?;
44/// # Ok(())
45/// # }
46/// ```
47#[derive(Default)]
48pub struct WasiEnvBuilder {
49    /// Name of entry function. Defaults to running `_start` if not specified.
50    pub(super) entry_function: Option<String>,
51    /// Command line arguments.
52    pub(super) args: Vec<String>,
53    /// Environment variables.
54    pub(super) envs: Vec<(String, Vec<u8>)>,
55    /// Signals that should get their handler overridden.
56    pub(super) signals: Vec<SignalDisposition>,
57    /// Pre-opened directories that will be accessible from WASI.
58    pub(super) preopens: Vec<PreopenedDir>,
59    /// Pre-opened virtual directories that will be accessible from WASI.
60    vfs_preopens: Vec<String>,
61    #[allow(clippy::type_complexity)]
62    pub(super) setup_fs_fn:
63        Option<Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>>,
64    pub(super) stdout: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
65    pub(super) stderr: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
66    pub(super) stdin: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
67    pub(super) fs: Option<WasiFsRoot>,
68    pub(super) engine: Option<Engine>,
69    pub(super) runtime: Option<Arc<dyn crate::Runtime + Send + Sync + 'static>>,
70    pub(super) current_dir: Option<PathBuf>,
71
72    /// List of webc dependencies to be injected.
73    pub(super) uses: Vec<BinaryPackage>,
74
75    pub(super) included_packages: HashSet<PackageId>,
76
77    pub(super) module_hash: Option<ModuleHash>,
78
79    /// List of host commands to map into the WASI instance.
80    pub(super) map_commands: HashMap<String, PathBuf>,
81
82    pub(super) capabilites: Capabilities,
83
84    #[cfg(feature = "journal")]
85    pub(super) snapshot_on: Vec<SnapshotTrigger>,
86
87    #[cfg(feature = "journal")]
88    pub(super) snapshot_interval: Option<std::time::Duration>,
89
90    #[cfg(feature = "journal")]
91    pub(super) stop_running_after_snapshot: bool,
92
93    #[cfg(feature = "journal")]
94    pub(super) read_only_journals: Vec<Arc<DynReadableJournal>>,
95
96    #[cfg(feature = "journal")]
97    pub(super) writable_journals: Vec<Arc<DynJournal>>,
98
99    pub(super) skip_stdio_during_bootstrap: bool,
100
101    #[cfg(feature = "ctrlc")]
102    pub(super) attach_ctrl_c: bool,
103}
104
105impl std::fmt::Debug for WasiEnvBuilder {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        // TODO: update this when stable
108        f.debug_struct("WasiEnvBuilder")
109            .field("entry_function", &self.entry_function)
110            .field("args", &self.args)
111            .field("envs", &self.envs)
112            .field("signals", &self.signals)
113            .field("preopens", &self.preopens)
114            .field("uses", &self.uses)
115            .field("setup_fs_fn exists", &self.setup_fs_fn.is_some())
116            .field("stdout_override exists", &self.stdout.is_some())
117            .field("stderr_override exists", &self.stderr.is_some())
118            .field("stdin_override exists", &self.stdin.is_some())
119            .field("engine_override_exists", &self.engine.is_some())
120            .field("runtime_override_exists", &self.runtime.is_some())
121            .finish()
122    }
123}
124
125/// Error type returned when bad data is given to [`WasiEnvBuilder`].
126#[derive(Error, Debug, Clone, PartialEq, Eq)]
127pub enum WasiStateCreationError {
128    #[error("bad environment variable format: `{0}`")]
129    EnvironmentVariableFormatError(String),
130    #[error("argument contains null byte: `{0}`")]
131    ArgumentContainsNulByte(String),
132    #[error("preopened directory not found: `{0}`")]
133    PreopenedDirectoryNotFound(PathBuf),
134    #[error("preopened directory error: `{0}`")]
135    PreopenedDirectoryError(String),
136    #[error("mapped dir alias has wrong format: `{0}`")]
137    MappedDirAliasFormattingError(String),
138    #[error("wasi filesystem creation error: `{0}`")]
139    WasiFsCreationError(String),
140    #[error("wasi filesystem setup error: `{0}`")]
141    WasiFsSetupError(String),
142    #[error(transparent)]
143    FileSystemError(#[from] FsError),
144    #[error("wasi inherit error: `{0}`")]
145    WasiInheritError(String),
146    #[error("wasi include package: `{0}`")]
147    WasiIncludePackageError(String),
148    #[error("control plane error")]
149    ControlPlane(#[from] ControlPlaneError),
150}
151
152fn validate_mapped_dir_alias(alias: &str) -> Result<(), WasiStateCreationError> {
153    if !alias.bytes().all(|b| b != b'\0') {
154        return Err(WasiStateCreationError::MappedDirAliasFormattingError(
155            format!("Alias \"{alias}\" contains a nul byte"),
156        ));
157    }
158
159    Ok(())
160}
161
162pub type SetupFsFn = Box<dyn Fn(&WasiInodes, &mut WasiFs) -> Result<(), String> + Send>;
163
164// TODO add other WasiFS APIs here like swapping out stdout, for example (though we need to
165// return stdout somehow, it's unclear what that API should look like)
166impl WasiEnvBuilder {
167    /// Creates an empty [`WasiEnvBuilder`].
168    pub fn new(program_name: impl Into<String>) -> Self {
169        WasiEnvBuilder {
170            args: vec![program_name.into()],
171            ..WasiEnvBuilder::default()
172        }
173    }
174
175    /// Attaches a ctrl-c handler which will send signals to the
176    /// process rather than immediately termiante it
177    #[cfg(feature = "ctrlc")]
178    pub fn attach_ctrl_c(mut self) -> Self {
179        self.attach_ctrl_c = true;
180        self
181    }
182
183    /// Add an environment variable pair.
184    ///
185    /// Both the key and value of an environment variable must not
186    /// contain a nul byte (`0x0`), and the key must not contain the
187    /// `=` byte (`0x3d`).
188    pub fn env<Key, Value>(mut self, key: Key, value: Value) -> Self
189    where
190        Key: AsRef<[u8]>,
191        Value: AsRef<[u8]>,
192    {
193        self.add_env(key, value);
194        self
195    }
196
197    /// Add an environment variable pair.
198    ///
199    /// Both the key and value of an environment variable must not
200    /// contain a nul byte (`0x0`), and the key must not contain the
201    /// `=` byte (`0x3d`).
202    pub fn add_env<Key, Value>(&mut self, key: Key, value: Value)
203    where
204        Key: AsRef<[u8]>,
205        Value: AsRef<[u8]>,
206    {
207        self.envs.push((
208            String::from_utf8_lossy(key.as_ref()).to_string(),
209            value.as_ref().to_vec(),
210        ));
211    }
212
213    /// Add multiple environment variable pairs.
214    ///
215    /// Both the key and value of the environment variables must not
216    /// contain a nul byte (`0x0`), and the key must not contain the
217    /// `=` byte (`0x3d`).
218    pub fn envs<I, Key, Value>(mut self, env_pairs: I) -> Self
219    where
220        I: IntoIterator<Item = (Key, Value)>,
221        Key: AsRef<[u8]>,
222        Value: AsRef<[u8]>,
223    {
224        self.add_envs(env_pairs);
225
226        self
227    }
228
229    /// Add multiple environment variable pairs.
230    ///
231    /// Both the key and value of the environment variables must not
232    /// contain a nul byte (`0x0`), and the key must not contain the
233    /// `=` byte (`0x3d`).
234    pub fn add_envs<I, Key, Value>(&mut self, env_pairs: I)
235    where
236        I: IntoIterator<Item = (Key, Value)>,
237        Key: AsRef<[u8]>,
238        Value: AsRef<[u8]>,
239    {
240        for (key, value) in env_pairs {
241            self.add_env(key, value);
242        }
243    }
244
245    /// Get a reference to the configured environment variables.
246    pub fn get_env(&self) -> &[(String, Vec<u8>)] {
247        &self.envs
248    }
249
250    /// Get a mutable reference to the configured environment variables.
251    pub fn get_env_mut(&mut self) -> &mut Vec<(String, Vec<u8>)> {
252        &mut self.envs
253    }
254
255    /// Add a signal handler override.
256    pub fn signal(mut self, sig_action: SignalDisposition) -> Self {
257        self.add_signal(sig_action);
258        self
259    }
260
261    /// Add a signal handler override.
262    pub fn add_signal(&mut self, sig_action: SignalDisposition) {
263        self.signals.push(sig_action);
264    }
265
266    /// Add multiple signal handler overrides.
267    pub fn signals<I>(mut self, signal_pairs: I) -> Self
268    where
269        I: IntoIterator<Item = SignalDisposition>,
270    {
271        self.add_signals(signal_pairs);
272
273        self
274    }
275
276    /// Add multiple signal handler overrides.
277    pub fn add_signals<I>(&mut self, signal_pairs: I)
278    where
279        I: IntoIterator<Item = SignalDisposition>,
280    {
281        for sig in signal_pairs {
282            self.add_signal(sig);
283        }
284    }
285
286    /// Get a reference to the configured signal handler overrides.
287    pub fn get_signals(&self) -> &[SignalDisposition] {
288        &self.signals
289    }
290
291    /// Get a mutable reference to the configured signalironment variables.
292    pub fn get_signals_mut(&mut self) -> &mut Vec<SignalDisposition> {
293        &mut self.signals
294    }
295
296    pub fn entry_function<S>(mut self, entry_function: S) -> Self
297    where
298        S: AsRef<str>,
299    {
300        self.set_entry_function(entry_function);
301        self
302    }
303
304    pub fn set_entry_function<S>(&mut self, entry_function: S)
305    where
306        S: AsRef<str>,
307    {
308        self.entry_function = Some(entry_function.as_ref().to_owned());
309    }
310
311    /// Add an argument.
312    ///
313    /// Arguments must not contain the nul (0x0) byte
314    // TODO: should take Into<Vec<u8>>
315    pub fn arg<V>(mut self, arg: V) -> Self
316    where
317        V: AsRef<[u8]>,
318    {
319        self.add_arg(arg);
320        self
321    }
322
323    /// Add an argument.
324    ///
325    /// Arguments must not contain the nul (0x0) byte.
326    // TODO: should take Into<Vec<u8>>
327    pub fn add_arg<V>(&mut self, arg: V)
328    where
329        V: AsRef<[u8]>,
330    {
331        self.args
332            .push(String::from_utf8_lossy(arg.as_ref()).to_string());
333    }
334
335    /// Add multiple arguments.
336    ///
337    /// Arguments must not contain the nul (0x0) byte
338    pub fn args<I, Arg>(mut self, args: I) -> Self
339    where
340        I: IntoIterator<Item = Arg>,
341        Arg: AsRef<[u8]>,
342    {
343        self.add_args(args);
344
345        self
346    }
347
348    /// Add multiple arguments.
349    ///
350    /// Arguments must not contain the nul (0x0) byte
351    pub fn add_args<I, Arg>(&mut self, args: I)
352    where
353        I: IntoIterator<Item = Arg>,
354        Arg: AsRef<[u8]>,
355    {
356        for arg in args {
357            self.add_arg(arg);
358        }
359    }
360
361    /// Get a reference to the configured arguments.
362    pub fn get_args(&self) -> &[String] {
363        &self.args
364    }
365
366    /// Get a mutable reference to the configured arguments.
367    pub fn get_args_mut(&mut self) -> &mut Vec<String> {
368        &mut self.args
369    }
370
371    /// Adds a container this module inherits from.
372    ///
373    /// This will make all of the container's files and commands available to the
374    /// resulting WASI instance.
375    pub fn use_webc(mut self, pkg: BinaryPackage) -> Self {
376        self.add_webc(pkg);
377        self
378    }
379
380    /// Sets the module hash for the running process. This ensures that the journal
381    /// can restore the records for the right module. If no module hash is supplied
382    /// then the process will start with a random module hash.
383    pub fn set_module_hash(&mut self, hash: ModuleHash) -> &mut Self {
384        self.module_hash.replace(hash);
385        self
386    }
387
388    /// Adds a container this module inherits from.
389    ///
390    /// This will make all of the container's files and commands available to the
391    /// resulting WASI instance.
392    pub fn add_webc(&mut self, pkg: BinaryPackage) -> &mut Self {
393        self.uses.push(pkg);
394        self
395    }
396
397    /// Adds a package that is already included in the [`WasiEnvBuilder`] filesystem.
398    /// These packages will not be merged to the final filesystem since they are already included.
399    pub fn include_package(&mut self, pkg_id: PackageId) -> &mut Self {
400        self.included_packages.insert(pkg_id);
401        self
402    }
403
404    /// Adds packages that is already included in the [`WasiEnvBuilder`] filesystem.
405    /// These packages will not be merged to the final filesystem since they are already included.
406    pub fn include_packages(&mut self, pkg_ids: impl IntoIterator<Item = PackageId>) -> &mut Self {
407        self.included_packages.extend(pkg_ids);
408
409        self
410    }
411
412    /// Adds a list of other containers this module inherits from.
413    ///
414    /// This will make all of the container's files and commands available to the
415    /// resulting WASI instance.
416    pub fn uses<I>(mut self, uses: I) -> Self
417    where
418        I: IntoIterator<Item = BinaryPackage>,
419    {
420        for pkg in uses {
421            self.add_webc(pkg);
422        }
423        self
424    }
425
426    /// Map an atom to a local binary
427    pub fn map_command<Name, Target>(mut self, name: Name, target: Target) -> Self
428    where
429        Name: AsRef<str>,
430        Target: AsRef<str>,
431    {
432        self.add_mapped_command(name, target);
433        self
434    }
435
436    /// Map an atom to a local binary
437    pub fn add_mapped_command<Name, Target>(&mut self, name: Name, target: Target)
438    where
439        Name: AsRef<str>,
440        Target: AsRef<str>,
441    {
442        let path_buf = PathBuf::from(target.as_ref().to_string());
443        self.map_commands
444            .insert(name.as_ref().to_string(), path_buf);
445    }
446
447    /// Maps a series of atoms to the local binaries
448    pub fn map_commands<I, Name, Target>(mut self, map_commands: I) -> Self
449    where
450        I: IntoIterator<Item = (Name, Target)>,
451        Name: AsRef<str>,
452        Target: AsRef<str>,
453    {
454        self.add_mapped_commands(map_commands);
455        self
456    }
457
458    /// Maps a series of atoms to local binaries.
459    pub fn add_mapped_commands<I, Name, Target>(&mut self, map_commands: I)
460    where
461        I: IntoIterator<Item = (Name, Target)>,
462        Name: AsRef<str>,
463        Target: AsRef<str>,
464    {
465        for (alias, target) in map_commands {
466            self.add_mapped_command(alias, target);
467        }
468    }
469
470    /// Preopen a directory
471    ///
472    /// This opens the given directory at the virtual root, `/`, and allows
473    /// the WASI module to read and write to the given directory.
474    pub fn preopen_dir<P>(mut self, po_dir: P) -> Result<Self, WasiStateCreationError>
475    where
476        P: AsRef<Path>,
477    {
478        self.add_preopen_dir(po_dir)?;
479        Ok(self)
480    }
481
482    /// Adds a preopen a directory
483    ///
484    /// This opens the given directory at the virtual root, `/`, and allows
485    /// the WASI module to read and write to the given directory.
486    pub fn add_preopen_dir<P>(&mut self, po_dir: P) -> Result<(), WasiStateCreationError>
487    where
488        P: AsRef<Path>,
489    {
490        let mut pdb = PreopenDirBuilder::new();
491        let path = po_dir.as_ref();
492        pdb.directory(path).read(true).write(true).create(true);
493        let preopen = pdb.build()?;
494
495        self.preopens.push(preopen);
496
497        Ok(())
498    }
499
500    /// Preopen multiple directories.
501    ///
502    /// This opens the given directories at the virtual root, `/`, and allows
503    /// the WASI module to read and write to the given directory.
504    pub fn preopen_dirs<I, P>(mut self, dirs: I) -> Result<Self, WasiStateCreationError>
505    where
506        I: IntoIterator<Item = P>,
507        P: AsRef<Path>,
508    {
509        for po_dir in dirs {
510            self.add_preopen_dir(po_dir)?;
511        }
512
513        Ok(self)
514    }
515
516    /// Preopen a directory and configure it.
517    ///
518    /// Usage:
519    ///
520    /// ```no_run
521    /// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
522    /// # fn main() -> Result<(), WasiStateCreationError> {
523    /// WasiEnv::builder("program_name")
524    ///    .preopen_build(|p| p.directory("src").read(true).write(true).create(true))?
525    ///    .preopen_build(|p| p.directory(".").alias("dot").read(true))?
526    ///    .build_init()?;
527    /// # Ok(())
528    /// # }
529    /// ```
530    pub fn preopen_build<F>(mut self, inner: F) -> Result<Self, WasiStateCreationError>
531    where
532        F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
533    {
534        self.add_preopen_build(inner)?;
535        Ok(self)
536    }
537
538    /// Preopen a directory and configure it.
539    ///
540    /// Usage:
541    ///
542    /// ```no_run
543    /// # use wasmer_wasix::{WasiEnv, WasiStateCreationError};
544    /// # fn main() -> Result<(), WasiStateCreationError> {
545    /// WasiEnv::builder("program_name")
546    ///    .preopen_build(|p| p.directory("src").read(true).write(true).create(true))?
547    ///    .preopen_build(|p| p.directory(".").alias("dot").read(true))?
548    ///    .build_init()?;
549    /// # Ok(())
550    /// # }
551    /// ```
552    pub fn add_preopen_build<F>(&mut self, inner: F) -> Result<(), WasiStateCreationError>
553    where
554        F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
555    {
556        let mut pdb = PreopenDirBuilder::new();
557        let po_dir = inner(&mut pdb).build()?;
558
559        self.preopens.push(po_dir);
560
561        Ok(())
562    }
563
564    /// Preopen the given directories from the
565    /// Virtual FS.
566    pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
567    where
568        I: IntoIterator<Item = String>,
569    {
570        for po_dir in po_dirs {
571            self.vfs_preopens.push(po_dir);
572        }
573
574        Ok(self)
575    }
576
577    /// Preopen a directory with a different name exposed to the WASI.
578    pub fn map_dir<P>(mut self, alias: &str, po_dir: P) -> Result<Self, WasiStateCreationError>
579    where
580        P: AsRef<Path>,
581    {
582        self.add_map_dir(alias, po_dir)?;
583        Ok(self)
584    }
585
586    /// Preopen a directory with a different name exposed to the WASI.
587    pub fn add_map_dir<P>(&mut self, alias: &str, po_dir: P) -> Result<(), WasiStateCreationError>
588    where
589        P: AsRef<Path>,
590    {
591        let mut pdb = PreopenDirBuilder::new();
592        let path = po_dir.as_ref();
593        pdb.directory(path)
594            .alias(alias)
595            .read(true)
596            .write(true)
597            .create(true);
598        let preopen = pdb.build()?;
599
600        self.preopens.push(preopen);
601
602        Ok(())
603    }
604
605    /// Preopen directorys with a different names exposed to the WASI.
606    pub fn map_dirs<I, P>(mut self, mapped_dirs: I) -> Result<Self, WasiStateCreationError>
607    where
608        I: IntoIterator<Item = (String, P)>,
609        P: AsRef<Path>,
610    {
611        for (alias, dir) in mapped_dirs {
612            self.add_map_dir(&alias, dir)?;
613        }
614
615        Ok(self)
616    }
617
618    /// Specifies one or more journal files that Wasmer will use to restore
619    /// the state of the WASM process.
620    ///
621    /// The state of the WASM process and its sandbox will be reapplied use
622    /// the journals in the order that you specify here.
623    #[cfg(feature = "journal")]
624    pub fn add_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) {
625        self.read_only_journals.push(journal);
626    }
627
628    /// Specifies one or more journal files that Wasmer will use to restore
629    /// the state of the WASM process.
630    ///
631    /// The state of the WASM process and its sandbox will be reapplied use
632    /// the journals in the order that you specify here.
633    ///
634    /// The last journal file specified will be created if it does not exist
635    /// and opened for read and write. New journal events will be written to this
636    /// file
637    #[cfg(feature = "journal")]
638    pub fn add_writable_journal(&mut self, journal: Arc<DynJournal>) {
639        self.writable_journals.push(journal);
640    }
641
642    pub fn get_current_dir(&mut self) -> Option<PathBuf> {
643        self.current_dir.clone()
644    }
645
646    pub fn set_current_dir(&mut self, dir: impl Into<PathBuf>) {
647        self.current_dir = Some(dir.into());
648    }
649
650    pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
651        self.set_current_dir(dir);
652        self
653    }
654
655    /// Overwrite the default WASI `stdout`, if you want to hold on to the
656    /// original `stdout` use [`WasiFs::swap_file`] after building.
657    pub fn stdout(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
658        self.stdout = Some(new_file);
659
660        self
661    }
662
663    /// Overwrite the default WASI `stdout`, if you want to hold on to the
664    /// original `stdout` use [`WasiFs::swap_file`] after building.
665    pub fn set_stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
666        self.stdout = Some(new_file);
667    }
668
669    /// Overwrite the default WASI `stderr`, if you want to hold on to the
670    /// original `stderr` use [`WasiFs::swap_file`] after building.
671    pub fn stderr(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
672        self.set_stderr(new_file);
673        self
674    }
675
676    /// Overwrite the default WASI `stderr`, if you want to hold on to the
677    /// original `stderr` use [`WasiFs::swap_file`] after building.
678    pub fn set_stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
679        self.stderr = Some(new_file);
680    }
681
682    /// Overwrite the default WASI `stdin`, if you want to hold on to the
683    /// original `stdin` use [`WasiFs::swap_file`] after building.
684    pub fn stdin(mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> Self {
685        self.stdin = Some(new_file);
686
687        self
688    }
689
690    /// Overwrite the default WASI `stdin`, if you want to hold on to the
691    /// original `stdin` use [`WasiFs::swap_file`] after building.
692    pub fn set_stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) {
693        self.stdin = Some(new_file);
694    }
695
696    /// Sets the FileSystem to be used with this WASI instance.
697    ///
698    /// This is usually used in case a custom `virtual_fs::FileSystem` is needed.
699    pub fn fs(mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) -> Self {
700        self.set_fs(fs);
701        self
702    }
703
704    pub fn set_fs(&mut self, fs: impl Into<Arc<dyn virtual_fs::FileSystem + Send + Sync>>) {
705        self.fs = Some(WasiFsRoot::Backing(fs.into()));
706    }
707
708    pub(crate) fn set_fs_root(&mut self, fs: WasiFsRoot) {
709        self.fs = Some(fs);
710    }
711
712    /// Sets a new sandbox FileSystem to be used with this WASI instance.
713    ///
714    /// This is usually used in case a custom `virtual_fs::FileSystem` is needed.
715    pub fn sandbox_fs(mut self, fs: TmpFileSystem) -> Self {
716        self.fs = Some(WasiFsRoot::Sandbox(fs));
717        self
718    }
719
720    /// Configure the WASI filesystem before running.
721    // TODO: improve ergonomics on this function
722    pub fn setup_fs(mut self, setup_fs_fn: SetupFsFn) -> Self {
723        self.setup_fs_fn = Some(setup_fs_fn);
724
725        self
726    }
727
728    /// Sets the wasmer engine and overrides the default; only used if
729    /// a runtime override is not provided.
730    pub fn engine(mut self, engine: Engine) -> Self {
731        self.set_engine(engine);
732        self
733    }
734
735    pub fn set_engine(&mut self, engine: Engine) {
736        self.engine = Some(engine);
737    }
738
739    /// Sets the WASI runtime implementation and overrides the default
740    /// implementation
741    pub fn runtime(mut self, runtime: Arc<dyn Runtime + Send + Sync>) -> Self {
742        self.set_runtime(runtime);
743        self
744    }
745
746    pub fn set_runtime(&mut self, runtime: Arc<dyn Runtime + Send + Sync>) {
747        self.runtime = Some(runtime);
748    }
749
750    pub fn capabilities(mut self, capabilities: Capabilities) -> Self {
751        self.set_capabilities(capabilities);
752        self
753    }
754
755    pub fn capabilities_mut(&mut self) -> &mut Capabilities {
756        &mut self.capabilites
757    }
758
759    pub fn set_capabilities(&mut self, capabilities: Capabilities) {
760        self.capabilites = capabilities;
761    }
762
763    #[cfg(feature = "journal")]
764    pub fn add_snapshot_trigger(&mut self, on: SnapshotTrigger) {
765        self.snapshot_on.push(on);
766    }
767
768    #[cfg(feature = "journal")]
769    pub fn with_snapshot_interval(&mut self, interval: std::time::Duration) {
770        self.snapshot_interval.replace(interval);
771    }
772
773    #[cfg(feature = "journal")]
774    pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) {
775        self.stop_running_after_snapshot = stop_running;
776    }
777
778    pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) {
779        self.skip_stdio_during_bootstrap = skip;
780    }
781
782    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
783    /// can be used to construct a new [`WasiEnv`].
784    ///
785    /// Returns the error from `WasiFs::new` if there's an error
786    ///
787    /// NOTE: You should prefer to not work directly with [`WasiEnvInit`].
788    /// Use [`WasiEnvBuilder::build`] or [`WasiEnvBuilder::instantiate`] instead
789    /// to ensure proper invokation of WASI modules.
790    pub fn build_init(mut self) -> Result<WasiEnvInit, WasiStateCreationError> {
791        for arg in self.args.iter() {
792            for b in arg.as_bytes().iter() {
793                if *b == 0 {
794                    return Err(WasiStateCreationError::ArgumentContainsNulByte(arg.clone()));
795                }
796            }
797        }
798
799        enum InvalidCharacter {
800            Nul,
801            Equal,
802        }
803
804        for (env_key, env_value) in self.envs.iter() {
805            match env_key.as_bytes().iter().find_map(|&ch| {
806                if ch == 0 {
807                    Some(InvalidCharacter::Nul)
808                } else if ch == b'=' {
809                    Some(InvalidCharacter::Equal)
810                } else {
811                    None
812                }
813            }) {
814                Some(InvalidCharacter::Nul) => {
815                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
816                        format!("found nul byte in env var key \"{env_key}\" (key=value)"),
817                    ));
818                }
819
820                Some(InvalidCharacter::Equal) => {
821                    return Err(WasiStateCreationError::EnvironmentVariableFormatError(
822                        format!("found equal sign in env var key \"{env_key}\" (key=value)"),
823                    ));
824                }
825
826                None => (),
827            }
828
829            if env_value.contains(&0) {
830                return Err(WasiStateCreationError::EnvironmentVariableFormatError(
831                    format!(
832                        "found nul byte in env var value \"{}\" (key=value)",
833                        String::from_utf8_lossy(env_value),
834                    ),
835                ));
836            }
837        }
838
839        // Determine the STDIN
840        let stdin: Box<dyn VirtualFile + Send + Sync + 'static> = self
841            .stdin
842            .take()
843            .unwrap_or_else(|| Box::new(ArcFile::new(Box::<super::Stdin>::default())));
844
845        let fs_backing = self
846            .fs
847            .take()
848            .unwrap_or_else(|| WasiFsRoot::Sandbox(TmpFileSystem::new()));
849
850        if let Some(dir) = &self.current_dir {
851            match fs_backing.read_dir(dir) {
852                Ok(_) => {
853                    // All good
854                }
855                Err(FsError::EntryNotFound) => {
856                    fs_backing.create_dir(dir).map_err(|err| {
857                        WasiStateCreationError::WasiFsSetupError(format!(
858                            "Could not create specified current directory at '{}': {err}",
859                            dir.display()
860                        ))
861                    })?;
862                }
863                Err(err) => {
864                    return Err(WasiStateCreationError::WasiFsSetupError(format!(
865                        "Could not check specified current directory at '{}': {err}",
866                        dir.display()
867                    )));
868                }
869            }
870        }
871
872        // self.preopens are checked in [`PreopenDirBuilder::build`]
873        let inodes = crate::state::WasiInodes::new();
874        let wasi_fs = {
875            // self.preopens are checked in [`PreopenDirBuilder::build`]
876            let mut wasi_fs =
877                WasiFs::new_with_preopen(&inodes, &self.preopens, &self.vfs_preopens, fs_backing)
878                    .map_err(WasiStateCreationError::WasiFsCreationError)?;
879
880            // set up the file system, overriding base files and calling the setup function
881            wasi_fs
882                .swap_file(__WASI_STDIN_FILENO, stdin)
883                .map_err(WasiStateCreationError::FileSystemError)?;
884
885            if let Some(stdout_override) = self.stdout.take() {
886                wasi_fs
887                    .swap_file(__WASI_STDOUT_FILENO, stdout_override)
888                    .map_err(WasiStateCreationError::FileSystemError)?;
889            }
890
891            if let Some(stderr_override) = self.stderr.take() {
892                wasi_fs
893                    .swap_file(__WASI_STDERR_FILENO, stderr_override)
894                    .map_err(WasiStateCreationError::FileSystemError)?;
895            }
896
897            if let Some(f) = &self.setup_fs_fn {
898                f(&inodes, &mut wasi_fs).map_err(WasiStateCreationError::WasiFsSetupError)?;
899            }
900            wasi_fs
901        };
902
903        if let Some(dir) = &self.current_dir {
904            let s = dir.to_str().ok_or_else(|| {
905                WasiStateCreationError::WasiFsSetupError(format!(
906                    "Specified current directory is not valid UTF-8: '{}'",
907                    dir.display()
908                ))
909            })?;
910            wasi_fs.set_current_dir(s);
911        }
912
913        for id in &self.included_packages {
914            wasi_fs.has_unioned.lock().unwrap().insert(id.clone());
915        }
916
917        let state = WasiState {
918            fs: wasi_fs,
919            secret: rand::rng().random::<[u8; 32]>(),
920            inodes,
921            args: std::sync::Mutex::new(self.args.clone()),
922            preopen: self.vfs_preopens.clone(),
923            futexs: Default::default(),
924            clock_offset: Default::default(),
925            envs: std::sync::Mutex::new(conv_env_vars(self.envs)),
926            signals: std::sync::Mutex::new(self.signals.iter().map(|s| (s.sig, s.disp)).collect()),
927        };
928
929        let runtime = self.runtime.unwrap_or_else(|| {
930            #[cfg(feature = "sys-thread")]
931            {
932                #[allow(unused_mut)]
933                let mut runtime = crate::runtime::PluggableRuntime::new(Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::default()));
934                runtime.set_engine(
935                    self
936                        .engine
937                        .as_ref()
938                        .expect(
939                            "Neither a runtime nor an engine was provided to WasiEnvBuilder. \
940                            This is not supported because it means the module that's going to \
941                            run with the resulting WasiEnv will have been loaded using a \
942                            different engine than the one that will exist within the WasiEnv. \
943                            Use either `set_runtime` or `set_engine` before calling `build_init`.",
944                        )
945                        .clone()
946                );
947                #[cfg(feature = "journal")]
948                for journal in self.read_only_journals.clone() {
949                    runtime.add_read_only_journal(journal);
950                }
951                #[cfg(feature = "journal")]
952                for journal in self.writable_journals.clone() {
953                    runtime.add_writable_journal(journal);
954                }
955                Arc::new(runtime)
956            }
957
958            #[cfg(not(feature = "sys-thread"))]
959            {
960                panic!("this build does not support a default runtime - specify one with WasiEnvBuilder::runtime()");
961            }
962        });
963
964        let uses = self.uses;
965        let map_commands = self.map_commands;
966
967        let bin_factory = BinFactory::new(runtime.clone());
968
969        let capabilities = self.capabilites;
970
971        let plane_config = ControlPlaneConfig {
972            max_task_count: capabilities.threading.max_threads,
973            enable_asynchronous_threading: capabilities.threading.enable_asynchronous_threading,
974            enable_exponential_cpu_backoff: capabilities.threading.enable_exponential_cpu_backoff,
975        };
976        let control_plane = WasiControlPlane::new(plane_config);
977
978        let init = WasiEnvInit {
979            state,
980            runtime,
981            webc_dependencies: uses,
982            mapped_commands: map_commands,
983            control_plane,
984            bin_factory,
985            capabilities,
986            memory_ty: None,
987            process: None,
988            thread: None,
989            #[cfg(feature = "journal")]
990            call_initialize: self.read_only_journals.is_empty()
991                && self.writable_journals.is_empty(),
992            #[cfg(not(feature = "journal"))]
993            call_initialize: true,
994            can_deep_sleep: false,
995            extra_tracing: true,
996            #[cfg(feature = "journal")]
997            snapshot_on: self.snapshot_on,
998            #[cfg(feature = "journal")]
999            stop_running_after_snapshot: self.stop_running_after_snapshot,
1000            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
1001        };
1002
1003        Ok(init)
1004    }
1005
1006    #[allow(clippy::result_large_err)]
1007    pub fn build(self) -> Result<WasiEnv, WasiRuntimeError> {
1008        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1009        let init = self.build_init()?;
1010        WasiEnv::from_init(init, module_hash)
1011    }
1012
1013    /// Construct a [`WasiFunctionEnv`].
1014    ///
1015    /// NOTE: you still must call [`WasiFunctionEnv::initialize`] to make an
1016    /// instance usable.
1017    #[doc(hidden)]
1018    #[allow(clippy::result_large_err)]
1019    pub fn finalize(
1020        self,
1021        store: &mut impl AsStoreMut,
1022    ) -> Result<WasiFunctionEnv, WasiRuntimeError> {
1023        let module_hash = self.module_hash.unwrap_or_else(ModuleHash::random);
1024        let init = self.build_init()?;
1025        let env = WasiEnv::from_init(init, module_hash)?;
1026        let func_env = WasiFunctionEnv::new(store, env);
1027        Ok(func_env)
1028    }
1029
1030    /// Consumes the [`WasiEnvBuilder`] and produces a [`WasiEnvInit`], which
1031    /// can be used to construct a new [`WasiEnv`].
1032    ///
1033    /// Returns the error from `WasiFs::new` if there's an error
1034    // FIXME: use a proper custom error type
1035    #[allow(clippy::result_large_err)]
1036    pub fn instantiate(
1037        self,
1038        module: Module,
1039        store: &mut impl AsStoreMut,
1040    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1041        self.instantiate_ext(module, ModuleHash::random(), store)
1042    }
1043
1044    #[allow(clippy::result_large_err)]
1045    pub fn instantiate_ext(
1046        self,
1047        module: Module,
1048        module_hash: ModuleHash,
1049        store: &mut impl AsStoreMut,
1050    ) -> Result<(Instance, WasiFunctionEnv), WasiRuntimeError> {
1051        let init = self.build_init()?;
1052        let call_init = init.call_initialize;
1053        let env = WasiEnv::from_init(init, module_hash)?;
1054        let memory = module
1055            .imports()
1056            .find_map(|i| match i.ty() {
1057                wasmer::ExternType::Memory(ty) => Some(*ty),
1058                _ => None,
1059            })
1060            .map(|ty| wasmer::Memory::new(store, ty))
1061            .transpose()
1062            .map_err(WasiThreadError::MemoryCreateFailed)?;
1063        Ok(env.instantiate(module, store, memory, true, call_init, None)?)
1064    }
1065}
1066
1067pub(crate) fn conv_env_vars(envs: Vec<(String, Vec<u8>)>) -> Vec<Vec<u8>> {
1068    envs.into_iter()
1069        .map(|(key, value)| {
1070            let mut env = Vec::with_capacity(key.len() + value.len() + 1);
1071            env.extend_from_slice(key.as_bytes());
1072            env.push(b'=');
1073            env.extend_from_slice(&value);
1074
1075            env
1076        })
1077        .collect()
1078}
1079
1080/// Builder for preopened directories.
1081#[derive(Debug, Default)]
1082pub struct PreopenDirBuilder {
1083    path: Option<PathBuf>,
1084    alias: Option<String>,
1085    read: bool,
1086    write: bool,
1087    create: bool,
1088}
1089
1090/// The built version of `PreopenDirBuilder`
1091#[derive(Debug, Clone, Default)]
1092pub(crate) struct PreopenedDir {
1093    pub(crate) path: PathBuf,
1094    pub(crate) alias: Option<String>,
1095    pub(crate) read: bool,
1096    pub(crate) write: bool,
1097    pub(crate) create: bool,
1098}
1099
1100impl PreopenDirBuilder {
1101    /// Create an empty builder
1102    pub(crate) fn new() -> Self {
1103        PreopenDirBuilder::default()
1104    }
1105
1106    /// Point the preopened directory to the path given by `po_dir`
1107    pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
1108    where
1109        FilePath: AsRef<Path>,
1110    {
1111        let path = po_dir.as_ref();
1112        self.path = Some(path.to_path_buf());
1113
1114        self
1115    }
1116
1117    /// Make this preopened directory appear to the WASI program as `alias`
1118    pub fn alias(&mut self, alias: &str) -> &mut Self {
1119        // We mount at preopened dirs at `/` by default and multiple `/` in a row
1120        // are equal to a single `/`.
1121        let alias = alias.trim_start_matches('/');
1122        self.alias = Some(alias.to_string());
1123
1124        self
1125    }
1126
1127    /// Set read permissions affecting files in the directory
1128    pub fn read(&mut self, toggle: bool) -> &mut Self {
1129        self.read = toggle;
1130
1131        self
1132    }
1133
1134    /// Set write permissions affecting files in the directory
1135    pub fn write(&mut self, toggle: bool) -> &mut Self {
1136        self.write = toggle;
1137
1138        self
1139    }
1140
1141    /// Set create permissions affecting files in the directory
1142    ///
1143    /// Create implies `write` permissions
1144    pub fn create(&mut self, toggle: bool) -> &mut Self {
1145        self.create = toggle;
1146        if toggle {
1147            self.write = true;
1148        }
1149
1150        self
1151    }
1152
1153    pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
1154        // ensure at least one is set
1155        if !(self.read || self.write || self.create) {
1156            return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
1157        }
1158
1159        if self.path.is_none() {
1160            return Err(WasiStateCreationError::PreopenedDirectoryError(
1161                "Preopened directories must point to a host directory".to_string(),
1162            ));
1163        }
1164        let path = self.path.clone().unwrap();
1165
1166        /*
1167        if !path.exists() {
1168            return Err(WasiStateCreationError::PreopenedDirectoryNotFound(path));
1169        }
1170        */
1171
1172        if let Some(alias) = &self.alias {
1173            validate_mapped_dir_alias(alias)?;
1174        }
1175
1176        Ok(PreopenedDir {
1177            path,
1178            alias: self.alias.clone(),
1179            read: self.read,
1180            write: self.write,
1181            create: self.create,
1182        })
1183    }
1184}
1185
1186#[cfg(test)]
1187mod test {
1188    use super::*;
1189
1190    #[test]
1191    fn env_var_errors() {
1192        #[cfg(not(target_arch = "wasm32"))]
1193        let runtime = tokio::runtime::Builder::new_multi_thread()
1194            .enable_all()
1195            .build()
1196            .unwrap();
1197        #[cfg(not(target_arch = "wasm32"))]
1198        let handle = runtime.handle().clone();
1199        #[cfg(not(target_arch = "wasm32"))]
1200        let _guard = handle.enter();
1201
1202        // `=` in the key is invalid.
1203        assert!(
1204            WasiEnv::builder("test_prog")
1205                .env("HOM=E", "/home/home")
1206                .build_init()
1207                .is_err(),
1208            "equal sign in key must be invalid"
1209        );
1210
1211        // `\0` in the key is invalid.
1212        assert!(
1213            WasiEnvBuilder::new("test_prog")
1214                .env("HOME\0", "/home/home")
1215                .build_init()
1216                .is_err(),
1217            "nul in key must be invalid"
1218        );
1219
1220        // `\0` in the value is invalid.
1221        assert!(
1222            WasiEnvBuilder::new("test_prog")
1223                .env("HOME", "/home/home\0")
1224                .build_init()
1225                .is_err(),
1226            "nul in value must be invalid"
1227        );
1228
1229        // `=` in the value is valid.
1230        assert!(
1231            WasiEnvBuilder::new("test_prog")
1232                .env("HOME", "/home/home=home")
1233                .engine(Engine::default())
1234                .build_init()
1235                .is_ok(),
1236            "equal sign in the value must be valid"
1237        );
1238    }
1239
1240    #[test]
1241    fn nul_character_in_args() {
1242        let output = WasiEnvBuilder::new("test_prog")
1243            .arg("--h\0elp")
1244            .build_init();
1245        let err = output.expect_err("should fail");
1246        assert!(matches!(
1247            err,
1248            WasiStateCreationError::ArgumentContainsNulByte(_)
1249        ));
1250
1251        let output = WasiEnvBuilder::new("test_prog")
1252            .args(["--help", "--wat\0"])
1253            .build_init();
1254        let err = output.expect_err("should fail");
1255        assert!(matches!(
1256            err,
1257            WasiStateCreationError::ArgumentContainsNulByte(_)
1258        ));
1259    }
1260}