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