1use crate::state::{default_fs_backing, WasiFs, WasiState};
4use crate::syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO};
5use crate::{WasiEnv, WasiFunctionEnv, WasiInodes};
6use generational_arena::Arena;
7use std::collections::HashMap;
8use std::ops::{Deref, DerefMut};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::sync::RwLock;
12use thiserror::Error;
13use wasmer::AsStoreMut;
14use wasmer_vfs::{FsError, VirtualFile};
15
16pub(crate) fn create_wasi_state(program_name: &str) -> WasiStateBuilder {
20 WasiStateBuilder {
21 args: vec![program_name.bytes().collect()],
22 ..WasiStateBuilder::default()
23 }
24}
25
26#[derive(Default)]
43pub struct WasiStateBuilder {
44 args: Vec<Vec<u8>>,
45 envs: Vec<(Vec<u8>, Vec<u8>)>,
46 preopens: Vec<PreopenedDir>,
47 vfs_preopens: Vec<String>,
48 #[allow(clippy::type_complexity)]
49 setup_fs_fn: Option<Box<dyn Fn(&mut WasiInodes, &mut WasiFs) -> Result<(), String> + Send>>,
50 stdout_override: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
51 stderr_override: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
52 stdin_override: Option<Box<dyn VirtualFile + Send + Sync + 'static>>,
53 fs_override: Option<Box<dyn wasmer_vfs::FileSystem>>,
54 runtime_override: Option<Arc<dyn crate::WasiRuntimeImplementation + Send + Sync + 'static>>,
55}
56
57impl std::fmt::Debug for WasiStateBuilder {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("WasiStateBuilder")
61 .field("args", &self.args)
62 .field("envs", &self.envs)
63 .field("preopens", &self.preopens)
64 .field("setup_fs_fn exists", &self.setup_fs_fn.is_some())
65 .field("stdout_override exists", &self.stdout_override.is_some())
66 .field("stderr_override exists", &self.stderr_override.is_some())
67 .field("stdin_override exists", &self.stdin_override.is_some())
68 .field("runtime_override_exists", &self.runtime_override.is_some())
69 .finish()
70 }
71}
72
73#[derive(Error, Debug, PartialEq, Eq)]
75pub enum WasiStateCreationError {
76 #[error("bad environment variable format: `{0}`")]
77 EnvironmentVariableFormatError(String),
78 #[error("argument contains null byte: `{0}`")]
79 ArgumentContainsNulByte(String),
80 #[error("preopened directory not found: `{0}`")]
81 PreopenedDirectoryNotFound(PathBuf),
82 #[error("preopened directory error: `{0}`")]
83 PreopenedDirectoryError(String),
84 #[error("mapped dir alias has wrong format: `{0}`")]
85 MappedDirAliasFormattingError(String),
86 #[error("wasi filesystem creation error: `{0}`")]
87 WasiFsCreationError(String),
88 #[error("wasi filesystem setup error: `{0}`")]
89 WasiFsSetupError(String),
90 #[error(transparent)]
91 FileSystemError(FsError),
92}
93
94fn validate_mapped_dir_alias(alias: &str) -> Result<(), WasiStateCreationError> {
95 if !alias.bytes().all(|b| b != b'\0') {
96 return Err(WasiStateCreationError::MappedDirAliasFormattingError(
97 format!("Alias \"{}\" contains a nul byte", alias),
98 ));
99 }
100
101 Ok(())
102}
103
104pub type SetupFsFn = Box<dyn Fn(&mut WasiInodes, &mut WasiFs) -> Result<(), String> + Send>;
105
106impl WasiStateBuilder {
109 pub fn env<Key, Value>(&mut self, key: Key, value: Value) -> &mut Self
115 where
116 Key: AsRef<[u8]>,
117 Value: AsRef<[u8]>,
118 {
119 self.envs
120 .push((key.as_ref().to_vec(), value.as_ref().to_vec()));
121
122 self
123 }
124
125 pub fn arg<Arg>(&mut self, arg: Arg) -> &mut Self
129 where
130 Arg: AsRef<[u8]>,
131 {
132 self.args.push(arg.as_ref().to_vec());
133
134 self
135 }
136
137 pub fn envs<I, Key, Value>(&mut self, env_pairs: I) -> &mut Self
143 where
144 I: IntoIterator<Item = (Key, Value)>,
145 Key: AsRef<[u8]>,
146 Value: AsRef<[u8]>,
147 {
148 env_pairs.into_iter().for_each(|(key, value)| {
149 self.env(key, value);
150 });
151
152 self
153 }
154
155 pub fn args<I, Arg>(&mut self, args: I) -> &mut Self
159 where
160 I: IntoIterator<Item = Arg>,
161 Arg: AsRef<[u8]>,
162 {
163 args.into_iter().for_each(|arg| {
164 self.arg(arg);
165 });
166
167 self
168 }
169
170 pub fn preopen_dir<FilePath>(
175 &mut self,
176 po_dir: FilePath,
177 ) -> Result<&mut Self, WasiStateCreationError>
178 where
179 FilePath: AsRef<Path>,
180 {
181 let mut pdb = PreopenDirBuilder::new();
182 let path = po_dir.as_ref();
183 pdb.directory(path).read(true).write(true).create(true);
184 let preopen = pdb.build()?;
185
186 self.preopens.push(preopen);
187
188 Ok(self)
189 }
190
191 pub fn preopen<F>(&mut self, inner: F) -> Result<&mut Self, WasiStateCreationError>
206 where
207 F: Fn(&mut PreopenDirBuilder) -> &mut PreopenDirBuilder,
208 {
209 let mut pdb = PreopenDirBuilder::new();
210 let po_dir = inner(&mut pdb).build()?;
211
212 self.preopens.push(po_dir);
213
214 Ok(self)
215 }
216
217 pub fn preopen_dirs<I, FilePath>(
222 &mut self,
223 po_dirs: I,
224 ) -> Result<&mut Self, WasiStateCreationError>
225 where
226 I: IntoIterator<Item = FilePath>,
227 FilePath: AsRef<Path>,
228 {
229 for po_dir in po_dirs {
230 self.preopen_dir(po_dir)?;
231 }
232
233 Ok(self)
234 }
235
236 pub fn preopen_vfs_dirs<I>(&mut self, po_dirs: I) -> Result<&mut Self, WasiStateCreationError>
239 where
240 I: IntoIterator<Item = String>,
241 {
242 for po_dir in po_dirs {
243 self.vfs_preopens.push(po_dir);
244 }
245
246 Ok(self)
247 }
248
249 pub fn map_dir<FilePath>(
251 &mut self,
252 alias: &str,
253 po_dir: FilePath,
254 ) -> Result<&mut Self, WasiStateCreationError>
255 where
256 FilePath: AsRef<Path>,
257 {
258 let mut pdb = PreopenDirBuilder::new();
259 let path = po_dir.as_ref();
260 pdb.directory(path)
261 .alias(alias)
262 .read(true)
263 .write(true)
264 .create(true);
265 let preopen = pdb.build()?;
266
267 self.preopens.push(preopen);
268
269 Ok(self)
270 }
271
272 pub fn map_dirs<I, FilePath>(
274 &mut self,
275 mapped_dirs: I,
276 ) -> Result<&mut Self, WasiStateCreationError>
277 where
278 I: IntoIterator<Item = (String, FilePath)>,
279 FilePath: AsRef<Path>,
280 {
281 for (alias, dir) in mapped_dirs {
282 self.map_dir(&alias, dir)?;
283 }
284
285 Ok(self)
286 }
287
288 pub fn stdout(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> &mut Self {
291 self.stdout_override = Some(new_file);
292
293 self
294 }
295
296 pub fn stderr(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> &mut Self {
299 self.stderr_override = Some(new_file);
300
301 self
302 }
303
304 pub fn stdin(&mut self, new_file: Box<dyn VirtualFile + Send + Sync + 'static>) -> &mut Self {
307 self.stdin_override = Some(new_file);
308
309 self
310 }
311
312 pub fn set_fs(&mut self, fs: Box<dyn wasmer_vfs::FileSystem>) -> &mut Self {
316 self.fs_override = Some(fs);
317
318 self
319 }
320
321 pub fn setup_fs(&mut self, setup_fs_fn: SetupFsFn) -> &mut Self {
324 self.setup_fs_fn = Some(setup_fs_fn);
325
326 self
327 }
328
329 pub fn runtime<R>(&mut self, runtime: R) -> &mut Self
332 where
333 R: crate::WasiRuntimeImplementation + Send + Sync + 'static,
334 {
335 self.runtime_override = Some(Arc::new(runtime));
336 self
337 }
338
339 pub fn build(&mut self) -> Result<WasiState, WasiStateCreationError> {
359 for (i, arg) in self.args.iter().enumerate() {
360 for b in arg.iter() {
361 if *b == 0 {
362 return Err(WasiStateCreationError::ArgumentContainsNulByte(
363 std::str::from_utf8(arg)
364 .unwrap_or(if i == 0 {
365 "Inner error: program name is invalid utf8!"
366 } else {
367 "Inner error: arg is invalid utf8!"
368 })
369 .to_string(),
370 ));
371 }
372 }
373 }
374
375 enum InvalidCharacter {
376 Nul,
377 Equal,
378 }
379
380 for (env_key, env_value) in self.envs.iter() {
381 match env_key.iter().find_map(|&ch| {
382 if ch == 0 {
383 Some(InvalidCharacter::Nul)
384 } else if ch == b'=' {
385 Some(InvalidCharacter::Equal)
386 } else {
387 None
388 }
389 }) {
390 Some(InvalidCharacter::Nul) => {
391 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
392 format!(
393 "found nul byte in env var key \"{}\" (key=value)",
394 String::from_utf8_lossy(env_key)
395 ),
396 ))
397 }
398
399 Some(InvalidCharacter::Equal) => {
400 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
401 format!(
402 "found equal sign in env var key \"{}\" (key=value)",
403 String::from_utf8_lossy(env_key)
404 ),
405 ))
406 }
407
408 None => (),
409 }
410
411 if env_value.iter().any(|&ch| ch == 0) {
412 return Err(WasiStateCreationError::EnvironmentVariableFormatError(
413 format!(
414 "found nul byte in env var value \"{}\" (key=value)",
415 String::from_utf8_lossy(env_value)
416 ),
417 ));
418 }
419 }
420
421 let fs_backing = self.fs_override.take().unwrap_or_else(default_fs_backing);
422
423 let inodes = RwLock::new(crate::state::WasiInodes {
425 arena: Arena::new(),
426 orphan_fds: HashMap::new(),
427 });
428 let wasi_fs = {
429 let mut inodes = inodes.write().unwrap();
430
431 let mut wasi_fs = WasiFs::new_with_preopen(
433 inodes.deref_mut(),
434 &self.preopens,
435 &self.vfs_preopens,
436 fs_backing,
437 )
438 .map_err(WasiStateCreationError::WasiFsCreationError)?;
439
440 if let Some(stdin_override) = self.stdin_override.take() {
442 wasi_fs
443 .swap_file(inodes.deref(), __WASI_STDIN_FILENO, stdin_override)
444 .map_err(WasiStateCreationError::FileSystemError)?;
445 }
446
447 if let Some(stdout_override) = self.stdout_override.take() {
448 wasi_fs
449 .swap_file(inodes.deref(), __WASI_STDOUT_FILENO, stdout_override)
450 .map_err(WasiStateCreationError::FileSystemError)?;
451 }
452
453 if let Some(stderr_override) = self.stderr_override.take() {
454 wasi_fs
455 .swap_file(inodes.deref(), __WASI_STDERR_FILENO, stderr_override)
456 .map_err(WasiStateCreationError::FileSystemError)?;
457 }
458
459 if let Some(f) = &self.setup_fs_fn {
460 f(inodes.deref_mut(), &mut wasi_fs)
461 .map_err(WasiStateCreationError::WasiFsSetupError)?;
462 }
463 wasi_fs
464 };
465
466 Ok(WasiState {
467 fs: wasi_fs,
468 inodes: Arc::new(inodes),
469 args: self.args.clone(),
470 threading: Default::default(),
471 envs: self
472 .envs
473 .iter()
474 .map(|(key, value)| {
475 let mut env = Vec::with_capacity(key.len() + value.len() + 1);
476 env.extend_from_slice(key);
477 env.push(b'=');
478 env.extend_from_slice(value);
479
480 env
481 })
482 .collect(),
483 })
484 }
485
486 pub fn finalize(
497 &mut self,
498 store: &mut impl AsStoreMut,
499 ) -> Result<WasiFunctionEnv, WasiStateCreationError> {
500 let state = self.build()?;
501
502 let mut env = WasiEnv::new(state);
503 if let Some(runtime) = self.runtime_override.as_ref() {
504 env.runtime = runtime.clone();
505 }
506 Ok(WasiFunctionEnv::new(store, env))
507 }
508}
509
510#[derive(Debug, Default)]
512pub struct PreopenDirBuilder {
513 path: Option<PathBuf>,
514 alias: Option<String>,
515 read: bool,
516 write: bool,
517 create: bool,
518}
519
520#[derive(Debug, Default)]
522pub(crate) struct PreopenedDir {
523 pub(crate) path: PathBuf,
524 pub(crate) alias: Option<String>,
525 pub(crate) read: bool,
526 pub(crate) write: bool,
527 pub(crate) create: bool,
528}
529
530impl PreopenDirBuilder {
531 pub(crate) fn new() -> Self {
533 PreopenDirBuilder::default()
534 }
535
536 pub fn directory<FilePath>(&mut self, po_dir: FilePath) -> &mut Self
538 where
539 FilePath: AsRef<Path>,
540 {
541 let path = po_dir.as_ref();
542 self.path = Some(path.to_path_buf());
543
544 self
545 }
546
547 pub fn alias(&mut self, alias: &str) -> &mut Self {
549 let alias = alias.trim_start_matches('/');
552 self.alias = Some(alias.to_string());
553
554 self
555 }
556
557 pub fn read(&mut self, toggle: bool) -> &mut Self {
559 self.read = toggle;
560
561 self
562 }
563
564 pub fn write(&mut self, toggle: bool) -> &mut Self {
566 self.write = toggle;
567
568 self
569 }
570
571 pub fn create(&mut self, toggle: bool) -> &mut Self {
575 self.create = toggle;
576 if toggle {
577 self.write = true;
578 }
579
580 self
581 }
582
583 pub(crate) fn build(&self) -> Result<PreopenedDir, WasiStateCreationError> {
584 if !(self.read || self.write || self.create) {
586 return Err(WasiStateCreationError::PreopenedDirectoryError("Preopened directories must have at least one of read, write, create permissions set".to_string()));
587 }
588
589 if self.path.is_none() {
590 return Err(WasiStateCreationError::PreopenedDirectoryError(
591 "Preopened directories must point to a host directory".to_string(),
592 ));
593 }
594 let path = self.path.clone().unwrap();
595
596 if let Some(alias) = &self.alias {
603 validate_mapped_dir_alias(alias)?;
604 }
605
606 Ok(PreopenedDir {
607 path,
608 alias: self.alias.clone(),
609 read: self.read,
610 write: self.write,
611 create: self.create,
612 })
613 }
614}
615
616#[cfg(test)]
617mod test {
618 use super::*;
619
620 #[test]
621 fn env_var_errors() {
622 assert!(
624 create_wasi_state("test_prog")
625 .env("HOM=E", "/home/home")
626 .build()
627 .is_err(),
628 "equal sign in key must be invalid"
629 );
630
631 assert!(
633 create_wasi_state("test_prog")
634 .env("HOME\0", "/home/home")
635 .build()
636 .is_err(),
637 "nul in key must be invalid"
638 );
639
640 assert!(
642 create_wasi_state("test_prog")
643 .env("HOME", "/home/home=home")
644 .build()
645 .is_ok(),
646 "equal sign in the value must be valid"
647 );
648
649 assert!(
651 create_wasi_state("test_prog")
652 .env("HOME", "/home/home\0")
653 .build()
654 .is_err(),
655 "nul in value must be invalid"
656 );
657 }
658
659 #[test]
660 fn nul_character_in_args() {
661 let output = create_wasi_state("test_prog").arg("--h\0elp").build();
662 match output {
663 Err(WasiStateCreationError::ArgumentContainsNulByte(_)) => assert!(true),
664 _ => assert!(false),
665 }
666 let output = create_wasi_state("test_prog")
667 .args(&["--help", "--wat\0"])
668 .build();
669 match output {
670 Err(WasiStateCreationError::ArgumentContainsNulByte(_)) => assert!(true),
671 _ => assert!(false),
672 }
673 }
674}