diff --git a/Cargo.lock b/Cargo.lock index 0e204ed..120ef87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,6 +732,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "do-everything" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand", + "wit-bindgen", +] + [[package]] name = "either" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index fe87d7f..7a8e9c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ name = "wasi-virt" exclude = ["testapp"] members = [ "virtual-adapter", + "tests/components/do-everything", "tests/components/file-read", "tests/components/get-env" ] diff --git a/README.md b/README.md index ca7859c..1b561e3 100644 --- a/README.md +++ b/README.md @@ -14,35 +14,31 @@ The virtualized component can be composed into a WASI Preview2 component with `wasm-tools compose`, providing fully-configurable WASI virtualization with host pass through or full encapsulation as needed. -Subsystem support: +Supports all of the current WASI subsystems: -- [x] Environment virtualization -- [x] Filesystem virtualization -- [ ] Stdio +- [x] Environment: Set environment variables, configure host environment variable permissions +- [x] Filesystem: Mount a read-only filesystem, configure host filesystem pass-through +- [x] Stdio: Currently only supports disabling - [ ] Sockets - [ ] Clocks -- [ ] [Your suggestion here](https://github.com/bytecodealliance/WASI-Virt/issues/new) +- [x] Exit +- [ ] Random While current virtualization support is limited, the goal for this project is to support a wide range of WASI virtualization use cases. +Have an unhandled use case? Post a [virtualization suggestion](https://github.com/bytecodealliance/WASI-Virt/issues/new). + ## Explainer -When wanting to run WebAssembly Components depending on WASI APIs in other environments it can provide -a point of friction having to port WASI interop to every target platform. +When wanting to run WebAssembly Components depending on WASI APIs in other environments it can provide a point of friction having to port WASI interop to every target platform. In addition having full unrestricted access to core operating system APIs is a security concern. -WASI Virt allows taking a component that depends on WASI APIs and using a virtualized adapter to convert -it into a component that no longer depends on those WASI APIs, or conditionally only depends on them in -a configurable way. +WASI Virt allows taking a component that depends on WASI APIs and using a virtualized adapter to convert it into a component that no longer depends on those WASI APIs, or conditionally only depends on them in a configurable way. -For example, consider converting an application to a WebAssembly Component that assumes it can load -read some files from the filesystem, but never needs to write. +For example, consider converting an application to a WebAssembly Component that assumes it can load read some files from the filesystem, but never needs to write. -Using WASI Virt, those specific file paths can be mounted and virtualized into the component itself as -a post-compile operation, while banning the final component from being able to access the host's filesystem at -all. The inner program still imports a wasi filesystem, but the filesystem implementation is provided by another component, rather than in the host environment. The composition of these two components no longer has a -filesystem import, so it can be run in hosts (or other components) which do not provide a filesystem API. +Using WASI Virt, those specific file paths can be mounted and virtualized into the component itself as a post-compile operation, while banning the final component from being able to access the host's filesystem at all. The inner program still imports a wasi filesystem, but the filesystem implementation is provided by another component, rather than in the host environment. The composition of these two components no longer has a filesystem import, so it can be run in hosts (or other components) which do not provide a filesystem API. ## Basic Usage @@ -50,6 +46,14 @@ filesystem import, so it can be run in hosts (or other components) which do not cargo install wasi-virt ``` +By default, all virtualizations encapsulate the host virtualization, unless explicitly enabling host passthrough via `--allow-env` or `--preopen`. + +In all of the following examples, the `component.wasm` argument is optional. If omitted, then the virtualized adapter is output into `virt.wasm`, which can be composed into any component with: + +``` +wasm-tools compose component.wasm -d virt.wasm -o component.virt.wasm +``` + ### Env Virtualization ``` @@ -80,24 +84,36 @@ wasi-virt component.wasm --preopen /=/restricted/path -o virt.wasm wasi-virt component.wasm --mount /virt-dir=./local --preopen /host-dir=/host/path -o virt.wasm ``` -By default, all virtualizations encapsulate the host virtualization, unless explicitly enabling -host passthrough via `--allow-env` or `--preopen`. - -In all of the above examples, the `component.wasm` argument is optional. If omitted, then the virtualized -adapter is output into `virt.wasm`, which can be composed into any component with: +### Stdio Virtualization ``` -wasm-tools compose component.wasm -d virt.wasm -o component.virt.wasm +# Ignore all stdio entirely +wasi-virt --allow-stdio + +# Throw an error if attempting any stdio +# (this is the default) +wasi-virt --deny-stdio + +# Provide stdin as an inline string +wasi-virt --stdin=test + +# Provide stdio ``` ## API +When using the virtualization API, subsystems are passthrough by default instead of deny by default. + ```rs use std::fs; use wasi_virt::{WasiVirt, FsEntry}; fn main() { - let virt = WasiVirt::new(); + let mut virt = WasiVirt::new_reactor(); + + // ignore stdio + virt.stdio().ignore(); + virt.env() // provide an allow list of host env vars .allow(&["PUBLIC_ENV_VAR"]) @@ -124,8 +140,19 @@ fn main() { } ``` -When calling a subsystem for the first time, its virtualization will be enabled. Subsystems -not used or configured at all will be omitted from the virtualization entirely. +When calling a subsystem for the first time, its virtualization will be enabled. Subsystems not used or configured at all will be omitted from the virtualization entirely. + +## Contributing + +To build, run `./build-adapter.sh` which builds the master `virtual-adapter` component, followed by `cargo build` to build +the virtualization tooling (located in `src`). + +Tests can be run with `cargo test`, which runs the tests in `tests/virt.rs`. + +Test components are built from the `tests/components` directory, and run against the configurations provided in `tests/cases`. + +To update WASI, `lib/wasi_snapshot_preview1.reactor.wasm` needs +to be updated to the latest Wasmtime build, and the `wit/deps` folder needs to be updated with the latest WASI definitions. # License @@ -136,4 +163,4 @@ See [LICENSE](LICENSE) for more details. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, -shall be licensed as above, without any additional terms or conditions. +shall be licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/lib/virtual_adapter.wasm b/lib/virtual_adapter.wasm index 781ad34..f781f41 100755 Binary files a/lib/virtual_adapter.wasm and b/lib/virtual_adapter.wasm differ diff --git a/src/bin/wasi-virt.rs b/src/bin/wasi-virt.rs index 8b091cb..25c7025 100644 --- a/src/bin/wasi-virt.rs +++ b/src/bin/wasi-virt.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use clap::Parser; use std::{env, error::Error, fs, path::PathBuf, time::SystemTime}; -use wasi_virt::WasiVirt; +use wasi_virt::{VirtExit, WasiVirt}; use wasm_compose::composer::ComponentComposer; #[derive(Parser, Debug)] @@ -21,6 +21,10 @@ struct Args { #[arg(short, long, verbatim_doc_comment)] config: Option, + /// Allow the component to exit + #[arg(long)] + allow_exit: Option, + // ENV /// Allow host access to all environment variables, or to a specific comma-separated list of variable names. #[arg(long, num_args(0..), use_value_delimiter(true), require_equals(true), value_name("ENV_VAR"))] @@ -79,6 +83,13 @@ fn main() -> Result<()> { // By default, we virtualize all subsystems // This ensures full encapsulation in the default (no argument) case + // exit + virt_opts.exit(if args.allow_exit.unwrap_or_default() { + VirtExit::Passthrough + } else { + Default::default() + }); + // env options let env = virt_opts.env(); match args.allow_env { diff --git a/src/lib.rs b/src/lib.rs index 1f7c528..9ddb120 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,13 @@ use std::env; use std::fs; use std::time::SystemTime; use virt_env::{create_env_virt, strip_env_virt}; -use virt_fs::{create_fs_virt, strip_fs_virt}; +use virt_io::create_io_virt; +use virt_io::stub_io_virt; +use virt_io::VirtStdio; +use walrus::Module; +use walrus::ValType; +use walrus_ops::add_stub_exported_func; +use walrus_ops::remove_exported_func; use wasm_metadata::Producers; use wasm_opt::Feature; use wasm_opt::OptimizationOptions; @@ -14,11 +20,19 @@ use wit_component::StringEncoding; mod data; mod virt_env; -mod virt_fs; +mod virt_io; mod walrus_ops; pub use virt_env::{HostEnv, VirtEnv}; -pub use virt_fs::{FsEntry, VirtFs, VirtualFiles}; +pub use virt_io::{FsEntry, VirtFs, VirtualFiles}; + +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum VirtExit { + #[default] + Unreachable, + Passthrough, +} /// Virtualization options /// @@ -36,6 +50,10 @@ pub struct WasiVirt { pub env: Option, /// Filesystem virtualization pub fs: Option, + /// Stdio virtualization + pub stdio: Option, + /// Exit virtualization + pub exit: Option, /// Disable wasm-opt run if desired pub wasm_opt: Option, } @@ -50,6 +68,10 @@ impl WasiVirt { Self::default() } + pub fn exit(&mut self, virt_exit: VirtExit) { + self.exit = Some(virt_exit); + } + pub fn env(&mut self) -> &mut VirtEnv { self.env.get_or_insert_with(Default::default) } @@ -58,25 +80,57 @@ impl WasiVirt { self.fs.get_or_insert_with(Default::default) } - pub fn finish(&self) -> Result { + pub fn stdio(&mut self) -> &mut VirtStdio { + self.stdio.get_or_insert_with(Default::default) + } + + pub fn opt(&mut self, opt: bool) { + self.wasm_opt = Some(opt); + } + + pub fn finish(&mut self) -> Result { let virt_adapter = include_bytes!("../lib/virtual_adapter.wasm"); let config = walrus::ModuleConfig::new(); let mut module = config.parse(virt_adapter)?; module.name = Some("wasi_virt".into()); + let mut has_io = false; + if let Some(env) = &self.env { create_env_virt(&mut module, env)?; } else { strip_env_virt(&mut module)?; } - let virtual_files = if let Some(fs) = &self.fs { - create_fs_virt(&mut module, fs)? + + let virtual_files = if self.fs.is_some() || self.stdio.is_some() { + has_io = true; + // pull in one io subsystem -> pull in all io subsystems + // (due to virtualization wrapping required for streams + poll) + self.fs(); + self.stdio(); + create_io_virt( + &mut module, + self.fs.as_ref().unwrap(), + self.stdio.as_ref().unwrap(), + )? } else { - strip_fs_virt(&mut module)?; Default::default() }; + if matches!(self.exit, Some(VirtExit::Unreachable)) { + add_stub_exported_func( + &mut module, + "wasi:cli-base/exit#exit", + vec![ValType::I32], + vec![], + )?; + } + + if !has_io { + strip_io_virt(&mut module)?; + } + // decode the component custom section to strip out the unused world exports // before reencoding. let mut component_section = module @@ -97,13 +151,13 @@ impl WasiVirt { .select_world(*pkg_id, Some("virtual-base"))?; let env_world = bindgen.resolve.select_world(*pkg_id, Some("virtual-env"))?; - let fs_world = bindgen.resolve.select_world(*pkg_id, Some("virtual-fs"))?; + let io_world = bindgen.resolve.select_world(*pkg_id, Some("virtual-io"))?; if self.env.is_some() { bindgen.resolve.merge_worlds(env_world, base_world)?; } - if self.fs.is_some() { - bindgen.resolve.merge_worlds(fs_world, base_world)?; + if has_io { + bindgen.resolve.merge_worlds(io_world, base_world)?; } let mut producers = Producers::default(); @@ -159,3 +213,72 @@ fn timestamp() -> u64 { Err(_) => panic!(), } } + +fn strip_io_virt(module: &mut Module) -> Result<()> { + stub_io_virt(module)?; + + remove_exported_func(module, "wasi:cli-base/preopens#get-directories")?; + + remove_exported_func(module, "wasi:filesystem/filesystem#read-via-stream")?; + remove_exported_func(module, "wasi:filesystem/filesystem#write-via-stream")?; + remove_exported_func(module, "wasi:filesystem/filesystem#append-via-stream")?; + remove_exported_func(module, "wasi:filesystem/filesystem#advise")?; + remove_exported_func(module, "wasi:filesystem/filesystem#sync-data")?; + remove_exported_func(module, "wasi:filesystem/filesystem#get-flags")?; + remove_exported_func(module, "wasi:filesystem/filesystem#get-type")?; + remove_exported_func(module, "wasi:filesystem/filesystem#set-size")?; + remove_exported_func(module, "wasi:filesystem/filesystem#set-times")?; + remove_exported_func(module, "wasi:filesystem/filesystem#read")?; + remove_exported_func(module, "wasi:filesystem/filesystem#write")?; + remove_exported_func(module, "wasi:filesystem/filesystem#read-directory")?; + remove_exported_func(module, "wasi:filesystem/filesystem#sync")?; + remove_exported_func(module, "wasi:filesystem/filesystem#create-directory-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#stat")?; + remove_exported_func(module, "wasi:filesystem/filesystem#stat-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#set-times-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#link-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#open-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#readlink-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#remove-directory-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#rename-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#symlink-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#access-at")?; + remove_exported_func(module, "wasi:filesystem/filesystem#unlink-file-at")?; + remove_exported_func( + module, + "wasi:filesystem/filesystem#change-file-permissions-at", + )?; + remove_exported_func( + module, + "wasi:filesystem/filesystem#change-directory-permissions-at", + )?; + remove_exported_func(module, "wasi:filesystem/filesystem#lock-shared")?; + remove_exported_func(module, "wasi:filesystem/filesystem#lock-exclusive")?; + remove_exported_func(module, "wasi:filesystem/filesystem#try-lock-shared")?; + remove_exported_func(module, "wasi:filesystem/filesystem#try-lock-exclusive")?; + remove_exported_func(module, "wasi:filesystem/filesystem#unlock")?; + remove_exported_func(module, "wasi:filesystem/filesystem#drop-descriptor")?; + remove_exported_func(module, "wasi:filesystem/filesystem#read-directory-entry")?; + remove_exported_func( + module, + "wasi:filesystem/filesystem#drop-directory-entry-stream", + )?; + + remove_exported_func(module, "wasi:io/streams#read")?; + remove_exported_func(module, "wasi:io/streams#blocking-read")?; + remove_exported_func(module, "wasi:io/streams#skip")?; + remove_exported_func(module, "wasi:io/streams#blocking-skip")?; + remove_exported_func(module, "wasi:io/streams#subscribe-to-input-stream")?; + remove_exported_func(module, "wasi:io/streams#drop-input-stream")?; + remove_exported_func(module, "wasi:io/streams#write")?; + remove_exported_func(module, "wasi:io/streams#blocking-write")?; + remove_exported_func(module, "wasi:io/streams#write-zeroes")?; + remove_exported_func(module, "wasi:io/streams#blocking-write-zeroes")?; + remove_exported_func(module, "wasi:io/streams#splice")?; + remove_exported_func(module, "wasi:io/streams#blocking-splice")?; + remove_exported_func(module, "wasi:io/streams#forward")?; + remove_exported_func(module, "wasi:io/streams#subscribe-to-output-stream")?; + remove_exported_func(module, "wasi:io/streams#drop-output-stream")?; + + Ok(()) +} diff --git a/src/virt_env.rs b/src/virt_env.rs index 365199d..16c98cb 100644 --- a/src/virt_env.rs +++ b/src/virt_env.rs @@ -65,7 +65,7 @@ impl VirtEnv { } } -pub fn create_env_virt<'a>(module: &'a mut Module, env: &VirtEnv) -> Result<()> { +pub(crate) fn create_env_virt<'a>(module: &'a mut Module, env: &VirtEnv) -> Result<()> { let env_ptr_addr = { let env_ptr_export = module .exports @@ -208,7 +208,7 @@ pub fn create_env_virt<'a>(module: &'a mut Module, env: &VirtEnv) -> Result<()> Ok(()) } -fn stub_env_virt(module: &mut Module) -> Result<()> { +pub(crate) fn stub_env_virt(module: &mut Module) -> Result<()> { stub_imported_func(module, "wasi:cli-base/environment", "get-arguments", true)?; stub_imported_func(module, "wasi:cli-base/environment", "get-environment", true)?; Ok(()) diff --git a/src/virt_fs.rs b/src/virt_io.rs similarity index 78% rename from src/virt_fs.rs rename to src/virt_io.rs index b2aa004..d347f3e 100644 --- a/src/virt_fs.rs +++ b/src/virt_io.rs @@ -7,17 +7,42 @@ use walrus::{ir::Value, ExportItem, GlobalKind, InitExpr, Module}; use crate::{ data::{Data, WasmEncode}, - walrus_ops::{ - get_active_data_segment, get_stack_global, remove_exported_func, stub_imported_func, - }, + walrus_ops::{get_active_data_segment, get_stack_global, stub_imported_func}, }; pub type VirtualFiles = BTreeMap; +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct VirtStdio { + pub stdin: bool, + pub stdout: bool, + pub stderr: bool, +} + +impl VirtStdio { + pub fn stdin(&mut self, allow: bool) -> &mut Self { + self.stdin = allow; + self + } + pub fn stdout(&mut self, allow: bool) -> &mut Self { + self.stdout = allow; + self + } + pub fn stderr(&mut self, allow: bool) -> &mut Self { + self.stderr = allow; + self + } +} + #[derive(Deserialize, Debug, Default, Clone)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct VirtFs { + /// Enable verbatim host preopens + #[serde(default)] + pub host_preopens: bool, /// Filesystem state to virtualize + #[serde(default)] pub preopens: BTreeMap, /// A cutoff size in bytes, above which /// files will be treated as passive segments. @@ -214,8 +239,36 @@ impl FsEntry { } } -pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result { +// io flags +const FLAGS_ENABLE_STDIN: u32 = 1; +const FLAGS_ENABLE_STDOUT: u32 = 2; +const FLAGS_ENABLE_STDERR: u32 = 4; +const FLAGS_HOST_PREOPENS: u32 = 8; +const FLAGS_HOST_PASSTHROUGH: u32 = 16; + +pub(crate) fn create_io_virt<'a>( + module: &'a mut Module, + fs: &VirtFs, + stdio: &VirtStdio, +) -> Result { let mut virtual_files = BTreeMap::new(); + let mut flags: u32 = 0; + + if fs.host_preopens { + flags |= FLAGS_HOST_PREOPENS; + } + if stdio.stdin { + flags |= FLAGS_ENABLE_STDIN; + } + if stdio.stdout { + flags |= FLAGS_ENABLE_STDOUT; + } + if stdio.stderr { + flags |= FLAGS_ENABLE_STDERR; + } + if !stdio.stdin && !stdio.stdout && !stdio.stderr { + stub_stdio_virt(module)?; + } // First we iterate the options and fill in all HostDir and HostFile entries // With inline directory and file entries @@ -260,7 +313,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result(module: &'a mut Module, fs: &VirtFs) -> Result unreachable!(), FsEntry::Symlink(_) => todo!("symlink support"), FsEntry::RuntimeFile(path) => { - host_passthrough = true; + fs_passthrough = true; let str = data_section.string(path)?; ( StaticIndexType::RuntimeHostFile, @@ -285,7 +338,7 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result { - host_passthrough = true; + fs_passthrough = true; let str = data_section.string(path)?; ( StaticIndexType::RuntimeHostDir, @@ -341,30 +394,32 @@ pub fn create_fs_virt<'a>(module: &'a mut Module, fs: &VirtFs) -> Result(module: &'a mut Module, fs: &VirtFs) -> Result(module: &'a mut Module, fs: &VirtFs) -> Result Result<()> { Ok(()) } -pub(crate) fn strip_fs_virt(module: &mut Module) -> Result<()> { - stub_fs_virt(module)?; - - remove_exported_func(module, "wasi:cli-base/preopens#get-directories")?; - - remove_exported_func(module, "wasi:filesystem/filesystem#read-via-stream")?; - remove_exported_func(module, "wasi:filesystem/filesystem#write-via-stream")?; - remove_exported_func(module, "wasi:filesystem/filesystem#append-via-stream")?; - remove_exported_func(module, "wasi:filesystem/filesystem#advise")?; - remove_exported_func(module, "wasi:filesystem/filesystem#sync-data")?; - remove_exported_func(module, "wasi:filesystem/filesystem#get-flags")?; - remove_exported_func(module, "wasi:filesystem/filesystem#get-type")?; - remove_exported_func(module, "wasi:filesystem/filesystem#set-size")?; - remove_exported_func(module, "wasi:filesystem/filesystem#set-times")?; - remove_exported_func(module, "wasi:filesystem/filesystem#read")?; - remove_exported_func(module, "wasi:filesystem/filesystem#write")?; - remove_exported_func(module, "wasi:filesystem/filesystem#read-directory")?; - remove_exported_func(module, "wasi:filesystem/filesystem#sync")?; - remove_exported_func(module, "wasi:filesystem/filesystem#create-directory-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#stat")?; - remove_exported_func(module, "wasi:filesystem/filesystem#stat-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#set-times-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#link-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#open-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#readlink-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#remove-directory-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#rename-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#symlink-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#access-at")?; - remove_exported_func(module, "wasi:filesystem/filesystem#unlink-file-at")?; - remove_exported_func( - module, - "wasi:filesystem/filesystem#change-file-permissions-at", - )?; - remove_exported_func( - module, - "wasi:filesystem/filesystem#change-directory-permissions-at", - )?; - remove_exported_func(module, "wasi:filesystem/filesystem#lock-shared")?; - remove_exported_func(module, "wasi:filesystem/filesystem#lock-exclusive")?; - remove_exported_func(module, "wasi:filesystem/filesystem#try-lock-shared")?; - remove_exported_func(module, "wasi:filesystem/filesystem#try-lock-exclusive")?; - remove_exported_func(module, "wasi:filesystem/filesystem#unlock")?; - remove_exported_func(module, "wasi:filesystem/filesystem#drop-descriptor")?; - remove_exported_func(module, "wasi:filesystem/filesystem#read-directory-entry")?; - remove_exported_func( - module, - "wasi:filesystem/filesystem#drop-directory-entry-stream", - )?; - - remove_exported_func(module, "wasi:io/streams#read")?; - remove_exported_func(module, "wasi:io/streams#blocking-read")?; - remove_exported_func(module, "wasi:io/streams#skip")?; - remove_exported_func(module, "wasi:io/streams#blocking-skip")?; - remove_exported_func(module, "wasi:io/streams#subscribe-to-input-stream")?; - remove_exported_func(module, "wasi:io/streams#drop-input-stream")?; - remove_exported_func(module, "wasi:io/streams#write")?; - remove_exported_func(module, "wasi:io/streams#blocking-write")?; - remove_exported_func(module, "wasi:io/streams#write-zeroes")?; - remove_exported_func(module, "wasi:io/streams#blocking-write-zeroes")?; - remove_exported_func(module, "wasi:io/streams#splice")?; - remove_exported_func(module, "wasi:io/streams#blocking-splice")?; - remove_exported_func(module, "wasi:io/streams#forward")?; - remove_exported_func(module, "wasi:io/streams#subscribe-to-output-stream")?; - remove_exported_func(module, "wasi:io/streams#drop-output-stream")?; +fn stub_stdio_virt(module: &mut Module) -> Result<()> { + stub_imported_func(module, "wasi:cli-base/stdin", "get-stdin", false)?; + stub_imported_func(module, "wasi:cli-base/stdout", "get-stdout", false)?; + stub_imported_func(module, "wasi:cli-base/stderr", "get-stderr", false)?; + Ok(()) +} +pub(crate) fn stub_io_virt(module: &mut Module) -> Result<()> { + stub_stdio_virt(module)?; + stub_fs_virt(module)?; Ok(()) } diff --git a/src/walrus_ops.rs b/src/walrus_ops.rs index d232d65..29fbd83 100644 --- a/src/walrus_ops.rs +++ b/src/walrus_ops.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use walrus::{ ir::Value, ActiveData, ActiveDataLocation, Data, DataKind, ExportItem, Function, FunctionBuilder, FunctionId, FunctionKind, GlobalKind, ImportKind, ImportedFunction, InitExpr, - MemoryId, Module, + MemoryId, Module, ValType, }; pub(crate) fn get_active_data_start(data: &Data, mem: MemoryId) -> Result { @@ -109,6 +109,21 @@ pub(crate) fn get_exported_func(module: &mut Module, name: &str) -> Result, + results: Vec, +) -> Result<()> { + let mut builder = FunctionBuilder::new(&mut module.types, ¶ms, &results); + builder.func_body().unreachable(); + let local_func = builder.local_func(vec![]); + let fid = module.funcs.add_local(local_func); + module.exports.add(export_name, ExportItem::Function(fid)); + + Ok(()) +} + pub(crate) fn stub_imported_func( module: &mut Module, import_module: &str, diff --git a/tests/cases/encapsulate.toml b/tests/cases/encapsulate.toml new file mode 100644 index 0000000..e40539c --- /dev/null +++ b/tests/cases/encapsulate.toml @@ -0,0 +1,237 @@ +component = "do-everything" + +host-fs-path = "/LICENSE" + +[virt-opts] +exit = "unreachable" +fs.host-preopens = false +stdio.stdin = false +stdio.stdout = false +stdio.stderr = false + +[virt-opts.fs.preopens."/".dir] +"LICENSE" = { virtualize = "./LICENSE" } + +[expect] +file-read = """ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +""" \ No newline at end of file diff --git a/tests/cases/passthrough.toml b/tests/cases/passthrough.toml new file mode 100644 index 0000000..7fb5854 --- /dev/null +++ b/tests/cases/passthrough.toml @@ -0,0 +1,234 @@ +component = "do-everything" + +host-fs-path = "/LICENSE" + +[virt-opts] +exit = "passthrough" +fs.host-preopens = true +stdio.stdin = true +stdio.stdout = true +stdio.stderr = true + +[expect] +file-read = """ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +""" \ No newline at end of file diff --git a/tests/components/do-everything/Cargo.toml b/tests/components/do-everything/Cargo.toml new file mode 100644 index 0000000..0903b84 --- /dev/null +++ b/tests/components/do-everything/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "do-everything" +version = "0.1.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = { workspace = true } +rand = "0.8.5" +wit-bindgen = { workspace = true } diff --git a/tests/components/do-everything/src/lib.rs b/tests/components/do-everything/src/lib.rs new file mode 100644 index 0000000..e227fdd --- /dev/null +++ b/tests/components/do-everything/src/lib.rs @@ -0,0 +1,32 @@ +use rand::prelude::*; +use std::env; +use std::fs; +use std::time::SystemTime; + +extern crate rand; + +wit_bindgen::generate!({ + path: "../../../wit", + world: "virt-test" +}); + +struct VirtTestImpl; + +export_virt_test!(VirtTestImpl); + +impl VirtTest for VirtTestImpl { + fn test_get_env() -> Vec<(String, String)> { + unreachable!(); + } + fn test_file_read(path: String) -> String { + let vars: Vec<(String, String)> = env::vars().collect(); + let mut rng = rand::thread_rng(); + println!("({:?}) TEST STDOUT - {:?}", SystemTime::now(), vars); + eprintln!( + "({:?}) TEST STDERR - {}", + SystemTime::now(), + rng.gen::() + ); + fs::read_to_string(&path).unwrap_or_else(|e| format!("ERR: {:?}", e)) + } +} diff --git a/tests/virt.rs b/tests/virt.rs index f614064..c9454b3 100644 --- a/tests/virt.rs +++ b/tests/virt.rs @@ -72,7 +72,7 @@ async fn virt_test() -> Result<()> { let test_case_name = test_case_file_name.strip_suffix(".toml").unwrap(); // Filtering... - // if test_case_name != "fs-host-read" { + // if test_case_name != "env-allow" { // continue; // } @@ -106,7 +106,9 @@ async fn virt_test() -> Result<()> { // create the test case specific virtualization let mut virt_component_path = generated_path.join(test_case_name); virt_component_path.set_extension("virt.wasm"); - let virt_opts = test.virt_opts.clone().unwrap_or_default(); + let mut virt_opts = test.virt_opts.clone().unwrap_or_default(); + virt_opts.exit(Default::default()); + let virt_component = virt_opts .finish() .with_context(|| format!("Error creating virtual adapter for {:?}", test_case_path))?; diff --git a/virtual-adapter/src/fs.rs b/virtual-adapter/src/fs.rs deleted file mode 100644 index 44a33b1..0000000 --- a/virtual-adapter/src/fs.rs +++ /dev/null @@ -1,752 +0,0 @@ -use crate::exports::wasi::cli_base::preopens::Preopens; -use crate::exports::wasi::filesystem::filesystem::{ - AccessType, Advice, Datetime, DescriptorFlags, DescriptorStat, DescriptorType, DirectoryEntry, - ErrorCode, Filesystem, Modes, NewTimestamp, OpenFlags, PathFlags, -}; -use crate::exports::wasi::io::streams::{StreamError, Streams}; -use crate::wasi::cli_base::preopens; -use crate::wasi::filesystem::filesystem; -// use crate::wasi::io::streams; - -// for debugging -// use crate::console; -// use std::fmt; - -use crate::VirtAdapter; -use std::alloc::Layout; -use std::cmp; -use std::collections::BTreeMap; -use std::ffi::CStr; -use std::slice; - -// static fs config -#[repr(C)] -pub struct Fs { - preopen_cnt: usize, - preopens: *const usize, - static_index_cnt: usize, - static_index: *const StaticIndexEntry, - host_passthrough: bool, -} - -impl Fs { - fn preopens() -> Vec<&'static StaticIndexEntry> { - let preopen_offsets = unsafe { slice::from_raw_parts(fs.preopens, fs.preopen_cnt) }; - let static_index = Fs::static_index(); - preopen_offsets - .iter() - .map(|&idx| &static_index[idx]) - .collect() - } - fn static_index() -> &'static [StaticIndexEntry] { - unsafe { slice::from_raw_parts(fs.static_index, fs.static_index_cnt) } - } -} - -// #[derive(Debug)] -struct Descriptor { - // the static entry referenced by this descriptor - entry: *const StaticIndexEntry, - // the descriptor index of this descriptor - fd: u32, - // if a host entry, the underlying host descriptor - // (if any) - host_fd: Option, -} - -impl Descriptor { - fn entry(&self) -> &StaticIndexEntry { - unsafe { self.entry.as_ref() }.unwrap() - } - - fn drop(&self) { - unsafe { - STATE.descriptor_table.remove(&self.fd); - } - if let Some(host_fd) = self.host_fd { - filesystem::drop_descriptor(host_fd); - } - } - - fn get_bytes<'a>(&mut self, offset: u64, len: u64) -> Result<(Vec, bool), ErrorCode> { - let entry = self.entry(); - match entry.ty { - StaticIndexType::ActiveFile => { - if offset as usize == unsafe { entry.data.active.1 } { - return Ok((vec![], true)); - } - if offset as usize > unsafe { entry.data.active.1 } { - return Err(ErrorCode::InvalidSeek); - } - let read_ptr = unsafe { entry.data.active.0.add(offset as usize) }; - let read_len = cmp::min( - unsafe { entry.data.active.1 } - offset as usize, - len as usize, - ); - let bytes = unsafe { slice::from_raw_parts(read_ptr, read_len) }; - Ok((bytes.to_vec(), read_len < len as usize)) - } - StaticIndexType::PassiveFile => { - if offset as usize == unsafe { entry.data.passive.1 } { - return Ok((vec![], true)); - } - if offset as usize > unsafe { entry.data.passive.1 } { - return Err(ErrorCode::InvalidSeek); - } - let read_len = cmp::min( - unsafe { entry.data.passive.1 } - offset as usize, - len as usize, - ); - let data = passive_alloc( - unsafe { entry.data.passive.0 }, - offset as u32, - read_len as u32, - ); - let bytes = unsafe { slice::from_raw_parts(data, read_len) }; - let vec = bytes.to_vec(); - unsafe { std::alloc::dealloc(data, Layout::from_size_align(1, 4).unwrap()) }; - Ok((vec, read_len < len as usize)) - } - StaticIndexType::Dir => todo!(), - StaticIndexType::RuntimeDir => todo!(), - StaticIndexType::RuntimeFile => { - if let Some(host_fd) = self.host_fd { - return filesystem::read(host_fd, len, offset).map_err(err_map); - } - - let path = unsafe { CStr::from_ptr(entry.data.runtime_path) }; - let path = path.to_str().unwrap(); - - let Some((preopen_fd, subpath)) = FsState::get_host_preopen(path) else { - return Err(ErrorCode::NoEntry); - }; - let host_fd = filesystem::open_at( - preopen_fd, - filesystem::PathFlags::empty(), - subpath, - filesystem::OpenFlags::empty(), - filesystem::DescriptorFlags::READ, - filesystem::Modes::READABLE, - ) - .map_err(err_map)?; - - self.host_fd = Some(host_fd); - filesystem::read(host_fd, len, offset).map_err(err_map) - } - } - } -} - -fn err_map(e: filesystem::ErrorCode) -> ErrorCode { - match e { - filesystem::ErrorCode::Access => ErrorCode::Access, - filesystem::ErrorCode::WouldBlock => ErrorCode::WouldBlock, - filesystem::ErrorCode::Already => ErrorCode::Already, - filesystem::ErrorCode::BadDescriptor => ErrorCode::BadDescriptor, - filesystem::ErrorCode::Busy => ErrorCode::Busy, - filesystem::ErrorCode::Deadlock => ErrorCode::Deadlock, - filesystem::ErrorCode::Quota => ErrorCode::Quota, - filesystem::ErrorCode::Exist => ErrorCode::Exist, - filesystem::ErrorCode::FileTooLarge => ErrorCode::FileTooLarge, - filesystem::ErrorCode::IllegalByteSequence => ErrorCode::IllegalByteSequence, - filesystem::ErrorCode::InProgress => ErrorCode::InProgress, - filesystem::ErrorCode::Interrupted => ErrorCode::Interrupted, - filesystem::ErrorCode::Invalid => ErrorCode::Invalid, - filesystem::ErrorCode::Io => ErrorCode::Io, - filesystem::ErrorCode::IsDirectory => ErrorCode::IsDirectory, - filesystem::ErrorCode::Loop => ErrorCode::Loop, - filesystem::ErrorCode::TooManyLinks => ErrorCode::TooManyLinks, - filesystem::ErrorCode::MessageSize => ErrorCode::MessageSize, - filesystem::ErrorCode::NameTooLong => ErrorCode::NameTooLong, - filesystem::ErrorCode::NoDevice => ErrorCode::NoDevice, - filesystem::ErrorCode::NoEntry => ErrorCode::NoEntry, - filesystem::ErrorCode::NoLock => ErrorCode::NoLock, - filesystem::ErrorCode::InsufficientMemory => ErrorCode::InsufficientMemory, - filesystem::ErrorCode::InsufficientSpace => ErrorCode::InsufficientSpace, - filesystem::ErrorCode::NotDirectory => ErrorCode::NotDirectory, - filesystem::ErrorCode::NotEmpty => ErrorCode::NotEmpty, - filesystem::ErrorCode::NotRecoverable => ErrorCode::NotRecoverable, - filesystem::ErrorCode::Unsupported => ErrorCode::Unsupported, - filesystem::ErrorCode::NoTty => ErrorCode::NoTty, - filesystem::ErrorCode::NoSuchDevice => ErrorCode::NoSuchDevice, - filesystem::ErrorCode::Overflow => ErrorCode::Overflow, - filesystem::ErrorCode::NotPermitted => ErrorCode::NotPermitted, - filesystem::ErrorCode::Pipe => ErrorCode::Pipe, - filesystem::ErrorCode::ReadOnly => ErrorCode::ReadOnly, - filesystem::ErrorCode::InvalidSeek => ErrorCode::InvalidSeek, - filesystem::ErrorCode::TextFileBusy => ErrorCode::TextFileBusy, - filesystem::ErrorCode::CrossDevice => ErrorCode::CrossDevice, - } -} - -impl StaticIndexEntry { - // fn idx(&self) -> usize { - // let static_index_start = unsafe { fs.static_index }; - // let cur_index_start = self as *const StaticIndexEntry; - // unsafe { cur_index_start.sub_ptr(static_index_start) } - // } - fn name(&self) -> &'static str { - let c_str = unsafe { CStr::from_ptr((*self).name) }; - c_str.to_str().unwrap() - } - fn ty(&self) -> DescriptorType { - match self.ty { - StaticIndexType::ActiveFile - | StaticIndexType::PassiveFile - | StaticIndexType::RuntimeFile => DescriptorType::RegularFile, - StaticIndexType::Dir | StaticIndexType::RuntimeDir => DescriptorType::Directory, - } - } - fn size(&self) -> Result { - match self.ty { - StaticIndexType::ActiveFile => Ok(unsafe { self.data.active.1 } as u64), - StaticIndexType::PassiveFile => Ok(unsafe { self.data.passive.1 } as u64), - StaticIndexType::Dir | StaticIndexType::RuntimeDir => Ok(0), - StaticIndexType::RuntimeFile => { - let path = unsafe { CStr::from_ptr(self.data.runtime_path) }; - let path = path.to_str().unwrap(); - let Some((fd, subpath)) = FsState::get_host_preopen(path) else { - return Err(ErrorCode::NoEntry); - }; - let stat = filesystem::stat_at(fd, filesystem::PathFlags::empty(), subpath) - .map_err(err_map)?; - Ok(stat.size) - } - } - } - fn child_list(&self) -> Result<&'static [StaticIndexEntry], ErrorCode> { - if !matches!(self.ty(), DescriptorType::Directory) { - return Err(ErrorCode::NotDirectory); - } - let (child_list_idx, child_list_len) = unsafe { (*self).data.dir }; - let static_index = Fs::static_index(); - Ok(&static_index[child_list_idx..child_list_idx + child_list_len]) - } - fn dir_lookup(&self, path: &str) -> Result<&StaticIndexEntry, ErrorCode> { - assert!(path.len() > 0); - let (first_part, rem) = match path.find('/') { - Some(idx) => (&path[0..idx], &path[idx + 1..]), - None => (path, ""), - }; - let child_list = self.child_list()?; - if let Ok(child_idx) = child_list.binary_search_by(|entry| entry.name().cmp(first_part)) { - let child = &child_list[child_idx]; - if rem.len() > 0 { - child.dir_lookup(rem) - } else { - Ok(child) - } - } else { - Err(ErrorCode::NoEntry) - } - } -} - -// #[derive(Debug)] -#[repr(C)] -struct StaticIndexEntry { - name: *const i8, - ty: StaticIndexType, - data: StaticFileData, -} - -#[repr(C)] -union StaticFileData { - /// Active memory data pointer for ActiveFile - active: (*const u8, usize), - /// Passive memory element index and len for PassiveFile - passive: (u32, usize), - /// Host path string for HostDir / HostFile - runtime_path: *const i8, - // Index and child entry count for Dir - dir: (usize, usize), -} - -// impl fmt::Debug for StaticFileData { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// f.write_str(&format!( -// "STATIC [{:?}, {:?}]", -// unsafe { self.dir.0 }, -// unsafe { self.dir.1 } -// ))?; -// Ok(()) -// } -// } - -// #[derive(Debug)] -#[allow(dead_code)] -#[repr(u32)] -enum StaticIndexType { - ActiveFile, - PassiveFile, - Dir, - RuntimeDir, - RuntimeFile, -} - -// This function gets mutated by the virtualizer -#[no_mangle] -#[inline(never)] -pub fn passive_alloc(passive_idx: u32, offset: u32, len: u32) -> *mut u8 { - return (passive_idx + offset + len) as *mut u8; -} - -#[no_mangle] -pub static mut fs: Fs = Fs { - preopen_cnt: 0, // [byte 0] - preopens: 0 as *const usize, // [byte 4] - static_index_cnt: 0, // [byte 8] - static_index: 0 as *const StaticIndexEntry, // [byte 12] - host_passthrough: false, // [byte 16] -}; - -// local fs state -pub struct FsState { - initialized: bool, - descriptor_cnt: u32, - preopen_directories: Vec, - host_preopen_directories: BTreeMap, - descriptor_table: BTreeMap, - stream_cnt: u32, - stream_table: BTreeMap, -} - -static mut STATE: FsState = FsState { - initialized: false, - descriptor_cnt: 3, - preopen_directories: Vec::new(), - host_preopen_directories: BTreeMap::new(), - descriptor_table: BTreeMap::new(), - stream_cnt: 0, - stream_table: BTreeMap::new(), -}; - -enum Stream { - File(FileStream), - Dir(DirStream), -} - -impl From for Stream { - fn from(value: FileStream) -> Self { - Stream::File(value) - } -} - -impl From for Stream { - fn from(value: DirStream) -> Self { - Stream::Dir(value) - } -} - -struct FileStream { - // local file descriptor - fd: u32, - // current offset - offset: u64, -} - -struct DirStream { - fd: u32, - idx: usize, -} - -impl FileStream { - fn new(fd: u32) -> Self { - Self { fd, offset: 0 } - } - fn read(&mut self, len: u64) -> Result<(Vec, bool), StreamError> { - let Some(descriptor) = FsState::get_descriptor(self.fd) else { - return Err(StreamError {}); - }; - let (bytes, done) = descriptor - .get_bytes(self.offset, len) - .map_err(|_| StreamError {})?; - self.offset += bytes.len() as u64; - Ok((bytes, done)) - } -} - -impl DirStream { - fn new(fd: u32) -> Self { - Self { fd, idx: 0 } - } - fn next(&mut self) -> Result, ErrorCode> { - let Some(descriptor) = FsState::get_descriptor(self.fd) else { - return Err(ErrorCode::BadDescriptor); - }; - let child_list = descriptor.entry().child_list()?; - if self.idx < child_list.len() { - let child = &child_list[self.idx]; - self.idx += 1; - Ok(Some(DirectoryEntry { - inode: None, - type_: child.ty(), - name: child.name().into(), - })) - } else { - Ok(None) - } - } -} - -impl FsState { - fn initialize() { - if unsafe { STATE.initialized } { - return; - } - if unsafe { fs.host_passthrough } { - let host_preopen_directories = unsafe { &mut STATE.host_preopen_directories }; - for (fd, name) in preopens::get_directories() { - host_preopen_directories.insert(name, fd); - } - } - let preopens = Fs::preopens(); - for preopen in preopens { - let fd = FsState::create_descriptor(preopen, DescriptorFlags::READ); - unsafe { STATE.preopen_directories.push(fd) } - } - unsafe { STATE.initialized = true }; - } - fn get_host_preopen<'a>(path: &'a str) -> Option<(u32, &'a str)> { - let path = if path.starts_with("./") { - &path[2..] - } else { - path - }; - for (preopen_name, fd) in unsafe { &STATE.host_preopen_directories } { - let preopen_name = if preopen_name.starts_with("./") { - &preopen_name[2..] - } else if preopen_name.starts_with(".") { - &preopen_name[1..] - } else { - preopen_name - }; - if path.starts_with(preopen_name) { - // ambient relative - if preopen_name.len() == 0 { - if path.as_bytes()[0] != b'/' { - return Some((*fd, &path)); - } - } else { - // root '/' match - if preopen_name == "/" && path.as_bytes()[0] == b'/' { - return Some((*fd, &path[1..])); - } - // exact match - if preopen_name.len() == path.len() { - return Some((*fd, "")); - } - // normal [x]/ match - if path.as_bytes()[preopen_name.len()] == b'/' { - return Some((*fd, &path[preopen_name.len() + 1..])); - } - } - } - } - None - } - fn create_descriptor(entry: &StaticIndexEntry, _flags: DescriptorFlags) -> u32 { - let fd = unsafe { STATE.descriptor_cnt }; - unsafe { STATE.descriptor_cnt += 1 }; - let descriptor = Descriptor { - entry, - fd, - host_fd: None, - }; - assert!(unsafe { STATE.descriptor_table.insert(fd, descriptor) }.is_none()); - fd - } - fn get_descriptor<'a>(fd: u32) -> Option<&'a mut Descriptor> { - unsafe { STATE.descriptor_table.get_mut(&fd) } - } - fn get_preopen_directories() -> Vec<(u32, String)> { - FsState::initialize(); - unsafe { &STATE.preopen_directories } - .iter() - .map(|&fd| { - let descriptor = FsState::get_descriptor(fd).unwrap(); - let name = descriptor.entry().name(); - (fd, name.to_string()) - }) - .collect() - } - fn create_stream>(stream: S) -> Result { - let sid = unsafe { STATE.stream_cnt }; - unsafe { STATE.stream_cnt += 1 }; - unsafe { STATE.stream_table.insert(sid, stream.into()) }; - Ok(sid) - } - fn get_stream<'a>(sid: u32) -> Option<&'a mut Stream> { - unsafe { STATE.stream_table.get_mut(&sid) } - } - fn drop_stream(sid: u32) { - unsafe { STATE.stream_table.remove(&sid) }; - } -} - -impl Preopens for VirtAdapter { - fn get_directories() -> Vec<(u32, String)> { - FsState::get_preopen_directories() - } -} - -impl Filesystem for VirtAdapter { - fn read_via_stream(fd: u32, offset: u64) -> Result { - if offset != 0 { - return Err(ErrorCode::InvalidSeek); - } - FsState::create_stream(FileStream::new(fd)) - } - fn write_via_stream(_: u32, _: u64) -> Result { - Err(ErrorCode::Access) - } - fn append_via_stream(_fd: u32) -> Result { - Err(ErrorCode::Access) - } - fn advise(_: u32, _: u64, _: u64, _: Advice) -> Result<(), ErrorCode> { - todo!() - } - fn sync_data(_: u32) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn get_flags(_fd: u32) -> Result { - Ok(DescriptorFlags::READ) - } - fn get_type(fd: u32) -> Result { - let Some(descriptor) = FsState::get_descriptor(fd) else { - return Err(ErrorCode::BadDescriptor); - }; - Ok(descriptor.entry().ty()) - } - fn set_size(_: u32, _: u64) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn set_times(_: u32, _: NewTimestamp, _: NewTimestamp) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn read(fd: u32, len: u64, offset: u64) -> Result<(Vec, bool), ErrorCode> { - let sid = VirtAdapter::read_via_stream(fd, offset)?; - let stream = FsState::get_stream(sid).unwrap(); - let Stream::File(filestream) = stream else { - unreachable!() - }; - let result = filestream.read(len).map_err(|_| ErrorCode::Io)?; - FsState::drop_stream(sid); - Ok(result) - } - fn write(_: u32, _: Vec, _: u64) -> Result { - Err(ErrorCode::Access) - } - fn read_directory(fd: u32) -> Result { - let Some(descriptor) = FsState::get_descriptor(fd) else { - return Err(ErrorCode::BadDescriptor); - }; - if descriptor.entry().ty() != DescriptorType::Directory { - return Err(ErrorCode::NotDirectory); - } - FsState::create_stream(DirStream::new(fd)) - } - fn sync(_: u32) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn create_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn stat(fd: u32) -> Result { - let Some(descriptor) = FsState::get_descriptor(fd) else { - return Err(ErrorCode::BadDescriptor); - }; - Ok(DescriptorStat { - device: 0, - inode: 0, - type_: descriptor.entry().ty(), - link_count: 0, - size: descriptor.entry().size()?, - data_access_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - data_modification_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - status_change_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - }) - } - fn stat_at(fd: u32, _flags: PathFlags, path: String) -> Result { - let Some(descriptor) = FsState::get_descriptor(fd) else { - return Err(ErrorCode::BadDescriptor); - }; - let child = descriptor.entry().dir_lookup(&path)?; - Ok(DescriptorStat { - device: 0, - inode: 0, - type_: child.ty(), - link_count: 0, - size: child.size()?, - data_access_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - data_modification_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - status_change_timestamp: Datetime { - seconds: 0, - nanoseconds: 0, - }, - }) - } - fn set_times_at( - _: u32, - _: PathFlags, - _: String, - _: NewTimestamp, - _: NewTimestamp, - ) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn link_at(_: u32, _: PathFlags, _: String, _: u32, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn open_at( - fd: u32, - _path_flags: PathFlags, - path: String, - _open_flags: OpenFlags, - descriptor_flags: DescriptorFlags, - _modes: Modes, - ) -> Result { - let Some(descriptor) = FsState::get_descriptor(fd) else { - return Err(ErrorCode::BadDescriptor); - }; - let child = descriptor.entry().dir_lookup(&path)?; - Ok(FsState::create_descriptor(child, descriptor_flags)) - } - fn readlink_at(_: u32, _: String) -> Result { - todo!() - } - fn remove_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn rename_at(_: u32, _: String, _: u32, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn symlink_at(_: u32, _: String, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn access_at(_: u32, _: PathFlags, _: String, _: AccessType) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn unlink_file_at(_: u32, _: String) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn change_file_permissions_at( - _: u32, - _: PathFlags, - _: String, - _: Modes, - ) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn change_directory_permissions_at( - _: u32, - _: PathFlags, - _: String, - _: Modes, - ) -> Result<(), ErrorCode> { - Err(ErrorCode::Access) - } - fn lock_shared(_fd: u32) -> Result<(), ErrorCode> { - Ok(()) - } - fn lock_exclusive(_fd: u32) -> Result<(), ErrorCode> { - Ok(()) - } - fn try_lock_shared(_fd: u32) -> Result<(), ErrorCode> { - Ok(()) - } - fn try_lock_exclusive(_fd: u32) -> Result<(), ErrorCode> { - Ok(()) - } - fn unlock(_: u32) -> Result<(), ErrorCode> { - Ok(()) - } - fn drop_descriptor(fd: u32) { - if let Some(descriptor) = FsState::get_descriptor(fd) { - descriptor.drop(); - }; - } - fn read_directory_entry(sid: u32) -> Result, ErrorCode> { - let Some(stream) = FsState::get_stream(sid) else { - return Err(ErrorCode::BadDescriptor); - }; - match stream { - Stream::Dir(dirstream) => dirstream.next(), - _ => { - return Err(ErrorCode::BadDescriptor); - } - } - } - fn drop_directory_entry_stream(sid: u32) { - FsState::drop_stream(sid); - } -} - -impl Streams for VirtAdapter { - fn read(sid: u32, len: u64) -> Result<(Vec, bool), StreamError> { - VirtAdapter::blocking_read(sid, len) - } - fn blocking_read(sid: u32, len: u64) -> Result<(Vec, bool), StreamError> { - let Some(stream) = FsState::get_stream(sid) else { - return Err(StreamError {}); - }; - match stream { - Stream::File(filestream) => filestream.read(len), - _ => Err(StreamError {}), - } - } - fn skip(_: u32, _: u64) -> Result<(u64, bool), StreamError> { - todo!() - } - fn blocking_skip(_: u32, _: u64) -> Result<(u64, bool), StreamError> { - todo!() - } - fn subscribe_to_input_stream(_: u32) -> u32 { - todo!() - } - fn drop_input_stream(sid: u32) { - FsState::drop_stream(sid); - } - fn write(_: u32, _: Vec) -> Result { - Err(StreamError {}) - } - fn blocking_write(_: u32, _: Vec) -> Result { - Err(StreamError {}) - } - fn write_zeroes(_: u32, _: u64) -> Result { - Err(StreamError {}) - } - fn blocking_write_zeroes(_: u32, _: u64) -> Result { - Err(StreamError {}) - } - fn splice(_: u32, _: u32, _: u64) -> Result<(u64, bool), StreamError> { - todo!() - } - fn blocking_splice(_: u32, _: u32, _: u64) -> Result<(u64, bool), StreamError> { - todo!() - } - fn forward(_: u32, _: u32) -> Result { - todo!() - } - fn subscribe_to_output_stream(_: u32) -> u32 { - todo!() - } - fn drop_output_stream(_: u32) { - todo!() - } -} diff --git a/virtual-adapter/src/io.rs b/virtual-adapter/src/io.rs new file mode 100644 index 0000000..3b28704 --- /dev/null +++ b/virtual-adapter/src/io.rs @@ -0,0 +1,1060 @@ +use crate::exports::wasi::cli_base::preopens::Preopens; +use crate::exports::wasi::cli_base::stderr::Stderr; +use crate::exports::wasi::cli_base::stdin::Stdin; +use crate::exports::wasi::cli_base::stdout::Stdout; +use crate::exports::wasi::filesystem::filesystem::{ + AccessType, Advice, Datetime, DescriptorFlags, DescriptorStat, DescriptorType, DirectoryEntry, + ErrorCode, Filesystem, Modes, NewTimestamp, OpenFlags, PathFlags, +}; +use crate::exports::wasi::io::streams::{StreamError, Streams}; + +use crate::wasi::cli_base::preopens; +use crate::wasi::cli_base::stderr; +use crate::wasi::cli_base::stdin; +use crate::wasi::cli_base::stdout; +use crate::wasi::filesystem::filesystem; +use crate::wasi::io::streams; + +use crate::VirtAdapter; + +// for debugging +// use crate::console; +// use std::fmt; + +use std::alloc::Layout; +use std::cmp; +use std::collections::BTreeMap; +use std::ffi::CStr; +use std::slice; + +// io flags +const FLAGS_ENABLE_STDIN: u32 = 1; +const FLAGS_ENABLE_STDOUT: u32 = 2; +const FLAGS_ENABLE_STDERR: u32 = 4; +const FLAGS_HOST_PREOPENS: u32 = 8; +const FLAGS_HOST_PASSTHROUGH: u32 = 16; + +// static fs config +#[repr(C)] +pub struct Io { + preopen_cnt: usize, + preopens: *const usize, + static_index_cnt: usize, + static_index: *const StaticIndexEntry, + flags: u32, +} + +impl Io { + fn preopens() -> Vec<&'static StaticIndexEntry> { + let preopen_offsets = unsafe { slice::from_raw_parts(io.preopens, io.preopen_cnt) }; + let static_index = Io::static_index(); + preopen_offsets + .iter() + .map(|&idx| &static_index[idx]) + .collect() + } + fn static_index() -> &'static [StaticIndexEntry] { + unsafe { slice::from_raw_parts(io.static_index, io.static_index_cnt) } + } + fn stdin() -> bool { + (unsafe { io.flags }) & FLAGS_ENABLE_STDIN > 0 + } + fn stdout() -> bool { + (unsafe { io.flags }) & FLAGS_ENABLE_STDOUT > 0 + } + fn stderr() -> bool { + (unsafe { io.flags }) & FLAGS_ENABLE_STDERR > 0 + } + fn host_passthrough() -> bool { + (unsafe { io.flags }) & FLAGS_HOST_PASSTHROUGH > 0 + } + fn host_preopens() -> bool { + (unsafe { io.flags }) & FLAGS_HOST_PREOPENS > 0 + } +} + +#[derive(Debug)] +enum DescriptorTarget { + StaticEntry(*const StaticIndexEntry), + HostDescriptor(u32), +} + +// #[derive(Debug)] +struct Descriptor { + // the descriptor index of this descriptor + fd: u32, + target: DescriptorTarget, +} + +impl Descriptor { + fn drop(&self) { + unsafe { + STATE.descriptor_table.remove(&self.fd); + } + if let DescriptorTarget::HostDescriptor(host_fd) = self.target { + filesystem::drop_descriptor(host_fd); + } + } + + fn get_type(&self) -> Result { + match self.target { + DescriptorTarget::StaticEntry(ptr) => { + let entry = entry(ptr); + Ok(entry.ty()) + } + DescriptorTarget::HostDescriptor(host_fd) => filesystem::get_type(host_fd) + .map(descriptor_ty_map) + .map_err(err_map), + } + } + + fn read<'a>(&mut self, offset: u64, len: u64) -> Result<(Vec, bool), ErrorCode> { + match self.target { + DescriptorTarget::StaticEntry(ptr) => { + let entry = entry(ptr); + match entry.ty { + StaticIndexType::ActiveFile => { + if offset as usize == unsafe { entry.data.active.1 } { + return Ok((vec![], true)); + } + if offset as usize > unsafe { entry.data.active.1 } { + return Err(ErrorCode::InvalidSeek); + } + let read_ptr = unsafe { entry.data.active.0.add(offset as usize) }; + let read_len = cmp::min( + unsafe { entry.data.active.1 } - offset as usize, + len as usize, + ); + let bytes = unsafe { slice::from_raw_parts(read_ptr, read_len) }; + Ok((bytes.to_vec(), read_len < len as usize)) + } + StaticIndexType::PassiveFile => { + if offset as usize == unsafe { entry.data.passive.1 } { + return Ok((vec![], true)); + } + if offset as usize > unsafe { entry.data.passive.1 } { + return Err(ErrorCode::InvalidSeek); + } + let read_len = cmp::min( + unsafe { entry.data.passive.1 } - offset as usize, + len as usize, + ); + let data = passive_alloc( + unsafe { entry.data.passive.0 }, + offset as u32, + read_len as u32, + ); + let bytes = unsafe { slice::from_raw_parts(data, read_len) }; + let vec = bytes.to_vec(); + unsafe { + std::alloc::dealloc(data, Layout::from_size_align(1, 4).unwrap()) + }; + Ok((vec, read_len < len as usize)) + } + StaticIndexType::RuntimeDir | StaticIndexType::Dir => { + Err(ErrorCode::IsDirectory) + } + StaticIndexType::RuntimeFile => { + // console::log("Internal error: Runtime file should not be reflected directly on descriptors"); + unreachable!(); + } + } + } + DescriptorTarget::HostDescriptor(host_fd) => { + filesystem::read(host_fd, len, offset).map_err(err_map) + } + } + } +} + +fn dir_map(d: filesystem::DirectoryEntry) -> DirectoryEntry { + DirectoryEntry { + inode: d.inode, + type_: descriptor_ty_map(d.type_), + name: d.name, + } +} + +fn stat_map(s: filesystem::DescriptorStat) -> DescriptorStat { + DescriptorStat { + device: s.device, + inode: s.inode, + type_: descriptor_ty_map(s.type_), + link_count: s.link_count, + size: s.size, + data_access_timestamp: s.data_modification_timestamp, + data_modification_timestamp: s.data_modification_timestamp, + status_change_timestamp: s.status_change_timestamp, + } +} + +fn descriptor_ty_map(d: filesystem::DescriptorType) -> DescriptorType { + match d { + filesystem::DescriptorType::Unknown => DescriptorType::Unknown, + filesystem::DescriptorType::BlockDevice => DescriptorType::BlockDevice, + filesystem::DescriptorType::CharacterDevice => DescriptorType::CharacterDevice, + filesystem::DescriptorType::Directory => DescriptorType::Directory, + filesystem::DescriptorType::Fifo => DescriptorType::Fifo, + filesystem::DescriptorType::SymbolicLink => DescriptorType::SymbolicLink, + filesystem::DescriptorType::RegularFile => DescriptorType::RegularFile, + filesystem::DescriptorType::Socket => DescriptorType::Socket, + } +} + +fn err_map(e: filesystem::ErrorCode) -> ErrorCode { + match e { + filesystem::ErrorCode::Access => ErrorCode::Access, + filesystem::ErrorCode::WouldBlock => ErrorCode::WouldBlock, + filesystem::ErrorCode::Already => ErrorCode::Already, + filesystem::ErrorCode::BadDescriptor => ErrorCode::BadDescriptor, + filesystem::ErrorCode::Busy => ErrorCode::Busy, + filesystem::ErrorCode::Deadlock => ErrorCode::Deadlock, + filesystem::ErrorCode::Quota => ErrorCode::Quota, + filesystem::ErrorCode::Exist => ErrorCode::Exist, + filesystem::ErrorCode::FileTooLarge => ErrorCode::FileTooLarge, + filesystem::ErrorCode::IllegalByteSequence => ErrorCode::IllegalByteSequence, + filesystem::ErrorCode::InProgress => ErrorCode::InProgress, + filesystem::ErrorCode::Interrupted => ErrorCode::Interrupted, + filesystem::ErrorCode::Invalid => ErrorCode::Invalid, + filesystem::ErrorCode::Io => ErrorCode::Io, + filesystem::ErrorCode::IsDirectory => ErrorCode::IsDirectory, + filesystem::ErrorCode::Loop => ErrorCode::Loop, + filesystem::ErrorCode::TooManyLinks => ErrorCode::TooManyLinks, + filesystem::ErrorCode::MessageSize => ErrorCode::MessageSize, + filesystem::ErrorCode::NameTooLong => ErrorCode::NameTooLong, + filesystem::ErrorCode::NoDevice => ErrorCode::NoDevice, + filesystem::ErrorCode::NoEntry => ErrorCode::NoEntry, + filesystem::ErrorCode::NoLock => ErrorCode::NoLock, + filesystem::ErrorCode::InsufficientMemory => ErrorCode::InsufficientMemory, + filesystem::ErrorCode::InsufficientSpace => ErrorCode::InsufficientSpace, + filesystem::ErrorCode::NotDirectory => ErrorCode::NotDirectory, + filesystem::ErrorCode::NotEmpty => ErrorCode::NotEmpty, + filesystem::ErrorCode::NotRecoverable => ErrorCode::NotRecoverable, + filesystem::ErrorCode::Unsupported => ErrorCode::Unsupported, + filesystem::ErrorCode::NoTty => ErrorCode::NoTty, + filesystem::ErrorCode::NoSuchDevice => ErrorCode::NoSuchDevice, + filesystem::ErrorCode::Overflow => ErrorCode::Overflow, + filesystem::ErrorCode::NotPermitted => ErrorCode::NotPermitted, + filesystem::ErrorCode::Pipe => ErrorCode::Pipe, + filesystem::ErrorCode::ReadOnly => ErrorCode::ReadOnly, + filesystem::ErrorCode::InvalidSeek => ErrorCode::InvalidSeek, + filesystem::ErrorCode::TextFileBusy => ErrorCode::TextFileBusy, + filesystem::ErrorCode::CrossDevice => ErrorCode::CrossDevice, + } +} + +fn entry(ptr: *const StaticIndexEntry) -> &'static StaticIndexEntry { + unsafe { ptr.as_ref() }.unwrap() +} + +impl StaticIndexEntry { + // fn idx(&self) -> usize { + // let static_index_start = unsafe { fs.static_index }; + // let cur_index_start = self as *const StaticIndexEntry; + // unsafe { cur_index_start.sub_ptr(static_index_start) } + // } + fn runtime_path(&self) -> &'static str { + let c_str = unsafe { CStr::from_ptr((*self).data.runtime_path) }; + c_str.to_str().unwrap() + } + fn name(&self) -> &'static str { + let c_str = unsafe { CStr::from_ptr((*self).name) }; + c_str.to_str().unwrap() + } + fn ty(&self) -> DescriptorType { + match self.ty { + StaticIndexType::ActiveFile + | StaticIndexType::PassiveFile + | StaticIndexType::RuntimeFile => DescriptorType::RegularFile, + StaticIndexType::Dir | StaticIndexType::RuntimeDir => DescriptorType::Directory, + } + } + fn size(&self) -> Result { + match self.ty { + StaticIndexType::ActiveFile => Ok(unsafe { self.data.active.1 } as u64), + StaticIndexType::PassiveFile => Ok(unsafe { self.data.passive.1 } as u64), + StaticIndexType::Dir | StaticIndexType::RuntimeDir => Ok(0), + StaticIndexType::RuntimeFile => { + let Some((fd, subpath)) = IoState::get_host_preopen(self.runtime_path()) else { + return Err(ErrorCode::NoEntry); + }; + let stat = filesystem::stat_at(fd, filesystem::PathFlags::empty(), subpath) + .map_err(err_map)?; + Ok(stat.size) + } + } + } + fn child_list(&self) -> Result<&'static [StaticIndexEntry], ErrorCode> { + if !matches!(self.ty(), DescriptorType::Directory) { + return Err(ErrorCode::NotDirectory); + } + let (child_list_idx, child_list_len) = unsafe { (*self).data.dir }; + let static_index = Io::static_index(); + Ok(&static_index[child_list_idx..child_list_idx + child_list_len]) + } + fn dir_lookup(&self, path: &str) -> Result<&'static StaticIndexEntry, ErrorCode> { + assert!(path.len() > 0); + let (first_part, rem) = match path.find('/') { + Some(idx) => (&path[0..idx], &path[idx + 1..]), + None => (path, ""), + }; + let child_list = self.child_list()?; + if let Ok(child_idx) = child_list.binary_search_by(|entry| entry.name().cmp(first_part)) { + let child = &child_list[child_idx]; + if rem.len() > 0 { + child.dir_lookup(rem) + } else { + Ok(child) + } + } else { + Err(ErrorCode::NoEntry) + } + } +} + +// #[derive(Debug)] +#[repr(C)] +struct StaticIndexEntry { + name: *const i8, + ty: StaticIndexType, + data: StaticFileData, +} + +#[repr(C)] +union StaticFileData { + /// Active memory data pointer for ActiveFile + active: (*const u8, usize), + /// Passive memory element index and len for PassiveFile + passive: (u32, usize), + /// Host path string for HostDir / HostFile + runtime_path: *const i8, + // Index and child entry count for Dir + dir: (usize, usize), +} + +// impl fmt::Debug for StaticFileData { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// f.write_str(&format!( +// "STATIC [{:?}, {:?}]", +// unsafe { self.dir.0 }, +// unsafe { self.dir.1 } +// ))?; +// Ok(()) +// } +// } + +// #[derive(Debug)] +#[allow(dead_code)] +#[repr(u32)] +enum StaticIndexType { + ActiveFile, + PassiveFile, + Dir, + RuntimeDir, + RuntimeFile, +} + +// This function gets mutated by the virtualizer +#[no_mangle] +#[inline(never)] +pub fn passive_alloc(passive_idx: u32, offset: u32, len: u32) -> *mut u8 { + return (passive_idx + offset + len) as *mut u8; +} + +#[no_mangle] +pub static mut io: Io = Io { + preopen_cnt: 0, // [byte 0] + preopens: 0 as *const usize, // [byte 4] + static_index_cnt: 0, // [byte 8] + static_index: 0 as *const StaticIndexEntry, // [byte 12] + flags: 0, // [byte 16] +}; + +// local fs state +pub struct IoState { + initialized: bool, + descriptor_cnt: u32, + preopen_directories: Vec<(u32, String)>, + host_preopen_directories: BTreeMap, + descriptor_table: BTreeMap, + stream_cnt: u32, + stream_table: BTreeMap, +} + +static mut STATE: IoState = IoState { + initialized: false, + descriptor_cnt: 0, + preopen_directories: Vec::new(), + host_preopen_directories: BTreeMap::new(), + descriptor_table: BTreeMap::new(), + stream_cnt: 0, + stream_table: BTreeMap::new(), +}; + +enum Stream { + Null, + StaticFile(StaticFileStream), + StaticDir(StaticDirStream), + Host(u32), +} + +impl From for Stream { + fn from(value: StaticFileStream) -> Self { + Stream::StaticFile(value) + } +} + +impl From for Stream { + fn from(value: StaticDirStream) -> Self { + Stream::StaticDir(value) + } +} + +struct StaticFileStream { + // local file descriptor + fd: u32, + // current offset + offset: u64, +} + +struct StaticDirStream { + fd: u32, + idx: usize, +} + +impl StaticFileStream { + fn new(fd: u32) -> Self { + Self { fd, offset: 0 } + } + fn read(&mut self, len: u64) -> Result<(Vec, bool), StreamError> { + let descriptor = IoState::get_descriptor(self.fd).map_err(|_| StreamError {})?; + let (bytes, done) = descriptor + .read(self.offset, len) + .map_err(|_| StreamError {})?; + self.offset += bytes.len() as u64; + Ok((bytes, done)) + } +} + +impl StaticDirStream { + fn new(fd: u32) -> Self { + Self { fd, idx: 0 } + } + fn next(&mut self) -> Result, ErrorCode> { + let descriptor = IoState::get_descriptor(self.fd)?; + let DescriptorTarget::StaticEntry(ptr) = descriptor.target else { + unreachable!() + }; + let entry = entry(ptr); + let child_list = entry.child_list()?; + let child = if self.idx < child_list.len() { + let child = &child_list[self.idx]; + Some(DirectoryEntry { + inode: None, + type_: child.ty(), + name: child.name().into(), + }) + } else { + None + }; + self.idx += 1; + Ok(child) + } +} + +impl IoState { + fn initialize() { + if unsafe { STATE.initialized } { + return; + } + // the first three streams are always stdin, stdout, stderr + assert!(unsafe { STATE.stream_cnt } == 0); + IoState::new_stream(if Io::stdin() { + Stream::Host(stdin::get_stdin()) + } else { + Stream::Null + }); + IoState::new_stream(if Io::stdout() { + Stream::Host(stdout::get_stdout()) + } else { + Stream::Null + }); + IoState::new_stream(if Io::stderr() { + Stream::Host(stderr::get_stderr()) + } else { + Stream::Null + }); + assert!(unsafe { STATE.stream_cnt } == 3); + + if Io::host_passthrough() || Io::host_preopens() { + let host_preopen_directories = unsafe { &mut STATE.host_preopen_directories }; + for (fd, name) in preopens::get_directories() { + if Io::host_preopens() { + let fd = IoState::new_descriptor(DescriptorTarget::HostDescriptor(fd)); + let entry = (fd, name.to_string()); + unsafe { STATE.preopen_directories.push(entry) } + } + if Io::host_passthrough() { + host_preopen_directories.insert(name, fd); + } + } + } + + let preopens = Io::preopens(); + for preopen in preopens { + let fd = IoState::new_descriptor(DescriptorTarget::StaticEntry(preopen)); + let entry = (fd, preopen.name().to_string()); + unsafe { STATE.preopen_directories.push(entry) } + } + + unsafe { STATE.initialized = true }; + } + fn get_host_preopen<'a>(path: &'a str) -> Option<(u32, &'a str)> { + let path = if path.starts_with("./") { + &path[2..] + } else { + path + }; + for (preopen_name, fd) in unsafe { &STATE.host_preopen_directories } { + let preopen_name = if preopen_name.starts_with("./") { + &preopen_name[2..] + } else if preopen_name.starts_with(".") { + &preopen_name[1..] + } else { + preopen_name + }; + if path.starts_with(preopen_name) { + // ambient relative + if preopen_name.len() == 0 { + if path.as_bytes()[0] != b'/' { + return Some((*fd, &path)); + } + } else { + // root '/' match + if preopen_name == "/" && path.as_bytes()[0] == b'/' { + return Some((*fd, &path[1..])); + } + // exact match + if preopen_name.len() == path.len() { + return Some((*fd, "")); + } + // normal [x]/ match + if path.as_bytes()[preopen_name.len()] == b'/' { + return Some((*fd, &path[preopen_name.len() + 1..])); + } + } + } + } + None + } + fn new_descriptor(target: DescriptorTarget) -> u32 { + let fd = unsafe { STATE.descriptor_cnt }; + let descriptor = Descriptor { fd, target }; + assert!(unsafe { STATE.descriptor_table.insert(fd, descriptor) }.is_none()); + unsafe { STATE.descriptor_cnt += 1 }; + fd + } + fn get_descriptor<'a>(fd: u32) -> Result<&'a mut Descriptor, ErrorCode> { + match unsafe { STATE.descriptor_table.get_mut(&fd) } { + Some(descriptor) => Ok(descriptor), + None => Err(ErrorCode::BadDescriptor), + } + } + fn new_stream>(stream: S) -> u32 { + let sid = unsafe { STATE.stream_cnt }; + unsafe { STATE.stream_cnt += 1 }; + unsafe { STATE.stream_table.insert(sid, stream.into()) }; + sid + } + fn get_stream<'a>(sid: u32) -> Result<&'a mut Stream, StreamError> { + match unsafe { STATE.stream_table.get_mut(&sid) } { + Some(stream) => Ok(stream), + None => Err(StreamError {}), + } + } +} + +impl Preopens for VirtAdapter { + fn get_directories() -> Vec<(u32, String)> { + IoState::initialize(); + unsafe { &STATE.preopen_directories }.clone() + } +} + +impl Filesystem for VirtAdapter { + fn read_via_stream(fd: u32, offset: u64) -> Result { + match IoState::get_descriptor(fd)?.target { + DescriptorTarget::StaticEntry(_) => { + if offset != 0 { + return Err(ErrorCode::InvalidSeek); + } + Ok(IoState::new_stream(StaticFileStream::new(fd))) + } + DescriptorTarget::HostDescriptor(host_fd) => { + let host_sid = filesystem::read_via_stream(host_fd, offset).map_err(err_map)?; + Ok(IoState::new_stream(Stream::Host(host_sid))) + } + } + } + fn write_via_stream(_: u32, _: u64) -> Result { + Err(ErrorCode::Access) + } + fn append_via_stream(_fd: u32) -> Result { + Err(ErrorCode::Access) + } + fn advise(_: u32, _: u64, _: u64, _: Advice) -> Result<(), ErrorCode> { + todo!() + } + fn sync_data(_: u32) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn get_flags(_fd: u32) -> Result { + Ok(DescriptorFlags::READ) + } + fn get_type(fd: u32) -> Result { + IoState::get_descriptor(fd)?.get_type() + } + fn set_size(_: u32, _: u64) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn set_times(_: u32, _: NewTimestamp, _: NewTimestamp) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn read(fd: u32, len: u64, offset: u64) -> Result<(Vec, bool), ErrorCode> { + let sid = VirtAdapter::read_via_stream(fd, offset)?; + let stream = IoState::get_stream(sid).unwrap(); + let Stream::StaticFile(filestream) = stream else { + unreachable!() + }; + let result = filestream.read(len).map_err(|_| ErrorCode::Io)?; + VirtAdapter::drop_input_stream(sid); + Ok(result) + } + fn write(_: u32, _: Vec, _: u64) -> Result { + Err(ErrorCode::Access) + } + fn read_directory(fd: u32) -> Result { + let descriptor = IoState::get_descriptor(fd)?; + if descriptor.get_type()? != DescriptorType::Directory { + return Err(ErrorCode::NotDirectory); + } + Ok(IoState::new_stream(StaticDirStream::new(fd))) + } + fn sync(_: u32) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn create_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn stat(fd: u32) -> Result { + let descriptor = IoState::get_descriptor(fd)?; + match descriptor.target { + DescriptorTarget::StaticEntry(ptr) => { + let entry = entry(ptr); + Ok(DescriptorStat { + device: 0, + inode: 0, + type_: entry.ty(), + link_count: 0, + size: entry.size()?, + data_access_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + data_modification_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + status_change_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + }) + } + DescriptorTarget::HostDescriptor(host_fd) => { + filesystem::stat(host_fd).map(stat_map).map_err(err_map) + } + } + } + fn stat_at(fd: u32, flags: PathFlags, path: String) -> Result { + let descriptor = IoState::get_descriptor(fd)?; + match descriptor.target { + DescriptorTarget::StaticEntry(ptr) => { + let entry = entry(ptr); + let child = entry.dir_lookup(&path)?; + if matches!( + child.ty, + StaticIndexType::RuntimeDir | StaticIndexType::RuntimeFile + ) { + let Some((host_fd, path)) = IoState::get_host_preopen(child.runtime_path()) + else { + return Err(ErrorCode::NoEntry); + }; + filesystem::stat_at( + host_fd, + filesystem::PathFlags::from_bits(flags.bits()).unwrap(), + &path, + ) + .map(stat_map) + .map_err(err_map) + } else { + Ok(DescriptorStat { + device: 0, + inode: 0, + type_: child.ty(), + link_count: 0, + size: child.size()?, + data_access_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + data_modification_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + status_change_timestamp: Datetime { + seconds: 0, + nanoseconds: 0, + }, + }) + } + } + DescriptorTarget::HostDescriptor(host_fd) => filesystem::stat_at( + host_fd, + filesystem::PathFlags::from_bits(flags.bits()).unwrap(), + &path, + ) + .map(stat_map) + .map_err(err_map), + } + } + fn set_times_at( + _: u32, + _: PathFlags, + _: String, + _: NewTimestamp, + _: NewTimestamp, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn link_at(_: u32, _: PathFlags, _: String, _: u32, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn open_at( + fd: u32, + path_flags: PathFlags, + path: String, + open_flags: OpenFlags, + descriptor_flags: DescriptorFlags, + modes: Modes, + ) -> Result { + let descriptor = IoState::get_descriptor(fd)?; + match descriptor.target { + DescriptorTarget::StaticEntry(ptr) => { + let entry = entry(ptr); + let child = entry.dir_lookup(&path)?; + if matches!( + child.ty, + StaticIndexType::RuntimeDir | StaticIndexType::RuntimeFile + ) { + let Some((host_fd, path)) = IoState::get_host_preopen(child.runtime_path()) + else { + return Err(ErrorCode::NoEntry); + }; + let child_fd = filesystem::open_at( + host_fd, + filesystem::PathFlags::from_bits(path_flags.bits()).unwrap(), + &path, + filesystem::OpenFlags::from_bits(open_flags.bits()).unwrap(), + filesystem::DescriptorFlags::from_bits(descriptor_flags.bits()).unwrap(), + filesystem::Modes::from_bits(modes.bits()).unwrap(), + ) + .map_err(err_map)?; + Ok(IoState::new_descriptor(DescriptorTarget::HostDescriptor( + child_fd, + ))) + } else { + Ok(IoState::new_descriptor(DescriptorTarget::StaticEntry( + child, + ))) + } + } + DescriptorTarget::HostDescriptor(host_fd) => { + let child_fd = filesystem::open_at( + host_fd, + filesystem::PathFlags::from_bits(path_flags.bits()).unwrap(), + &path, + filesystem::OpenFlags::from_bits(open_flags.bits()).unwrap(), + filesystem::DescriptorFlags::from_bits(descriptor_flags.bits()).unwrap(), + filesystem::Modes::from_bits(modes.bits()).unwrap(), + ) + .map_err(err_map)?; + Ok(IoState::new_descriptor(DescriptorTarget::HostDescriptor( + child_fd, + ))) + } + } + } + fn readlink_at(_: u32, _: String) -> Result { + todo!() + } + fn remove_directory_at(_: u32, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn rename_at(_: u32, _: String, _: u32, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn symlink_at(_: u32, _: String, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn access_at(_: u32, _: PathFlags, _: String, _: AccessType) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn unlink_file_at(_: u32, _: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn change_file_permissions_at( + _: u32, + _: PathFlags, + _: String, + _: Modes, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn change_directory_permissions_at( + _: u32, + _: PathFlags, + _: String, + _: Modes, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Access) + } + fn lock_shared(_fd: u32) -> Result<(), ErrorCode> { + Ok(()) + } + fn lock_exclusive(_fd: u32) -> Result<(), ErrorCode> { + Ok(()) + } + fn try_lock_shared(_fd: u32) -> Result<(), ErrorCode> { + Ok(()) + } + fn try_lock_exclusive(_fd: u32) -> Result<(), ErrorCode> { + Ok(()) + } + fn unlock(_: u32) -> Result<(), ErrorCode> { + Ok(()) + } + fn drop_descriptor(fd: u32) { + let Ok(descriptor) = IoState::get_descriptor(fd) else { + return; + }; + descriptor.drop(); + } + fn read_directory_entry(sid: u32) -> Result, ErrorCode> { + match IoState::get_stream(sid).map_err(|_| ErrorCode::BadDescriptor)? { + Stream::StaticDir(dirstream) => dirstream.next(), + Stream::Host(sid) => filesystem::read_directory_entry(*sid) + .map(|e| e.map(dir_map)) + .map_err(err_map), + _ => { + return Err(ErrorCode::BadDescriptor); + } + } + } + fn drop_directory_entry_stream(sid: u32) { + let Ok(stream) = IoState::get_stream(sid) else { + return; + }; + match stream { + Stream::Null | Stream::StaticFile(_) | Stream::StaticDir(_) => {} + Stream::Host(sid) => filesystem::drop_directory_entry_stream(*sid), + } + unsafe { STATE.stream_table.remove(&sid) }; + } +} + +impl Streams for VirtAdapter { + fn read(sid: u32, len: u64) -> Result<(Vec, bool), StreamError> { + VirtAdapter::blocking_read(sid, len) + } + fn blocking_read(sid: u32, len: u64) -> Result<(Vec, bool), StreamError> { + let stream = IoState::get_stream(sid)?; + match stream { + Stream::StaticFile(filestream) => filestream.read(len), + Stream::Host(sid) => streams::blocking_read(*sid, len).map_err(|_| StreamError {}), + Stream::Null => Ok((vec![], true)), + Stream::StaticDir(_) => stream_err(), + } + } + fn skip(sid: u32, offset: u64) -> Result<(u64, bool), StreamError> { + match IoState::get_stream(sid)? { + Stream::Null => Ok((0, true)), + Stream::StaticDir(_) | Stream::StaticFile(_) => stream_err(), + Stream::Host(sid) => streams::skip(*sid, offset).map_err(|_| StreamError {}), + } + } + fn blocking_skip(sid: u32, offset: u64) -> Result<(u64, bool), StreamError> { + match IoState::get_stream(sid)? { + Stream::Null => Ok((0, true)), + Stream::StaticFile(_) | Stream::StaticDir(_) => stream_err(), + Stream::Host(sid) => streams::blocking_skip(*sid, offset).map_err(|_| StreamError {}), + } + } + fn subscribe_to_input_stream(sid: u32) -> u32 { + let Ok(stream) = IoState::get_stream(sid) else { + panic!() + }; + match stream { + Stream::Null => 0, + Stream::StaticFile(_) | Stream::StaticDir(_) => 0, + Stream::Host(sid) => streams::subscribe_to_input_stream(*sid), + } + } + fn drop_input_stream(sid: u32) { + let Ok(stream) = IoState::get_stream(sid) else { + return; + }; + match stream { + Stream::Null | Stream::StaticFile(_) | Stream::StaticDir(_) => {} + Stream::Host(sid) => streams::drop_input_stream(*sid), + } + unsafe { STATE.stream_table.remove(&sid) }; + } + fn write(sid: u32, bytes: Vec) -> Result { + match IoState::get_stream(sid)? { + Stream::Null => Ok(bytes.len() as u64), + Stream::StaticFile(_) | Stream::StaticDir(_) => stream_err(), + Stream::Host(sid) => streams::write(*sid, bytes.as_slice()).map_err(|_| StreamError {}), + } + } + fn blocking_write(sid: u32, bytes: Vec) -> Result { + match IoState::get_stream(sid)? { + Stream::Null => Ok(bytes.len() as u64), + Stream::StaticFile(_) | Stream::StaticDir(_) => stream_err(), + Stream::Host(sid) => streams::write(*sid, bytes.as_slice()).map_err(|_| StreamError {}), + } + } + fn write_zeroes(sid: u32, len: u64) -> Result { + match IoState::get_stream(sid)? { + Stream::Null => Ok(len), + Stream::StaticFile(_) | Stream::StaticDir(_) => stream_err(), + Stream::Host(sid) => streams::write_zeroes(*sid, len).map_err(|_| StreamError {}), + } + } + fn blocking_write_zeroes(sid: u32, len: u64) -> Result { + match IoState::get_stream(sid)? { + Stream::Null => Ok(len), + Stream::StaticFile(_) | Stream::StaticDir(_) => stream_err(), + Stream::Host(sid) => { + streams::blocking_write_zeroes(*sid, len).map_err(|_| StreamError {}) + } + } + } + fn splice(to_sid: u32, from_sid: u32, len: u64) -> Result<(u64, bool), StreamError> { + let to_sid = match IoState::get_stream(to_sid)? { + Stream::Null => { + return Ok((len, true)); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + let from_sid = match IoState::get_stream(from_sid)? { + Stream::Null => { + return Ok((len, true)); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + streams::splice(to_sid, from_sid, len).map_err(|_| StreamError {}) + } + fn blocking_splice(to_sid: u32, from_sid: u32, len: u64) -> Result<(u64, bool), StreamError> { + let to_sid = match IoState::get_stream(to_sid)? { + Stream::Null => { + return Ok((len, true)); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + let from_sid = match IoState::get_stream(from_sid)? { + Stream::Null => { + return Ok((len, true)); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + streams::blocking_splice(to_sid, from_sid, len).map_err(|_| StreamError {}) + } + fn forward(to_sid: u32, from_sid: u32) -> Result { + let to_sid = match IoState::get_stream(to_sid)? { + Stream::Null => { + return Ok(0); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + let from_sid = match IoState::get_stream(from_sid)? { + Stream::Null => { + return Ok(0); + } + Stream::StaticFile(_) | Stream::StaticDir(_) => { + return stream_err(); + } + Stream::Host(sid) => *sid, + }; + streams::forward(to_sid, from_sid).map_err(|_| StreamError {}) + } + fn subscribe_to_output_stream(sid: u32) -> u32 { + let Ok(stream) = IoState::get_stream(sid) else { + panic!(); + }; + match stream { + Stream::Null => 0, + Stream::StaticFile(_) | Stream::StaticDir(_) => 0, + Stream::Host(sid) => streams::subscribe_to_output_stream(*sid), + } + } + fn drop_output_stream(sid: u32) { + let Ok(stream) = IoState::get_stream(sid) else { + return; + }; + match stream { + Stream::Null | Stream::StaticFile(_) | Stream::StaticDir(_) => {} + Stream::Host(sid) => streams::drop_output_stream(*sid), + } + unsafe { STATE.stream_table.remove(&sid) }; + } +} + +fn stream_err() -> Result { + Err(StreamError {}) +} + +// we enforce these descriptor numbers here internally +// then defer to the host descriptor number assignments indirectly +impl Stdin for VirtAdapter { + fn get_stdin() -> u32 { + 0 + } +} + +impl Stdout for VirtAdapter { + fn get_stdout() -> u32 { + 1 + } +} + +impl Stderr for VirtAdapter { + fn get_stderr() -> u32 { + 2 + } +} diff --git a/virtual-adapter/src/lib.rs b/virtual-adapter/src/lib.rs index 0db6e15..aeb265b 100644 --- a/virtual-adapter/src/lib.rs +++ b/virtual-adapter/src/lib.rs @@ -1,7 +1,7 @@ #![no_main] mod env; -mod fs; +mod io; wit_bindgen::generate!({ path: "../wit", diff --git a/wit/virt.wit b/wit/virt.wit index 75f7e82..cf0358a 100644 --- a/wit/virt.wit +++ b/wit/virt.wit @@ -14,6 +14,12 @@ world virtual-adapter { export wasi:cli-base/environment export wasi:filesystem/filesystem export wasi:cli-base/preopens + import wasi:cli-base/stdin + import wasi:cli-base/stdout + import wasi:cli-base/stderr + export wasi:cli-base/stdin + export wasi:cli-base/stdout + export wasi:cli-base/stderr } world virtual-base { @@ -27,13 +33,20 @@ world virtual-env { export wasi:cli-base/environment } -world virtual-fs { +world virtual-io { import wasi:cli-base/preopens import wasi:filesystem/filesystem import wasi:io/streams export wasi:io/streams export wasi:filesystem/filesystem export wasi:cli-base/preopens + import wasi:cli-base/stdin + import wasi:cli-base/stdout + import wasi:cli-base/stderr + export wasi:cli-base/stdin + export wasi:cli-base/stdout + export wasi:cli-base/stderr + import wasi:poll/poll } world virt-test {