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