Skip to main content

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