use crate::fdentry::FdEntry; use crate::{wasi, Error, Result}; use std::borrow::Borrow; use std::collections::HashMap; use std::env; use std::ffi::{CString, OsString}; use std::fs::File; use std::path::{Path, PathBuf}; enum PendingFdEntry { Thunk(fn() -> Result), File(File), } impl std::fmt::Debug for PendingFdEntry { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Thunk(f) => write!( fmt, "PendingFdEntry::Thunk({:p})", f as *const fn() -> Result ), Self::File(f) => write!(fmt, "PendingFdEntry::File({:?})", f), } } } #[derive(Debug, Eq, Hash, PartialEq)] enum PendingCString { Bytes(Vec), OsString(OsString), } impl From> for PendingCString { fn from(bytes: Vec) -> Self { Self::Bytes(bytes) } } impl From for PendingCString { fn from(s: OsString) -> Self { Self::OsString(s) } } impl PendingCString { fn into_string(self) -> Result { match self { Self::Bytes(v) => String::from_utf8(v).map_err(|_| Error::EILSEQ), Self::OsString(s) => s.into_string().map_err(|_| Error::EILSEQ), } } /// Create a `CString` containing valid UTF-8, or fail with `Error::EILSEQ`. fn into_utf8_cstring(self) -> Result { self.into_string() .and_then(|s| CString::new(s).map_err(|_| Error::EILSEQ)) } } /// A builder allowing customizable construction of `WasiCtx` instances. pub struct WasiCtxBuilder { fds: HashMap, preopens: Vec<(PathBuf, File)>, args: Vec, env: HashMap, } impl WasiCtxBuilder { /// Builder for a new `WasiCtx`. pub fn new() -> Self { let mut builder = Self { fds: HashMap::new(), preopens: Vec::new(), args: vec![], env: HashMap::new(), }; builder.fds.insert(0, PendingFdEntry::Thunk(FdEntry::null)); builder.fds.insert(1, PendingFdEntry::Thunk(FdEntry::null)); builder.fds.insert(2, PendingFdEntry::Thunk(FdEntry::null)); builder } /// Add arguments to the command-line arguments list. /// /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail /// with `Error::EILSEQ`. pub fn args>(mut self, args: impl IntoIterator) -> Self { self.args = args .into_iter() .map(|arg| arg.as_ref().to_vec().into()) .collect(); self } /// Add an argument to the command-line arguments list. /// /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail /// with `Error::EILSEQ`. pub fn arg>(mut self, arg: S) -> Self { self.args.push(arg.as_ref().to_vec().into()); self } /// Inherit the command-line arguments from the host process. /// /// If any arguments from the host process contain invalid UTF-8, `WasiCtxBuilder::build()` will /// fail with `Error::EILSEQ`. pub fn inherit_args(mut self) -> Self { self.args = env::args_os().map(PendingCString::OsString).collect(); self } /// Inherit stdin from the host process. pub fn inherit_stdin(mut self) -> Self { self.fds .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); self } /// Inherit stdout from the host process. pub fn inherit_stdout(mut self) -> Self { self.fds .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); self } /// Inherit stdout from the host process. pub fn inherit_stderr(mut self) -> Self { self.fds .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); self } /// Inherit the stdin, stdout, and stderr streams from the host process. pub fn inherit_stdio(mut self) -> Self { self.fds .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); self.fds .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); self.fds .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); self } /// Inherit the environment variables from the host process. /// /// If any environment variables from the host process contain invalid Unicode (UTF-16 for /// Windows, UTF-8 for other platforms), `WasiCtxBuilder::build()` will fail with /// `Error::EILSEQ`. pub fn inherit_env(mut self) -> Self { self.env = std::env::vars_os() .map(|(k, v)| (k.into(), v.into())) .collect(); self } /// Add an entry to the environment. /// /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. pub fn env>(mut self, k: S, v: S) -> Self { self.env .insert(k.as_ref().to_vec().into(), v.as_ref().to_vec().into()); self } /// Add entries to the environment. /// /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. pub fn envs, T: Borrow<(S, S)>>( mut self, envs: impl IntoIterator, ) -> Self { self.env = envs .into_iter() .map(|t| { let (k, v) = t.borrow(); (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) }) .collect(); self } /// Provide a File to use as stdin pub fn stdin(mut self, file: File) -> Self { self.fds.insert(0, PendingFdEntry::File(file)); self } /// Provide a File to use as stdout pub fn stdout(mut self, file: File) -> Self { self.fds.insert(1, PendingFdEntry::File(file)); self } /// Provide a File to use as stderr pub fn stderr(mut self, file: File) -> Self { self.fds.insert(2, PendingFdEntry::File(file)); self } /// Add a preopened directory. pub fn preopened_dir>(mut self, dir: File, guest_path: P) -> Self { self.preopens.push((guest_path.as_ref().to_owned(), dir)); self } /// Build a `WasiCtx`, consuming this `WasiCtxBuilder`. /// /// If any of the arguments or environment variables in this builder cannot be converted into /// `CString`s, either due to NUL bytes or Unicode conversions, this returns `Error::EILSEQ`. pub fn build(self) -> Result { // Process arguments and environment variables into `CString`s, failing quickly if they // contain any NUL bytes, or if conversion from `OsString` fails. let args = self .args .into_iter() .map(|arg| arg.into_utf8_cstring()) .collect::>>()?; let env = self .env .into_iter() .map(|(k, v)| { k.into_string().and_then(|mut pair| { v.into_string().and_then(|v| { pair.push('='); pair.push_str(v.as_str()); // We have valid UTF-8, but the keys and values have not yet been checked // for NULs, so we do a final check here. CString::new(pair).map_err(|_| Error::EILSEQ) }) }) }) .collect::>>()?; let mut fds: HashMap = HashMap::new(); // Populate the non-preopen fds. for (fd, pending) in self.fds { log::debug!("WasiCtx inserting ({:?}, {:?})", fd, pending); match pending { PendingFdEntry::Thunk(f) => { fds.insert(fd, f()?); } PendingFdEntry::File(f) => { fds.insert(fd, FdEntry::from(f)?); } } } // Then add the preopen fds. Startup code in the guest starts looking at fd 3 for preopens, // so we start from there. This variable is initially 2, though, because the loop // immediately does the increment and check for overflow. let mut preopen_fd: wasi::__wasi_fd_t = 2; for (guest_path, dir) in self.preopens { // We do the increment at the beginning of the loop body, so that we don't overflow // unnecessarily if we have exactly the maximum number of file descriptors. preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; if !dir.metadata()?.is_dir() { return Err(Error::EBADF); } // We don't currently allow setting file descriptors other than 0-2, but this will avoid // collisions if we restore that functionality in the future. while fds.contains_key(&preopen_fd) { preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; } let mut fe = FdEntry::from(dir)?; fe.preopen_path = Some(guest_path); log::debug!("WasiCtx inserting ({:?}, {:?})", preopen_fd, fe); fds.insert(preopen_fd, fe); log::debug!("WasiCtx fds = {:?}", fds); } Ok(WasiCtx { args, env, fds }) } } #[derive(Debug)] pub struct WasiCtx { fds: HashMap, pub(crate) args: Vec, pub(crate) env: Vec, } impl WasiCtx { /// Make a new `WasiCtx` with some default settings. /// /// - File descriptors 0, 1, and 2 inherit stdin, stdout, and stderr from the host process. /// /// - Environment variables are inherited from the host process. /// /// To override these behaviors, use `WasiCtxBuilder`. pub fn new>(args: impl IntoIterator) -> Result { WasiCtxBuilder::new() .args(args) .inherit_stdio() .inherit_env() .build() } /// Check if `WasiCtx` contains the specified raw WASI `fd`. pub(crate) unsafe fn contains_fd_entry(&self, fd: wasi::__wasi_fd_t) -> bool { self.fds.contains_key(&fd) } /// Get an immutable `FdEntry` corresponding to the specified raw WASI `fd`. pub(crate) unsafe fn get_fd_entry(&self, fd: wasi::__wasi_fd_t) -> Result<&FdEntry> { self.fds.get(&fd).ok_or(Error::EBADF) } /// Get a mutable `FdEntry` corresponding to the specified raw WASI `fd`. pub(crate) unsafe fn get_fd_entry_mut( &mut self, fd: wasi::__wasi_fd_t, ) -> Result<&mut FdEntry> { self.fds.get_mut(&fd).ok_or(Error::EBADF) } /// Insert the specified `FdEntry` into the `WasiCtx` object. /// /// The `FdEntry` will automatically get another free raw WASI `fd` assigned. Note that /// the two subsequent free raw WASI `fd`s do not have to be stored contiguously. pub(crate) fn insert_fd_entry(&mut self, fe: FdEntry) -> Result { // Never insert where stdio handles are expected to be. let mut fd = 3; while self.fds.contains_key(&fd) { if let Some(next_fd) = fd.checked_add(1) { fd = next_fd; } else { return Err(Error::EMFILE); } } self.fds.insert(fd, fe); Ok(fd) } /// Insert the specified `FdEntry` with the specified raw WASI `fd` key into the `WasiCtx` /// object. pub(crate) fn insert_fd_entry_at( &mut self, fd: wasi::__wasi_fd_t, fe: FdEntry, ) -> Option { self.fds.insert(fd, fe) } /// Remove `FdEntry` corresponding to the specified raw WASI `fd` from the `WasiCtx` object. pub(crate) fn remove_fd_entry(&mut self, fd: wasi::__wasi_fd_t) -> Result { self.fds.remove(&fd).ok_or(Error::EBADF) } }