wasmer_wasix/state/
builder.rs

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