From 5ce2a2f2bd11b392abe0a3a967042d89096c5df3 Mon Sep 17 00:00:00 2001 From: Surma Date: Fri, 6 Nov 2026 16:34:33 +0000 Subject: [PATCH] vfs: host-side WASI preview1 over sausage-vfs (T11) Preview1 state lives in the host, so Go modules (esbuild) work. esbuild bundles byte-identical to native; uutils and QuickJS-ng file tests pass. The failed guest-side adapter attempt (T7) moves to spikes/s7-preview1-adapter/. --- spikes/s7-preview1-adapter/README.md | 53 ++ spikes/s7-preview1-adapter/build.nu | 29 + spikes/s7-preview1-adapter/diagnostic.patch | 54 ++ spikes/s7-preview1-adapter/eager-state.patch | 14 + spikes/s7-preview1-adapter/test.nu | 43 + vfs/Cargo.toml | 4 +- vfs/src/lib.rs | 1 + vfs/src/preview1/README.md | 43 + vfs/src/preview1/calls.rs | 605 ++++++++++++++ vfs/src/preview1/mod.rs | 342 ++++++++ vfs/src/preview1/witx/LICENSE | 220 +++++ vfs/src/preview1/witx/typenames.witx | 750 ++++++++++++++++++ .../preview1/witx/wasi_snapshot_preview1.witx | 521 ++++++++++++ vfs/tests/preview1.rs | 360 +++++++++ 14 files changed, 3038 insertions(+), 1 deletion(-) create mode 100644 spikes/s7-preview1-adapter/README.md create mode 100644 spikes/s7-preview1-adapter/build.nu create mode 100644 spikes/s7-preview1-adapter/diagnostic.patch create mode 100644 spikes/s7-preview1-adapter/eager-state.patch create mode 100644 spikes/s7-preview1-adapter/test.nu create mode 100644 vfs/src/preview1/README.md create mode 100644 vfs/src/preview1/calls.rs create mode 100644 vfs/src/preview1/mod.rs create mode 100644 vfs/src/preview1/witx/LICENSE create mode 100644 vfs/src/preview1/witx/typenames.witx create mode 100644 vfs/src/preview1/witx/wasi_snapshot_preview1.witx create mode 100644 vfs/tests/preview1.rs diff --git a/spikes/s7-preview1-adapter/README.md b/spikes/s7-preview1-adapter/README.md new file mode 100644 index 0000000..c147450 --- /dev/null +++ b/spikes/s7-preview1-adapter/README.md @@ -0,0 +1,53 @@ +# Preview1 command adapter experiment + +This directory tests D39 with Go esbuild, uutils, and QuickJS. +The patch reserves the adapter state before the guest calls `_start`. +It retains both state sentinels and all WASI semantics. + +## Build + +From the repository root: + +```sh +nix develop -c nu spikes/s7-preview1-adapter/build.nu +nix develop -c nu spikes/s7-preview1-adapter/test.nu +``` + +The build pins Wasmtime v49.0.2 to commit +`3c8a3e79aa3188a1a12b3aebbaa097abccceac92` and uses its Cargo.lock with `--locked`. +The repository flake pins the Rust toolchain through flake.lock. +The build requires the `wasm32-unknown-unknown` standard library. +T11 removed this target from the production flake after T7 disproved the patch. +To repeat this historical experiment, supply that target in a separate Rust toolchain. +It places sources, a persistent target directory, and the adapter under `/tmp/p1-adapter/`. + +The test uses the S6 esbuild v0.27.7 module at `/tmp/s6-esbuild/esbuild.wasm`. +The S6 build recipe can create this input. +The test copies the S6 fixture into its own scratch directory and installs its locked dependencies. +It compares module and component bundles byte-for-byte and checks rendered elements with LinkeDOM. +It also adapts the lab's preview1 uutils and QuickJS modules. +These smoke tests check command arguments and stdout. +They do not check every utility or JavaScript feature. + +The test requires these existing lab inputs: + +- `/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm` +- `/tmp/wasi-userland-lab/qjs-wasi.wasm` + +## Result + +The eager-state patch does not fix esbuild. +The component traps during `--version` after Go overwrites the adapter state sentinels. +The uutils and QuickJS smoke tests pass. +The task report records the sentinel addresses and values. + +`diagnostic.patch` is an alternative patch against the same pristine Wasmtime revision. +It includes the eager-state change and prints state sentinels before `_start` and upon corruption. +It does not disable assertions. +Do not apply both patches to one source tree. +The diagnostic build uses the same Cargo command as the normal build. +Use `wasm-tools component new` with its resulting adapter to reproduce the trace. + +The eager-state change does not reserve its memory from Go's allocator. +Go starts its allocation cursor at the end of its static data, not at the current memory size. +The host-side adapter is the next planned approach under D39. diff --git a/spikes/s7-preview1-adapter/build.nu b/spikes/s7-preview1-adapter/build.nu new file mode 100644 index 0000000..ddcac45 --- /dev/null +++ b/spikes/s7-preview1-adapter/build.nu @@ -0,0 +1,29 @@ +# Build the command adapter from pinned Wasmtime sources and a local patch. +def main [] { + let root = '/tmp/p1-adapter' + let source = $root | path join wasmtime + let revision = '3c8a3e79aa3188a1a12b3aebbaa097abccceac92' + mkdir $root + if not ($source | path exists) { + ^git clone --depth 1 --branch v49.0.2 https://github.com/bytecodealliance/wasmtime.git $source + if $env.LAST_EXIT_CODE != 0 { error make {msg: 'clone failed'} } + } + if ((^git -C $source rev-parse HEAD | str trim) != $revision) { + error make {msg: 'unexpected Wasmtime revision'} + } + let patch = $env.FILE_PWD | path join eager-state.patch + let applied = (^git -C $source apply --reverse --check $patch | complete) + if $applied.exit_code != 0 { + ^git -C $source apply $patch + if $env.LAST_EXIT_CODE != 0 { error make {msg: 'patch failed'} } + } + cd $source + with-env {CARGO_TARGET_DIR: '/tmp/p1-adapter/target', CARGO_BUILD_JOBS: '2'} { + ^cargo build --locked -p wasi-preview1-component-adapter --target wasm32-unknown-unknown --release --features command --no-default-features + if $env.LAST_EXIT_CODE != 0 { error make {msg: 'adapter build failed'} } + } + cp ($root | path join target wasm32-unknown-unknown release wasi_snapshot_preview1.wasm) ($root | path join adapter-command.wasm) + ^wasm-tools validate ($root | path join adapter-command.wasm) + if $env.LAST_EXIT_CODE != 0 { error make {msg: 'adapter validation failed'} } + print 'PASS patched command adapter build' +} diff --git a/spikes/s7-preview1-adapter/diagnostic.patch b/spikes/s7-preview1-adapter/diagnostic.patch new file mode 100644 index 0000000..de090f0 --- /dev/null +++ b/spikes/s7-preview1-adapter/diagnostic.patch @@ -0,0 +1,54 @@ +diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs +index 7ce52c6..ed23010 100644 +--- a/crates/wasi-preview1-component-adapter/src/lib.rs ++++ b/crates/wasi-preview1-component-adapter/src/lib.rs +@@ -129,6 +129,11 @@ pub extern "C" fn run() -> u32 { + unsafe extern "C" { + safe fn _start(); + } ++ // Reserve adapter state before Go snapshots the linear-memory heap extent. ++ // Lazy memory.grow allocations otherwise overlap Go's sbrk allocator. ++ let state = State::ptr(); ++ eprint!("before _start: "); ++ state.trace(); + _start(); + 0 + } +@@ -2852,8 +2857,24 @@ unsafe extern "C" { + } + + impl State { ++ fn trace(&self) { ++ eprint!("ptr="); ++ crate::macros::eprint_u32(self as *const Self as u32); ++ eprint!(" magic1="); ++ crate::macros::eprint_u32(self.magic1); ++ eprint!(" magic2="); ++ crate::macros::eprint_u32(self.magic2); ++ eprint!(" memory_pages="); ++ crate::macros::eprint_u32(core::arch::wasm32::memory_size::<0>() as u32); ++ eprintln!(""); ++ } ++ + fn with(f: impl FnOnce(&State) -> Result<(), Errno>) -> Errno { + let state_ref = State::ptr(); ++ if state_ref.magic1 != MAGIC || state_ref.magic2 != MAGIC { ++ eprint!("corrupt state: "); ++ state_ref.trace(); ++ } + assert_eq!(state_ref.magic1, MAGIC); + assert_eq!(state_ref.magic2, MAGIC); + let ret = f(state_ref); +diff --git a/crates/wasi-preview1-component-adapter/src/macros.rs b/crates/wasi-preview1-component-adapter/src/macros.rs +index d5649f4..3b5adf4 100644 +--- a/crates/wasi-preview1-component-adapter/src/macros.rs ++++ b/crates/wasi-preview1-component-adapter/src/macros.rs +@@ -40,7 +40,7 @@ pub fn eprint_unreachable(line: u32) { + crate::macros::eprint_u32(line); + } + +-fn eprint_u32(x: u32) { ++pub(crate) fn eprint_u32(x: u32) { + if x == 0 { + eprint!("0"); + } else { diff --git a/spikes/s7-preview1-adapter/eager-state.patch b/spikes/s7-preview1-adapter/eager-state.patch new file mode 100644 index 0000000..40b63b5 --- /dev/null +++ b/spikes/s7-preview1-adapter/eager-state.patch @@ -0,0 +1,14 @@ +diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs +index 7ce52c6..55a8480 100644 +--- a/crates/wasi-preview1-component-adapter/src/lib.rs ++++ b/crates/wasi-preview1-component-adapter/src/lib.rs +@@ -129,6 +129,9 @@ pub extern "C" fn run() -> u32 { + unsafe extern "C" { + safe fn _start(); + } ++ // Reserve adapter state before Go snapshots the linear-memory heap extent. ++ // Lazy memory.grow allocations otherwise overlap Go's sbrk allocator. ++ State::ptr(); + _start(); + 0 + } diff --git a/spikes/s7-preview1-adapter/test.nu b/spikes/s7-preview1-adapter/test.nu new file mode 100644 index 0000000..9d0f083 --- /dev/null +++ b/spikes/s7-preview1-adapter/test.nu @@ -0,0 +1,43 @@ +# Compare esbuild bundles and test preview1 uutils and QuickJS with this adapter. +use std/assert +def checked [program: string, args: list] { + let result = (run-external $program ...$args | complete) + if $result.exit_code != 0 { + print $result.stdout + print --stderr $result.stderr + error make {msg: $'Failed: ($program) ($args | str join " ")'} + } + $result.stdout | str trim +} +def main [] { + let root = '/tmp/p1-adapter' + let adapter = $root | path join adapter-command.wasm + for guest in [ + {name: coreutils, path: '/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm', args: [echo adapter-ok], expected: adapter-ok}, + {name: QuickJS, path: '/tmp/wasi-userland-lab/qjs-wasi.wasm', args: [-e 'console.log(6 * 7)'], expected: '42'} + ] { + let binary = $root | path join $'($guest.name).wasm' + checked wasm-tools [component new $guest.path --adapt $'wasi_snapshot_preview1=($adapter)' -o $binary] | ignore + assert equal (checked wasmtime ([run $binary] | append $guest.args)) $guest.expected + print $'PASS ($guest.name): ($guest.expected)' + } + let fixture = $env.FILE_PWD | path join ../s6-esbuild | path expand + let app = $root | path join app + mkdir $app + cp ($fixture | path join app.tsx) ($fixture | path join check.mjs) ($fixture | path join package.json) ($fixture | path join package-lock.json) $app + cd $app + print (checked npm [ci --ignore-scripts --no-audit --no-fund]) + let flags = [app.tsx --bundle --minify --format=iife --platform=browser --jsx=automatic --jsx-import-source=preact --legal-comments=none --log-level=error] + for variant in [module component] { + let binary = if $variant == module { '/tmp/s6-esbuild/esbuild.wasm' } else { $root | path join esbuild.component.wasm } + if $variant == component { + checked wasm-tools [component new /tmp/s6-esbuild/esbuild.wasm --adapt $'wasi_snapshot_preview1=($adapter)' -o $binary] | ignore + } + assert equal (checked wasmtime [run --dir .::/ $binary --version]) '0.27.7' + checked wasmtime ([run --dir .::/ $binary] | append $flags | append $'--outfile=($variant).js') | ignore + print (checked node [check.mjs $'($variant).js']) + } + assert equal (open --raw module.js) (open --raw component.js) + print 'PASS byte-identical module/component bundles' + print 'PASS all adapter checks' +} diff --git a/vfs/Cargo.toml b/vfs/Cargo.toml index 27cdc51..633b14a 100644 --- a/vfs/Cargo.toml +++ b/vfs/Cargo.toml @@ -11,7 +11,9 @@ cap-fs-ext = "4" wasmtime = "49" wasmtime-wasi = "49" bytes = "1" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros", "io-util", "time"] } +wiggle = "49" +getrandom = "0.3" [dev-dependencies] wasip2 = "1" diff --git a/vfs/src/lib.rs b/vfs/src/lib.rs index b6065d4..03d816c 100644 --- a/vfs/src/lib.rs +++ b/vfs/src/lib.rs @@ -6,6 +6,7 @@ pub mod host; pub mod memory; pub mod mounts; pub mod observe; +pub mod preview1; pub mod process; mod readonly; mod streams; diff --git a/vfs/src/preview1/README.md b/vfs/src/preview1/README.md new file mode 100644 index 0000000..4c86968 --- /dev/null +++ b/vfs/src/preview1/README.md @@ -0,0 +1,43 @@ +# Preview1 over the virtual filesystem + +`State` owns a process's preview1 descriptors outside guest memory. +It uses the caller's `Arc` for every filesystem operation. +The caller can supply the same mount namespace that its components use. +The namespace has one preopen, `/`, at descriptor 3. +Relative paths through descriptor 3 resolve against the process cwd. +Paths through other directory descriptors resolve against that descriptor's virtual path. +The resolver normalizes `..` before the mount namespace routes the operation. + +Construct `State::new(root, cwd, argv, env, stdin, stdout, stderr)`. +The stream types accept boxed Tokio `AsyncRead` and `AsyncWrite` implementations. +The state wraps them with Wasmtime's existing pollable streams. +These streams supply readiness without a guest-memory buffer for adapter state. +Register imports with `add_to_linker(&mut linker, |store| &mut store.preview1)`. +Instantiate the core module asynchronously and call its `_start` export. +A `wasmtime_wasi::I32Exit` error carries the process exit status. + +Wiggle generates the ABI from the copied preview1 WITX files. +Their source is Wasmtime v49.0.2, commit `3c8a3e79aa3188a1a12b3aebbaa097abccceac92`. +Wiggle checks guest pointers before the implementation accesses their contents. +The implementation maps filesystem errors to preview1 errno values. + +The Layer interface does not expose inode identity. +Metadata therefore hashes the virtual path, as the component adapter does. +Directory descriptors retain their original paths after a rename. +Hard links, symlink creation, file preallocation, signals, and sockets return `NOTSUP`. +Preview1 does not expose the platform's WASI 0.2 sockets. + +The integration test uses the existing S6 and lab guests: + +- esbuild v0.27.7: `/tmp/s6-esbuild/esbuild.wasm` and `/tmp/s6-esbuild/esbuild-native`. +- uutils 0.12.0: `/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm`. +- QuickJS-ng v0.18.0: `/tmp/wasi-userland-lab/qjs-wasi.wasm`. + +The QuickJS-ng input matches its recorded release SHA-256. +The S6 fixture also requires `/tmp/s6-esbuild/app/node_modules`. +It stores local Wasmtime compilation artifacts in a persistent test cache. +Run it from the repository root: + +```sh +CARGO_TARGET_DIR=target/preview1 nix develop -c cargo test -p sausage-vfs --test preview1 -- --nocapture +``` diff --git a/vfs/src/preview1/calls.rs b/vfs/src/preview1/calls.rs new file mode 100644 index 0000000..3a238a1 --- /dev/null +++ b/vfs/src/preview1/calls.rs @@ -0,0 +1,605 @@ +//! Preview1 ABI operations over host-owned descriptors and async streams. +use super::*; +use types::*; +impl wasi_snapshot_preview1::WasiSnapshotPreview1 for State { + fn args_get( + &mut self, + m: &mut GuestMemory<'_>, + argv: GuestPtr>, + buf: GuestPtr, + ) -> Result<(), Error> { + strings(m, &self.argv, argv, buf) + } + fn args_sizes_get(&mut self, _: &mut GuestMemory<'_>) -> Result<(Size, Size), Error> { + sizes(&self.argv) + } + fn environ_get( + &mut self, + m: &mut GuestMemory<'_>, + env: GuestPtr>, + buf: GuestPtr, + ) -> Result<(), Error> { + strings(m, &self.env, env, buf) + } + fn environ_sizes_get(&mut self, _: &mut GuestMemory<'_>) -> Result<(Size, Size), Error> { + sizes(&self.env) + } + fn clock_res_get(&mut self, _: &mut GuestMemory<'_>, id: Clockid) -> Result { + self.clock(id)?; + Ok(1) + } + fn clock_time_get( + &mut self, + _: &mut GuestMemory<'_>, + id: Clockid, + _: Timestamp, + ) -> Result { + self.clock(id) + } + async fn fd_advise( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + _: Filesize, + _: Filesize, + _: Advice, + ) -> Result<(), Error> { + self.fd(fd)?; + Ok(()) + } + fn fd_allocate( + &mut self, + _: &mut GuestMemory<'_>, + _: Fd, + _: Filesize, + _: Filesize, + ) -> Result<(), Error> { + Err(Errno::Notsup.into()) + } + async fn fd_close(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result<(), Error> { + self.descriptors.remove(&u32::from(fd)).ok_or(Errno::Badf)?; + Ok(()) + } + async fn fd_datasync(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result<(), Error> { + self.file(fd)?.sync().await?; + Ok(()) + } + async fn fd_sync(&mut self, m: &mut GuestMemory<'_>, fd: Fd) -> Result<(), Error> { + self.fd_datasync(m, fd).await + } + async fn fd_fdstat_get(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result { + let (ty, flags) = if u32::from(fd) <= 2 { + (Filetype::Unknown, Fdflags::empty()) + } else { + let d = self.fd(fd)?; + ( + if d.directory { + Filetype::Directory + } else { + Filetype::RegularFile + }, + d.flags, + ) + }; + Ok(Fdstat { + fs_filetype: ty, + fs_flags: flags, + fs_rights_base: Rights::all(), + fs_rights_inheriting: Rights::all(), + }) + } + fn fd_fdstat_set_flags( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + flags: Fdflags, + ) -> Result<(), Error> { + if u32::from(fd) > 2 { + self.descriptors + .get_mut(&u32::from(fd)) + .ok_or(Errno::Badf)? + .flags = flags; + } + Ok(()) + } + fn fd_fdstat_set_rights( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + _: Rights, + _: Rights, + ) -> Result<(), Error> { + self.fd(fd)?; + Ok(()) + } + async fn fd_filestat_get( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + ) -> Result { + if u32::from(fd) <= 2 { + return Ok(Filestat { + dev: 0, + ino: u32::from(fd) as u64, + filetype: Filetype::Unknown, + nlink: 1, + size: 0, + atim: 0, + mtim: 0, + ctim: 0, + }); + } + let d = self.fd(fd)?.clone(); + Ok(stat( + if let Some(file) = d.file { + file.stat().await? + } else { + self.root.stat(&d.path, true).await? + }, + &d.path, + )) + } + async fn fd_filestat_set_size( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + size: Filesize, + ) -> Result<(), Error> { + self.file(fd)?.set_size(size).await?; + Ok(()) + } + async fn fd_filestat_set_times( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + atim: Timestamp, + mtim: Timestamp, + flags: Fstflags, + ) -> Result<(), Error> { + let (a, m) = times(atim, mtim, flags); + self.file(fd)?.set_times(a, m).await?; + Ok(()) + } + async fn fd_read( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + iovs: IovecArray, + ) -> Result { + self.read(m, fd, iovs, None).await + } + async fn fd_pread( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + iovs: IovecArray, + offset: Filesize, + ) -> Result { + self.read(m, fd, iovs, Some(offset)).await + } + async fn fd_write( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + iovs: CiovecArray, + ) -> Result { + self.write(m, fd, iovs, None).await + } + async fn fd_pwrite( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + iovs: CiovecArray, + offset: Filesize, + ) -> Result { + self.write(m, fd, iovs, Some(offset)).await + } + fn fd_prestat_get(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result { + if u32::from(fd) != 3 { + return Err(Errno::Badf.into()); + } + Ok(Prestat::Dir(PrestatDir { pr_name_len: 1 })) + } + fn fd_prestat_dir_name( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + path: GuestPtr, + len: Size, + ) -> Result<(), Error> { + if u32::from(fd) != 3 { + return Err(Errno::Badf.into()); + } + if len < 1 { + return Err(Errno::Nametoolong.into()); + } + m.write(path, b'/')?; + Ok(()) + } + async fn fd_renumber( + &mut self, + _: &mut GuestMemory<'_>, + from: Fd, + to: Fd, + ) -> Result<(), Error> { + let d = self + .descriptors + .remove(&u32::from(from)) + .ok_or(Errno::Badf)?; + self.descriptors.insert(u32::from(to), d); + Ok(()) + } + async fn fd_seek( + &mut self, + _: &mut GuestMemory<'_>, + fd: Fd, + offset: Filedelta, + whence: Whence, + ) -> Result { + if u32::from(fd) <= 2 { + return Err(Errno::Spipe.into()); + } + let base = match whence { + Whence::Set => 0, + Whence::Cur => self.fd(fd)?.offset, + Whence::End => self.file(fd)?.stat().await?.size, + }; + let value = base.checked_add_signed(offset).ok_or(Errno::Inval)?; + self.descriptors + .get_mut(&u32::from(fd)) + .ok_or(Errno::Badf)? + .offset = value; + Ok(value) + } + fn fd_tell(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result { + Ok(self.fd(fd)?.offset) + } + async fn fd_readdir( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + buf: GuestPtr, + len: Size, + cookie: Dircookie, + ) -> Result { + let d = self.fd(fd)?; + if !d.directory { + return Err(Errno::Notdir.into()); + } + let mut entries = vec![ + crate::Entry { + name: ".".into(), + directory: true, + symlink: false, + }, + crate::Entry { + name: "..".into(), + directory: true, + symlink: false, + }, + ]; + entries.extend(self.root.list(&d.path).await?); + let mut bytes = Vec::new(); + for (i, entry) in entries.iter().enumerate().skip(cookie as usize) { + // Preview1 dirent: next cookie, inode, name length, type, padding. + bytes.extend_from_slice(&((i + 1) as u64).to_le_bytes()); + bytes.extend_from_slice(&((i + 1) as u64).to_le_bytes()); + bytes.extend_from_slice(&(entry.name.len() as u32).to_le_bytes()); + bytes.push(if entry.symlink { + 7 + } else if entry.directory { + 3 + } else { + 4 + }); + bytes.extend_from_slice(&[0; 3]); + bytes.extend_from_slice(entry.name.as_bytes()); + if bytes.len() >= len as usize { + break; + } + } + bytes.truncate(len as usize); + m.copy_from_slice(&bytes, buf.as_array(bytes.len() as u32))?; + Ok(bytes.len() as u32) + } + async fn path_create_directory( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + path: GuestPtr, + ) -> Result<(), Error> { + self.root.mkdir(&self.path(m, fd, path)?).await?; + Ok(()) + } + async fn path_filestat_get( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + flags: Lookupflags, + path: GuestPtr, + ) -> Result { + let path = self.path(m, fd, path)?; + Ok(stat( + self.root + .stat(&path, flags.contains(Lookupflags::SYMLINK_FOLLOW)) + .await?, + &path, + )) + } + async fn path_filestat_set_times( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + flags: Lookupflags, + path: GuestPtr, + atim: Timestamp, + mtim: Timestamp, + f: Fstflags, + ) -> Result<(), Error> { + let path = self.path(m, fd, path)?; + let file = self + .root + .open( + &path, + Open { + follow: flags.contains(Lookupflags::SYMLINK_FOLLOW), + ..Open::default() + }, + ) + .await?; + let (a, b) = times(atim, mtim, f); + file.set_times(a, b).await?; + Ok(()) + } + async fn path_open( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + flags: Lookupflags, + path: GuestPtr, + oflags: Oflags, + rights: Rights, + _: Rights, + fdflags: Fdflags, + ) -> Result { + let path = self.path(m, fd, path)?; + let file = self + .root + .open( + &path, + Open { + follow: flags.contains(Lookupflags::SYMLINK_FOLLOW), + write: rights.contains(Rights::FD_WRITE), + create: oflags.contains(Oflags::CREAT), + exclusive: oflags.contains(Oflags::EXCL), + truncate: oflags.contains(Oflags::TRUNC), + directory: oflags.contains(Oflags::DIRECTORY), + }, + ) + .await?; + let directory = file.stat().await?.directory; + let next = (4..) + .find(|n| !self.descriptors.contains_key(n)) + .ok_or(Errno::Mfile)?; + self.descriptors.insert( + next, + Descriptor { + path, + file: Some(file), + directory, + offset: 0, + flags: fdflags, + }, + ); + Ok(next.into()) + } + async fn path_readlink( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + path: GuestPtr, + buf: GuestPtr, + len: Size, + ) -> Result { + let target = self.root.readlink(&self.path(m, fd, path)?).await?; + let bytes = &target.as_bytes()[..target.len().min(len as usize)]; + m.copy_from_slice(bytes, buf.as_array(bytes.len() as u32))?; + Ok(bytes.len() as u32) + } + async fn path_remove_directory( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + path: GuestPtr, + ) -> Result<(), Error> { + self.root.remove_dir(&self.path(m, fd, path)?).await?; + Ok(()) + } + async fn path_unlink_file( + &mut self, + m: &mut GuestMemory<'_>, + fd: Fd, + path: GuestPtr, + ) -> Result<(), Error> { + self.root.remove_file(&self.path(m, fd, path)?).await?; + Ok(()) + } + async fn path_rename( + &mut self, + m: &mut GuestMemory<'_>, + from: Fd, + src: GuestPtr, + to: Fd, + dst: GuestPtr, + ) -> Result<(), Error> { + self.root + .rename(&self.path(m, from, src)?, &self.path(m, to, dst)?) + .await?; + Ok(()) + } + async fn path_link( + &mut self, + _: &mut GuestMemory<'_>, + _: Fd, + _: Lookupflags, + _: GuestPtr, + _: Fd, + _: GuestPtr, + ) -> Result<(), Error> { + Err(Errno::Notsup.into()) + } + async fn path_symlink( + &mut self, + _: &mut GuestMemory<'_>, + _: GuestPtr, + _: Fd, + _: GuestPtr, + ) -> Result<(), Error> { + Err(Errno::Notsup.into()) + } + async fn poll_oneoff( + &mut self, + m: &mut GuestMemory<'_>, + subs: GuestPtr, + events: GuestPtr, + count: Size, + ) -> Result { + if count == 0 { + return Err(Errno::Inval.into()); + } + let mut pending = Vec::new(); + for ptr in subs.as_array(count).iter() { + let sub = m.read(ptr?)?; + let (delay, stream, ty, error) = match sub.u { + SubscriptionU::Clock(c) => { + let now = self.clock(c.id)?; + let delay = if c.flags.contains(Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME) { + c.timeout.saturating_sub(now) + } else { + c.timeout + }; + (Some(delay), None, Eventtype::Clock, Errno::Success) + } + SubscriptionU::FdRead(f) => { + let fd = u32::from(f.file_descriptor); + if fd == 0 { + (None, Some(fd), Eventtype::FdRead, Errno::Success) + } else { + ( + Some(0), + None, + Eventtype::FdRead, + if self.fd(f.file_descriptor).is_ok() { + Errno::Success + } else { + Errno::Badf + }, + ) + } + } + SubscriptionU::FdWrite(f) => { + let fd = u32::from(f.file_descriptor); + if fd == 1 || fd == 2 { + (None, Some(fd), Eventtype::FdWrite, Errno::Success) + } else { + ( + Some(0), + None, + Eventtype::FdWrite, + if self.fd(f.file_descriptor).is_ok() { + Errno::Success + } else { + Errno::Badf + }, + ) + } + } + }; + pending.push(( + delay, + stream, + Event { + userdata: sub.userdata, + error, + type_: ty, + fd_readwrite: EventFdReadwrite { + nbytes: 1, + flags: Eventrwflags::empty(), + }, + }, + )); + } + let delay = pending.iter().filter_map(|(d, _, _)| *d).min(); + let has = |fd| pending.iter().any(|(_, s, _)| *s == Some(fd)); + let wait_start = Instant::now(); + let stream = tokio::select! { + _ = self.stdin.ready(), if has(0) => Some(0), + _ = self.stdout.ready(), if has(1) => Some(1), + _ = self.stderr.ready(), if has(2) => Some(2), + _ = tokio::time::sleep(Duration::from_nanos(delay.unwrap_or(0))), if delay.is_some() => None, + }; + let elapsed = wait_start.elapsed().as_nanos() as u64; + let mut written = 0; + for (delay, fd, mut event) in pending { + if delay.is_some_and(|d| d <= elapsed) || fd.is_some() && fd == stream { + if fd == Some(0) && matches!(self.stdin.read(0), Err(StreamError::Closed)) { + event.fd_readwrite.nbytes = 0; + event.fd_readwrite.flags = Eventrwflags::FD_READWRITE_HANGUP; + } + if fd == Some(1) && self.stdout.check_write().is_err() + || fd == Some(2) && self.stderr.check_write().is_err() + { + event.error = Errno::Io; + } + m.write(events.add(written)?, event)?; + written += 1; + } + } + Ok(written) + } + fn proc_exit(&mut self, _: &mut GuestMemory<'_>, status: Exitcode) -> wasmtime::Error { + wasmtime_wasi::I32Exit(status as i32).into() + } + fn proc_raise(&mut self, _: &mut GuestMemory<'_>, _: Signal) -> Result<(), Error> { + Err(Errno::Notsup.into()) + } + fn sched_yield(&mut self, _: &mut GuestMemory<'_>) -> Result<(), Error> { + Ok(()) + } + fn random_get( + &mut self, + m: &mut GuestMemory<'_>, + buf: GuestPtr, + len: Size, + ) -> Result<(), Error> { + let _ = m.as_slice(buf.as_array(len))?; + let mut bytes = vec![0; len as usize]; + getrandom::fill(&mut bytes).map_err(|_| Errno::Io)?; + m.copy_from_slice(&bytes, buf.as_array(len))?; + Ok(()) + } + fn sock_accept(&mut self, _: &mut GuestMemory<'_>, _: Fd, _: Fdflags) -> Result { + Err(Errno::Notsup.into()) + } + fn sock_recv( + &mut self, + _: &mut GuestMemory<'_>, + _: Fd, + _: IovecArray, + _: Riflags, + ) -> Result<(Size, Roflags), Error> { + Err(Errno::Notsup.into()) + } + fn sock_send( + &mut self, + _: &mut GuestMemory<'_>, + _: Fd, + _: CiovecArray, + _: Siflags, + ) -> Result { + Err(Errno::Notsup.into()) + } + fn sock_shutdown(&mut self, _: &mut GuestMemory<'_>, _: Fd, _: Sdflags) -> Result<(), Error> { + Err(Errno::Notsup.into()) + } +} diff --git a/vfs/src/preview1/mod.rs b/vfs/src/preview1/mod.rs new file mode 100644 index 0000000..bc078f9 --- /dev/null +++ b/vfs/src/preview1/mod.rs @@ -0,0 +1,342 @@ +//! Host-owned preview1 descriptors and WASI calls over a process filesystem layer. +//! Guests retain only their ABI buffers. The host owns all descriptor state. +#![allow( + clippy::too_many_arguments, + reason = "The preview1 ABI fixes the generated parameter lists." +)] +use crate::{File, Layer, Open, Stat, resolve}; +use std::{ + collections::BTreeMap, + io, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; +use wasmtime_wasi::p2::{ + DynInputStream, DynOutputStream, StreamError, + pipe::{AsyncReadStream, AsyncWriteStream}, +}; +use wiggle::{GuestMemory, GuestPtr}; +mod calls; + +wiggle::from_witx!({ + witx: ["src/preview1/witx/wasi_snapshot_preview1.witx"], + async: { wasi_snapshot_preview1::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, + fd_filestat_set_size, fd_filestat_set_times, fd_read, fd_pread, fd_seek, + fd_sync, fd_readdir, fd_write, fd_pwrite, poll_oneoff, path_create_directory, + path_filestat_get, path_filestat_set_times, path_link, path_open, + path_readlink, path_remove_directory, path_rename, path_symlink, + path_unlink_file, fd_renumber + } }, + errors: { errno => trappable Error }, +}); + +/// Async input supplied by the process table. +pub type Input = Box; +/// Async output supplied by the process table. +pub type Output = Box; +/// A process context with one preopen `/` and a virtual current directory. +pub struct State { + root: Arc, + cwd: String, + argv: Vec, + env: Vec, + stdin: DynInputStream, + stdout: DynOutputStream, + stderr: DynOutputStream, + descriptors: BTreeMap, + start: Instant, +} +#[derive(Clone)] +struct Descriptor { + path: String, + file: Option>, + offset: u64, + flags: types::Fdflags, + directory: bool, +} +impl State { + pub fn new( + root: Arc, + cwd: &str, + argv: Vec, + env: Vec<(String, String)>, + stdin: Input, + stdout: Output, + stderr: Output, + ) -> Self { + let descriptors = BTreeMap::from([( + 3, + Descriptor { + path: "/".into(), + file: None, + offset: 0, + flags: types::Fdflags::empty(), + directory: true, + }, + )]); + Self { + root, + cwd: resolve("/", cwd), + argv, + env: env.into_iter().map(|(k, v)| format!("{k}={v}")).collect(), + stdin: Box::new(AsyncReadStream::new(stdin)), + stdout: Box::new(AsyncWriteStream::new(8192, stdout)), + stderr: Box::new(AsyncWriteStream::new(8192, stderr)), + descriptors, + start: Instant::now(), + } + } + fn fd(&self, fd: types::Fd) -> Result<&Descriptor, types::Error> { + self.descriptors + .get(&u32::from(fd)) + .ok_or_else(|| types::Errno::Badf.into()) + } + fn file(&self, fd: types::Fd) -> Result, types::Error> { + self.fd(fd)? + .file + .clone() + .ok_or_else(|| types::Errno::Isdir.into()) + } + fn path( + &self, + memory: &GuestMemory<'_>, + fd: types::Fd, + path: GuestPtr, + ) -> Result { + let descriptor = self.fd(fd)?; + if !descriptor.directory { + return Err(types::Errno::Notdir.into()); + } + let path = memory.as_str(path)?.ok_or(types::Errno::Fault)?.to_owned(); + // The root preopen resolves relative paths against the process cwd. + let base = if u32::from(fd) == 3 { + &self.cwd + } else { + &descriptor.path + }; + Ok(resolve(base, &path)) + } + fn clock(&self, id: types::Clockid) -> Result { + match id { + types::Clockid::Realtime => Ok(SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| types::Errno::Overflow)? + .as_nanos() as u64), + types::Clockid::Monotonic => Ok(self.start.elapsed().as_nanos() as u64), + _ => Err(types::Errno::Inval.into()), + } + } + async fn read( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + iovs: types::IovecArray, + position: Option, + ) -> Result { + let mut total = 0; + let mut offset = position.unwrap_or_else(|| self.fd(fd).map(|d| d.offset).unwrap_or(0)); + for ptr in iovs.iter() { + let iov = memory.read(ptr?)?; + if iov.buf_len == 0 { + continue; + } + let _ = memory.as_slice(iov.buf.as_array(iov.buf_len))?; + let bytes = if u32::from(fd) == 0 { + match self.stdin.blocking_read(iov.buf_len as usize).await { + Ok(bytes) => bytes.to_vec(), + Err(StreamError::Closed) => vec![], + Err(error) => return Err(error.into()), + } + } else { + self.file(fd)?.read(offset, iov.buf_len as usize).await? + }; + memory.copy_from_slice(&bytes, iov.buf.as_array(bytes.len() as u32))?; + total += bytes.len() as u32; + offset += bytes.len() as u64; + if bytes.len() < iov.buf_len as usize { + break; + } + } + if position.is_none() && u32::from(fd) > 2 { + self.descriptors + .get_mut(&u32::from(fd)) + .ok_or(types::Errno::Badf)? + .offset = offset; + } + Ok(total) + } + async fn write( + &mut self, + memory: &GuestMemory<'_>, + fd: types::Fd, + iovs: types::CiovecArray, + position: Option, + ) -> Result { + let mut total = 0; + let mut offset = position.unwrap_or_else(|| self.fd(fd).map(|d| d.offset).unwrap_or(0)); + for ptr in iovs.iter() { + let iov = memory.read(ptr?)?; + let bytes = memory.to_vec(iov.buf.as_array(iov.buf_len))?; + let n = match u32::from(fd) { + 1 => { + let len = bytes.len(); + self.stdout.blocking_write_and_flush(bytes.into()).await?; + len + } + 2 => { + let len = bytes.len(); + self.stderr.blocking_write_and_flush(bytes.into()).await?; + len + } + _ => { + let descriptor = self.fd(fd)?; + if position.is_none() && descriptor.flags.contains(types::Fdflags::APPEND) { + self.file(fd)?.append(bytes).await? + } else { + self.file(fd)?.write(offset, bytes).await? + } + } + }; + total += n as u32; + offset += n as u64; + if n < iov.buf_len as usize { + break; + } + } + if position.is_none() && u32::from(fd) > 2 { + self.descriptors + .get_mut(&u32::from(fd)) + .ok_or(types::Errno::Badf)? + .offset = offset; + } + Ok(total) + } +} +/// Add async preview1 imports to a core-module linker. +pub fn add_to_linker( + linker: &mut wasmtime::Linker, + accessor: impl Fn(&mut T) -> &mut State + Copy + Send + Sync + 'static, +) -> wasmtime::Result<()> { + wasi_snapshot_preview1::add_to_linker(linker, accessor) +} +impl wiggle::GuestErrorType for types::Errno { + fn success() -> Self { + Self::Success + } +} +impl From for types::Error { + fn from(_: wiggle::GuestError) -> Self { + types::Errno::Fault.into() + } +} +impl From for types::Error { + fn from(error: StreamError) -> Self { + match error { + StreamError::Closed => types::Errno::Pipe.into(), + StreamError::LastOperationFailed(error) => match error.downcast::() { + Ok(error) => error.into(), + Err(error) => types::Error::trap(error), + }, + StreamError::Trap(error) => types::Error::trap(error), + } + } +} +impl From for types::Error { + fn from(e: io::Error) -> Self { + errno(&e).into() + } +} +fn errno(e: &io::Error) -> types::Errno { + use io::ErrorKind::*; + match e.kind() { + NotFound => types::Errno::Noent, + AlreadyExists => types::Errno::Exist, + PermissionDenied => types::Errno::Acces, + NotADirectory => types::Errno::Notdir, + IsADirectory => types::Errno::Isdir, + DirectoryNotEmpty => types::Errno::Notempty, + InvalidInput => types::Errno::Inval, + Unsupported => types::Errno::Notsup, + ReadOnlyFilesystem => types::Errno::Rofs, + CrossesDevices => types::Errno::Xdev, + WouldBlock => types::Errno::Again, + Interrupted => types::Errno::Intr, + _ => types::Errno::Io, + } +} +fn filetype(s: &Stat) -> types::Filetype { + if s.symlink { + types::Filetype::SymbolicLink + } else if s.directory { + types::Filetype::Directory + } else { + types::Filetype::RegularFile + } +} +fn timestamp(t: Option) -> u64 { + t.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) +} +fn stat(s: Stat, path: &str) -> types::Filestat { + use std::hash::{Hash, Hasher}; + let mut hash = std::collections::hash_map::DefaultHasher::new(); + path.hash(&mut hash); + types::Filestat { + dev: 1, + ino: hash.finish(), + filetype: filetype(&s), + nlink: 1, + size: s.size, + atim: timestamp(s.accessed), + mtim: timestamp(s.modified), + ctim: timestamp(s.modified), + } +} +fn times(atim: u64, mtim: u64, flags: types::Fstflags) -> (Option, Option) { + let time = |value, set, now| { + if flags.contains(now) { + Some(SystemTime::now()) + } else if flags.contains(set) { + SystemTime::UNIX_EPOCH.checked_add(Duration::from_nanos(value)) + } else { + None + } + }; + ( + time(atim, types::Fstflags::ATIM, types::Fstflags::ATIM_NOW), + time(mtim, types::Fstflags::MTIM, types::Fstflags::MTIM_NOW), + ) +} +fn strings( + memory: &mut GuestMemory<'_>, + values: &[String], + mut ptr: GuestPtr>, + mut buf: GuestPtr, +) -> Result<(), types::Error> { + for value in values { + memory.write(ptr, buf)?; + ptr = ptr.add(1)?; + let mut bytes = value.as_bytes().to_vec(); + bytes.push(0); + memory.copy_from_slice(&bytes, buf.as_array(bytes.len() as u32))?; + buf = buf.add(bytes.len() as u32)?; + } + Ok(()) +} +fn sizes(values: &[String]) -> Result<(u32, u32), types::Error> { + Ok(( + values + .len() + .try_into() + .map_err(|_| types::Errno::Overflow)?, + values + .iter() + .map(|s| s.len() + 1) + .sum::() + .try_into() + .map_err(|_| types::Errno::Overflow)?, + )) +} diff --git a/vfs/src/preview1/witx/LICENSE b/vfs/src/preview1/witx/LICENSE new file mode 100644 index 0000000..f9d8195 --- /dev/null +++ b/vfs/src/preview1/witx/LICENSE @@ -0,0 +1,220 @@ + + 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. + diff --git a/vfs/src/preview1/witx/typenames.witx b/vfs/src/preview1/witx/typenames.witx new file mode 100644 index 0000000..82ea276 --- /dev/null +++ b/vfs/src/preview1/witx/typenames.witx @@ -0,0 +1,750 @@ +;; Type names used by low-level WASI interfaces. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/blob/main/legacy/tools/witx-docs.md) +;; for an explanation of what that means. + +(typename $size u32) + +;;; Non-negative file size or length of a region within a file. +(typename $filesize u64) + +;;; Timestamp in nanoseconds. +(typename $timestamp u64) + +;;; Identifiers for clocks. +(typename $clockid + (enum (@witx tag u32) + ;;; The clock measuring real time. Time value zero corresponds with + ;;; 1970-01-01T00:00:00Z. + $realtime + ;;; The store-wide monotonic clock, which is defined as a clock measuring + ;;; real time, whose value cannot be adjusted and which cannot have negative + ;;; clock jumps. The epoch of this clock is undefined. The absolute time + ;;; value of this clock therefore has no meaning. + $monotonic + ;;; The CPU-time clock associated with the current process. + $process_cputime_id + ;;; The CPU-time clock associated with the current thread. + $thread_cputime_id + ) +) + +;;; Error codes returned by functions. +;;; Not all of these error codes are returned by the functions provided by this +;;; API; some are used in higher-level library layers, and others are provided +;;; merely for alignment with POSIX. +(typename $errno + (enum (@witx tag u16) + ;;; No error occurred. System call completed successfully. + $success + ;;; Argument list too long. + $2big + ;;; Permission denied. + $acces + ;;; Address in use. + $addrinuse + ;;; Address not available. + $addrnotavail + ;;; Address family not supported. + $afnosupport + ;;; Resource unavailable, or operation would block. + $again + ;;; Connection already in progress. + $already + ;;; Bad file descriptor. + $badf + ;;; Bad message. + $badmsg + ;;; Device or resource busy. + $busy + ;;; Operation canceled. + $canceled + ;;; No child processes. + $child + ;;; Connection aborted. + $connaborted + ;;; Connection refused. + $connrefused + ;;; Connection reset. + $connreset + ;;; Resource deadlock would occur. + $deadlk + ;;; Destination address required. + $destaddrreq + ;;; Mathematics argument out of domain of function. + $dom + ;;; Reserved. + $dquot + ;;; File exists. + $exist + ;;; Bad address. + $fault + ;;; File too large. + $fbig + ;;; Host is unreachable. + $hostunreach + ;;; Identifier removed. + $idrm + ;;; Illegal byte sequence. + $ilseq + ;;; Operation in progress. + $inprogress + ;;; Interrupted function. + $intr + ;;; Invalid argument. + $inval + ;;; I/O error. + $io + ;;; Socket is connected. + $isconn + ;;; Is a directory. + $isdir + ;;; Too many levels of symbolic links. + $loop + ;;; File descriptor value too large. + $mfile + ;;; Too many links. + $mlink + ;;; Message too large. + $msgsize + ;;; Reserved. + $multihop + ;;; Filename too long. + $nametoolong + ;;; Network is down. + $netdown + ;;; Connection aborted by network. + $netreset + ;;; Network unreachable. + $netunreach + ;;; Too many files open in system. + $nfile + ;;; No buffer space available. + $nobufs + ;;; No such device. + $nodev + ;;; No such file or directory. + $noent + ;;; Executable file format error. + $noexec + ;;; No locks available. + $nolck + ;;; Reserved. + $nolink + ;;; Not enough space. + $nomem + ;;; No message of the desired type. + $nomsg + ;;; Protocol not available. + $noprotoopt + ;;; No space left on device. + $nospc + ;;; Function not supported. + $nosys + ;;; The socket is not connected. + $notconn + ;;; Not a directory or a symbolic link to a directory. + $notdir + ;;; Directory not empty. + $notempty + ;;; State not recoverable. + $notrecoverable + ;;; Not a socket. + $notsock + ;;; Not supported, or operation not supported on socket. + $notsup + ;;; Inappropriate I/O control operation. + $notty + ;;; No such device or address. + $nxio + ;;; Value too large to be stored in data type. + $overflow + ;;; Previous owner died. + $ownerdead + ;;; Operation not permitted. + $perm + ;;; Broken pipe. + $pipe + ;;; Protocol error. + $proto + ;;; Protocol not supported. + $protonosupport + ;;; Protocol wrong type for socket. + $prototype + ;;; Result too large. + $range + ;;; Read-only file system. + $rofs + ;;; Invalid seek. + $spipe + ;;; No such process. + $srch + ;;; Reserved. + $stale + ;;; Connection timed out. + $timedout + ;;; Text file busy. + $txtbsy + ;;; Cross-device link. + $xdev + ;;; Extension: Capabilities insufficient. + $notcapable + ) +) + +;;; File descriptor rights, determining which actions may be performed. +(typename $rights + (flags (@witx repr u64) + ;;; The right to invoke `fd_datasync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::dsync`. + $fd_datasync + ;;; The right to invoke `fd_read` and `sock_recv`. + ;; + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + $fd_read + ;;; The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + $fd_seek + ;;; The right to invoke `fd_fdstat_set_flags`. + $fd_fdstat_set_flags + ;;; The right to invoke `fd_sync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::rsync` and `fdflags::dsync`. + $fd_sync + ;;; The right to invoke `fd_seek` in such a way that the file offset + ;;; remains unaltered (i.e., `whence::cur` with offset zero), or to + ;;; invoke `fd_tell`. + $fd_tell + ;;; The right to invoke `fd_write` and `sock_send`. + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + $fd_write + ;;; The right to invoke `fd_advise`. + $fd_advise + ;;; The right to invoke `fd_allocate`. + $fd_allocate + ;;; The right to invoke `path_create_directory`. + $path_create_directory + ;;; If `path_open` is set, the right to invoke `path_open` with `oflags::creat`. + $path_create_file + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; source directory. + $path_link_source + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; target directory. + $path_link_target + ;;; The right to invoke `path_open`. + $path_open + ;;; The right to invoke `fd_readdir`. + $fd_readdir + ;;; The right to invoke `path_readlink`. + $path_readlink + ;;; The right to invoke `path_rename` with the file descriptor as the source directory. + $path_rename_source + ;;; The right to invoke `path_rename` with the file descriptor as the target directory. + $path_rename_target + ;;; The right to invoke `path_filestat_get`. + $path_filestat_get + ;;; The right to change a file's size (there is no `path_filestat_set_size`). + ;;; If `path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + $path_filestat_set_size + ;;; The right to invoke `path_filestat_set_times`. + $path_filestat_set_times + ;;; The right to invoke `fd_filestat_get`. + $fd_filestat_get + ;;; The right to invoke `fd_filestat_set_size`. + $fd_filestat_set_size + ;;; The right to invoke `fd_filestat_set_times`. + $fd_filestat_set_times + ;;; The right to invoke `path_symlink`. + $path_symlink + ;;; The right to invoke `path_remove_directory`. + $path_remove_directory + ;;; The right to invoke `path_unlink_file`. + $path_unlink_file + ;;; If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + ;;; If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + $poll_fd_readwrite + ;;; The right to invoke `sock_shutdown`. + $sock_shutdown + ;;; The right to invoke `sock_accept`. + $sock_accept + ) +) + +;;; A file descriptor handle. +(typename $fd (handle)) + +;;; A region of memory for scatter/gather reads. +(typename $iovec + (record + ;;; The address of the buffer to be filled. + (field $buf (@witx pointer u8)) + ;;; The length of the buffer to be filled. + (field $buf_len $size) + ) +) + +;;; A region of memory for scatter/gather writes. +(typename $ciovec + (record + ;;; The address of the buffer to be written. + (field $buf (@witx const_pointer u8)) + ;;; The length of the buffer to be written. + (field $buf_len $size) + ) +) + +(typename $iovec_array (list $iovec)) +(typename $ciovec_array (list $ciovec)) + +;;; Relative offset within a file. +(typename $filedelta s64) + +;;; The position relative to which to set the offset of the file descriptor. +(typename $whence + (enum (@witx tag u8) + ;;; Seek relative to start-of-file. + $set + ;;; Seek relative to current position. + $cur + ;;; Seek relative to end-of-file. + $end + ) +) + +;;; A reference to the offset of a directory entry. +;;; +;;; The value 0 signifies the start of the directory. +(typename $dircookie u64) + +;;; The type for the `dirent::d_namlen` field of `dirent` struct. +(typename $dirnamlen u32) + +;;; File serial number that is unique within its file system. +(typename $inode u64) + +;;; The type of a file descriptor or file. +(typename $filetype + (enum (@witx tag u8) + ;;; The type of the file descriptor or file is unknown or is different from any of the other types specified. + $unknown + ;;; The file descriptor or file refers to a block device inode. + $block_device + ;;; The file descriptor or file refers to a character device inode. + $character_device + ;;; The file descriptor or file refers to a directory inode. + $directory + ;;; The file descriptor or file refers to a regular file inode. + $regular_file + ;;; The file descriptor or file refers to a datagram socket. + $socket_dgram + ;;; The file descriptor or file refers to a byte-stream socket. + $socket_stream + ;;; The file refers to a symbolic link inode. + $symbolic_link + ) +) + +;;; A directory entry. +(typename $dirent + (record + ;;; The offset of the next directory entry stored in this directory. + (field $d_next $dircookie) + ;;; The serial number of the file referred to by this directory entry. + (field $d_ino $inode) + ;;; The length of the name of the directory entry. + (field $d_namlen $dirnamlen) + ;;; The type of the file referred to by this directory entry. + (field $d_type $filetype) + ) +) + +;;; File or memory access pattern advisory information. +(typename $advice + (enum (@witx tag u8) + ;;; The application has no advice to give on its behavior with respect to the specified data. + $normal + ;;; The application expects to access the specified data sequentially from lower offsets to higher offsets. + $sequential + ;;; The application expects to access the specified data in a random order. + $random + ;;; The application expects to access the specified data in the near future. + $willneed + ;;; The application expects that it will not access the specified data in the near future. + $dontneed + ;;; The application expects to access the specified data once and then not reuse it thereafter. + $noreuse + ) +) + +;;; File descriptor flags. +(typename $fdflags + (flags (@witx repr u16) + ;;; Append mode: Data written to the file is always appended to the file's end. + $append + ;;; Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + $dsync + ;;; Non-blocking mode. + $nonblock + ;;; Synchronized read I/O operations. + $rsync + ;;; Write according to synchronized I/O file integrity completion. In + ;;; addition to synchronizing the data stored in the file, the implementation + ;;; may also synchronously update the file's metadata. + $sync + ) +) + +;;; File descriptor attributes. +(typename $fdstat + (record + ;;; File type. + (field $fs_filetype $filetype) + ;;; File descriptor flags. + (field $fs_flags $fdflags) + ;;; Rights that apply to this file descriptor. + (field $fs_rights_base $rights) + ;;; Maximum set of rights that may be installed on new file descriptors that + ;;; are created through this file descriptor, e.g., through `path_open`. + (field $fs_rights_inheriting $rights) + ) +) + +;;; Identifier for a device containing a file system. Can be used in combination +;;; with `inode` to uniquely identify a file or directory in the filesystem. +(typename $device u64) + +;;; Which file time attributes to adjust. +(typename $fstflags + (flags (@witx repr u16) + ;;; Adjust the last data access timestamp to the value stored in `filestat::atim`. + $atim + ;;; Adjust the last data access timestamp to the time of clock `clockid::realtime`. + $atim_now + ;;; Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + $mtim + ;;; Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + $mtim_now + ) +) + +;;; Flags determining the method of how paths are resolved. +(typename $lookupflags + (flags (@witx repr u32) + ;;; As long as the resolved path corresponds to a symbolic link, it is expanded. + $symlink_follow + ) +) + +;;; Open flags used by `path_open`. +(typename $oflags + (flags (@witx repr u16) + ;;; Create file if it does not exist. + $creat + ;;; Fail if not a directory. + $directory + ;;; Fail if file already exists. + $excl + ;;; Truncate file to size 0. + $trunc + ) +) + +;;; Number of hard links to an inode. +(typename $linkcount u64) + +;;; File attributes. +(typename $filestat + (record + ;;; Device ID of device containing the file. + (field $dev $device) + ;;; File serial number. + (field $ino $inode) + ;;; File type. + (field $filetype $filetype) + ;;; Number of hard links to the file. + (field $nlink $linkcount) + ;;; For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + (field $size $filesize) + ;;; Last data access timestamp. + (field $atim $timestamp) + ;;; Last data modification timestamp. + (field $mtim $timestamp) + ;;; Last file status change timestamp. + (field $ctim $timestamp) + ) +) + +;;; User-provided value that may be attached to objects that is retained when +;;; extracted from the implementation. +(typename $userdata u64) + +;;; Type of a subscription to an event or its occurrence. +(typename $eventtype + (enum (@witx tag u8) + ;;; The time value of clock `subscription_clock::id` has + ;;; reached timestamp `subscription_clock::timeout`. + $clock + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has data + ;;; available for reading. This event always triggers for regular files. + $fd_read + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has capacity + ;;; available for writing. This event always triggers for regular files. + $fd_write + ) +) + +;;; The state of the file descriptor subscribed to with +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $eventrwflags + (flags (@witx repr u16) + ;;; The peer of this socket has closed or disconnected. + $fd_readwrite_hangup + ) +) + +;;; The contents of an `event` when type is `eventtype::fd_read` or +;;; `eventtype::fd_write`. +(typename $event_fd_readwrite + (record + ;;; The number of bytes available for reading or writing. + (field $nbytes $filesize) + ;;; The state of the file descriptor. + (field $flags $eventrwflags) + ) +) + +;;; An event that occurred. +(typename $event + (record + ;;; User-provided value that got attached to `subscription::userdata`. + (field $userdata $userdata) + ;;; If non-zero, an error that occurred while processing the subscription request. + (field $error $errno) + ;;; The type of event that occurred + (field $type $eventtype) + ;;; The contents of the event, if it is an `eventtype::fd_read` or + ;;; `eventtype::fd_write`. `eventtype::clock` events ignore this field. + (field $fd_readwrite $event_fd_readwrite) + ) +) + +;;; Flags determining how to interpret the timestamp provided in +;;; `subscription_clock::timeout`. +(typename $subclockflags + (flags (@witx repr u16) + ;;; If set, treat the timestamp provided in + ;;; `subscription_clock::timeout` as an absolute timestamp of clock + ;;; `subscription_clock::id`. If clear, treat the timestamp + ;;; provided in `subscription_clock::timeout` relative to the + ;;; current time value of clock `subscription_clock::id`. + $subscription_clock_abstime + ) +) + +;;; The contents of a `subscription` when type is `eventtype::clock`. +(typename $subscription_clock + (record + ;;; The clock against which to compare the timestamp. + (field $id $clockid) + ;;; The absolute or relative timestamp. + (field $timeout $timestamp) + ;;; The amount of time that the implementation may wait additionally + ;;; to coalesce with other events. + (field $precision $timestamp) + ;;; Flags specifying whether the timeout is absolute or relative + (field $flags $subclockflags) + ) +) + +;;; The contents of a `subscription` when type is type is +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $subscription_fd_readwrite + (record + ;;; The file descriptor on which to wait for it to become ready for reading or writing. + (field $file_descriptor $fd) + ) +) + +;;; The contents of a `subscription`. +(typename $subscription_u + (union + (@witx tag $eventtype) + $subscription_clock + $subscription_fd_readwrite + $subscription_fd_readwrite + ) +) + +;;; Subscription to an event. +(typename $subscription + (record + ;;; User-provided value that is attached to the subscription in the + ;;; implementation and returned through `event::userdata`. + (field $userdata $userdata) + ;;; The type of the event to which to subscribe, and its contents + (field $u $subscription_u) + ) +) + +;;; Exit code generated by a process when exiting. +(typename $exitcode u32) + +;;; Signal condition. +(typename $signal + (enum (@witx tag u8) + ;;; No signal. Note that POSIX has special semantics for `kill(pid, 0)`, + ;;; so this value is reserved. + $none + ;;; Hangup. + ;;; Action: Terminates the process. + $hup + ;;; Terminate interrupt signal. + ;;; Action: Terminates the process. + $int + ;;; Terminal quit signal. + ;;; Action: Terminates the process. + $quit + ;;; Illegal instruction. + ;;; Action: Terminates the process. + $ill + ;;; Trace/breakpoint trap. + ;;; Action: Terminates the process. + $trap + ;;; Process abort signal. + ;;; Action: Terminates the process. + $abrt + ;;; Access to an undefined portion of a memory object. + ;;; Action: Terminates the process. + $bus + ;;; Erroneous arithmetic operation. + ;;; Action: Terminates the process. + $fpe + ;;; Kill. + ;;; Action: Terminates the process. + $kill + ;;; User-defined signal 1. + ;;; Action: Terminates the process. + $usr1 + ;;; Invalid memory reference. + ;;; Action: Terminates the process. + $segv + ;;; User-defined signal 2. + ;;; Action: Terminates the process. + $usr2 + ;;; Write on a pipe with no one to read it. + ;;; Action: Ignored. + $pipe + ;;; Alarm clock. + ;;; Action: Terminates the process. + $alrm + ;;; Termination signal. + ;;; Action: Terminates the process. + $term + ;;; Child process terminated, stopped, or continued. + ;;; Action: Ignored. + $chld + ;;; Continue executing, if stopped. + ;;; Action: Continues executing, if stopped. + $cont + ;;; Stop executing. + ;;; Action: Stops executing. + $stop + ;;; Terminal stop signal. + ;;; Action: Stops executing. + $tstp + ;;; Background process attempting read. + ;;; Action: Stops executing. + $ttin + ;;; Background process attempting write. + ;;; Action: Stops executing. + $ttou + ;;; High bandwidth data is available at a socket. + ;;; Action: Ignored. + $urg + ;;; CPU time limit exceeded. + ;;; Action: Terminates the process. + $xcpu + ;;; File size limit exceeded. + ;;; Action: Terminates the process. + $xfsz + ;;; Virtual timer expired. + ;;; Action: Terminates the process. + $vtalrm + ;;; Profiling timer expired. + ;;; Action: Terminates the process. + $prof + ;;; Window changed. + ;;; Action: Ignored. + $winch + ;;; I/O possible. + ;;; Action: Terminates the process. + $poll + ;;; Power failure. + ;;; Action: Terminates the process. + $pwr + ;;; Bad system call. + ;;; Action: Terminates the process. + $sys + ) +) + +;;; Flags provided to `sock_recv`. +(typename $riflags + (flags (@witx repr u16) + ;;; Returns the message without removing it from the socket's receive queue. + $recv_peek + ;;; On byte-stream sockets, block until the full amount of data can be returned. + $recv_waitall + ) +) + +;;; Flags returned by `sock_recv`. +(typename $roflags + (flags (@witx repr u16) + ;;; Returned by `sock_recv`: Message data has been truncated. + $recv_data_truncated + ) +) + +;;; Flags provided to `sock_send`. As there are currently no flags +;;; defined, it must be set to zero. +(typename $siflags u16) + +;;; Which channels on a socket to shut down. +(typename $sdflags + (flags (@witx repr u8) + ;;; Disables further receive operations. + $rd + ;;; Disables further send operations. + $wr + ) +) + +;;; Identifiers for preopened capabilities. +(typename $preopentype + (enum (@witx tag u8) + ;;; A pre-opened directory. + $dir + ) +) + +;;; The contents of a $prestat when type is `preopentype::dir`. +(typename $prestat_dir + (record + ;;; The length of the directory name for use with `fd_prestat_dir_name`. + (field $pr_name_len $size) + ) +) + +;;; Information about a pre-opened capability. +(typename $prestat + (union (@witx tag $preopentype) + $prestat_dir + ) +) + diff --git a/vfs/src/preview1/witx/wasi_snapshot_preview1.witx b/vfs/src/preview1/witx/wasi_snapshot_preview1.witx new file mode 100644 index 0000000..f9e2aa2 --- /dev/null +++ b/vfs/src/preview1/witx/wasi_snapshot_preview1.witx @@ -0,0 +1,521 @@ +;; WASI Preview. This is an evolution of the API that WASI initially +;; launched with. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/blob/main/legacy/tools/witx-docs.md) +;; for an explanation of what that means. + +(use "typenames.witx") + +(module $wasi_snapshot_preview1 + ;;; Linear memory to be accessed by WASI functions that need it. + (import "memory" (memory)) + + ;;; Read command-line argument data. + ;;; The size of the array should match that returned by `args_sizes_get`. + ;;; Each argument is expected to be `\0` terminated. + (@interface func (export "args_get") + (param $argv (@witx pointer (@witx pointer u8))) + (param $argv_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return command-line argument data sizes. + (@interface func (export "args_sizes_get") + ;;; Returns the number of arguments and the size of the argument string + ;;; data, or an error. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Read environment variable data. + ;;; The sizes of the buffers should match that returned by `environ_sizes_get`. + ;;; Key/value pairs are expected to be joined with `=`s, and terminated with `\0`s. + (@interface func (export "environ_get") + (param $environ (@witx pointer (@witx pointer u8))) + (param $environ_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return environment variable data sizes. + (@interface func (export "environ_sizes_get") + ;;; Returns the number of environment variable arguments and the size of the + ;;; environment variable data. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Return the resolution of a clock. + ;;; Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, + ;;; return `errno::inval`. + ;;; Note: This is similar to `clock_getres` in POSIX. + (@interface func (export "clock_res_get") + ;;; The clock for which to return the resolution. + (param $id $clockid) + ;;; The resolution of the clock, or an error if one happened. + (result $error (expected $timestamp (error $errno))) + ) + ;;; Return the time value of a clock. + ;;; Note: This is similar to `clock_gettime` in POSIX. + (@interface func (export "clock_time_get") + ;;; The clock for which to return the time. + (param $id $clockid) + ;;; The maximum lag (exclusive) that the returned time value may have, compared to its actual value. + (param $precision $timestamp) + ;;; The time value of the clock. + (result $error (expected $timestamp (error $errno))) + ) + + ;;; Provide file advisory information on a file descriptor. + ;;; Note: This is similar to `posix_fadvise` in POSIX. + (@interface func (export "fd_advise") + (param $fd $fd) + ;;; The offset within the file to which the advisory applies. + (param $offset $filesize) + ;;; The length of the region to which the advisory applies. + (param $len $filesize) + ;;; The advice. + (param $advice $advice) + (result $error (expected (error $errno))) + ) + + ;;; Force the allocation of space in a file. + ;;; Note: This is similar to `posix_fallocate` in POSIX. + (@interface func (export "fd_allocate") + (param $fd $fd) + ;;; The offset at which to start the allocation. + (param $offset $filesize) + ;;; The length of the area that is allocated. + (param $len $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Close a file descriptor. + ;;; Note: This is similar to `close` in POSIX. + (@interface func (export "fd_close") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Synchronize the data of a file to disk. + ;;; Note: This is similar to `fdatasync` in POSIX. + (@interface func (export "fd_datasync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Get the attributes of a file descriptor. + ;;; Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. + (@interface func (export "fd_fdstat_get") + (param $fd $fd) + ;;; The buffer where the file descriptor's attributes are stored. + (result $error (expected $fdstat (error $errno))) + ) + + ;;; Adjust the flags associated with a file descriptor. + ;;; Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + (@interface func (export "fd_fdstat_set_flags") + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the rights associated with a file descriptor. + ;;; This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights + (@interface func (export "fd_fdstat_set_rights") + (param $fd $fd) + ;;; The desired rights of the file descriptor. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of an open file. + (@interface func (export "fd_filestat_get") + (param $fd $fd) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + ;;; Note: This is similar to `ftruncate` in POSIX. + (@interface func (export "fd_filestat_set_size") + (param $fd $fd) + ;;; The desired file size. + (param $size $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the timestamps of an open file or directory. + ;;; Note: This is similar to `futimens` in POSIX. + (@interface func (export "fd_filestat_set_times") + (param $fd $fd) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Read from a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `preadv` in POSIX. + (@interface func (export "fd_pread") + (param $fd $fd) + ;;; List of scatter/gather vectors in which to store data. + (param $iovs $iovec_array) + ;;; The offset within the file at which to read. + (param $offset $filesize) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_get") + (param $fd $fd) + ;;; The buffer where the description is stored. + (result $error (expected $prestat (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_dir_name") + (param $fd $fd) + ;;; A buffer into which to write the preopened directory name. + (param $path (@witx pointer u8)) + (param $path_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Write to a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `pwritev` in POSIX. + (@interface func (export "fd_pwrite") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + ;;; The offset within the file at which to write. + (param $offset $filesize) + ;;; The number of bytes written. + (result $error (expected $size (error $errno))) + ) + + ;;; Read from a file descriptor. + ;;; Note: This is similar to `readv` in POSIX. + (@interface func (export "fd_read") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $iovs $iovec_array) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Read directory entries from a directory. + ;;; When successful, the contents of the output buffer consist of a sequence of + ;;; directory entries. Each directory entry consists of a `dirent` object, + ;;; followed by `dirent::d_namlen` bytes holding the name of the directory + ;;; entry. + ;; + ;;; This function fills the output buffer as much as possible, potentially + ;;; truncating the last directory entry. This allows the caller to grow its + ;;; read buffer size in case it's too small to fit a single large directory + ;;; entry, or skip the oversized directory entry. + (@interface func (export "fd_readdir") + (param $fd $fd) + ;;; The buffer where directory entries are stored + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The location within the directory to start reading + (param $cookie $dircookie) + ;;; The number of bytes stored in the read buffer. If less than the size of the read buffer, the end of the directory has been reached. + (result $error (expected $size (error $errno))) + ) + + ;;; Atomically replace a file descriptor by renumbering another file descriptor. + ;; + ;;; Due to the strong focus on thread safety, this environment does not provide + ;;; a mechanism to duplicate or renumber a file descriptor to an arbitrary + ;;; number, like `dup2()`. This would be prone to race conditions, as an actual + ;;; file descriptor with the same number could be allocated by a different + ;;; thread at the same time. + ;; + ;;; This function provides a way to atomically renumber file descriptors, which + ;;; would disappear if `dup2()` were to be removed entirely. + (@interface func (export "fd_renumber") + (param $fd $fd) + ;;; The file descriptor to overwrite. + (param $to $fd) + (result $error (expected (error $errno))) + ) + + ;;; Move the offset of a file descriptor. + ;;; Note: This is similar to `lseek` in POSIX. + (@interface func (export "fd_seek") + (param $fd $fd) + ;;; The number of bytes to move. + (param $offset $filedelta) + ;;; The base from which the offset is relative. + (param $whence $whence) + ;;; The new offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Synchronize the data and metadata of a file to disk. + ;;; Note: This is similar to `fsync` in POSIX. + (@interface func (export "fd_sync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Return the current offset of a file descriptor. + ;;; Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + (@interface func (export "fd_tell") + (param $fd $fd) + ;;; The current offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Write to a file descriptor. + ;;; Note: This is similar to `writev` in POSIX. + (@interface func (export "fd_write") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + (result $error (expected $size (error $errno))) + ) + + ;;; Create a directory. + ;;; Note: This is similar to `mkdirat` in POSIX. + (@interface func (export "path_create_directory") + (param $fd $fd) + ;;; The path at which to create the directory. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of a file or directory. + ;;; Note: This is similar to `stat` in POSIX. + (@interface func (export "path_filestat_get") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to inspect. + (param $path string) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the timestamps of a file or directory. + ;;; Note: This is similar to `utimensat` in POSIX. + (@interface func (export "path_filestat_set_times") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to operate on. + (param $path string) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Create a hard link. + ;;; Note: This is similar to `linkat` in POSIX. + (@interface func (export "path_link") + (param $old_fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $old_flags $lookupflags) + ;;; The source path from which to link. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path at which to create the hard link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Open a file or directory. + ;; + ;;; The returned file descriptor is not guaranteed to be the lowest-numbered + ;;; file descriptor not currently open; it is randomized to prevent + ;;; applications from depending on making assumptions about indexes, since this + ;;; is error-prone in multi-threaded contexts. The returned file descriptor is + ;;; guaranteed to be less than 2**31. + ;; + ;;; Note: This is similar to `openat` in POSIX. + (@interface func (export "path_open") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $dirflags $lookupflags) + ;;; The relative path of the file or directory to open, relative to the + ;;; `path_open::fd` directory. + (param $path string) + ;;; The method by which to open the file. + (param $oflags $oflags) + ;;; The initial rights of the newly created file descriptor. The + ;;; implementation is allowed to return a file descriptor with fewer rights + ;;; than specified, if and only if those rights do not apply to the type of + ;;; file being opened. + ;; + ;;; The *base* rights are rights that will apply to operations using the file + ;;; descriptor itself, while the *inheriting* rights are rights that apply to + ;;; file descriptors derived from it. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (param $fdflags $fdflags) + ;;; The file descriptor of the file that has been opened. + (result $error (expected $fd (error $errno))) + ) + + ;;; Read the contents of a symbolic link. + ;;; Note: This is similar to `readlinkat` in POSIX. + (@interface func (export "path_readlink") + (param $fd $fd) + ;;; The path of the symbolic link from which to read. + (param $path string) + ;;; The buffer to which to write the contents of the symbolic link. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The number of bytes placed in the buffer. + (result $error (expected $size (error $errno))) + ) + + ;;; Remove a directory. + ;;; Return `errno::notempty` if the directory is not empty. + ;;; Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + (@interface func (export "path_remove_directory") + (param $fd $fd) + ;;; The path to a directory to remove. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Rename a file or directory. + ;;; Note: This is similar to `renameat` in POSIX. + (@interface func (export "path_rename") + (param $fd $fd) + ;;; The source path of the file or directory to rename. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path to which to rename the file or directory. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Create a symbolic link. + ;;; Note: This is similar to `symlinkat` in POSIX. + (@interface func (export "path_symlink") + ;;; The contents of the symbolic link. + (param $old_path string) + (param $fd $fd) + ;;; The destination path at which to create the symbolic link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + + ;;; Unlink a file. + ;;; Return `errno::isdir` if the path refers to a directory. + ;;; Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + (@interface func (export "path_unlink_file") + (param $fd $fd) + ;;; The path to a file to unlink. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Concurrently poll for the occurrence of a set of events. + (@interface func (export "poll_oneoff") + ;;; The events to which to subscribe. + (param $in (@witx const_pointer $subscription)) + ;;; The events that have occurred. + (param $out (@witx pointer $event)) + ;;; Both the number of subscriptions and events. + (param $nsubscriptions $size) + ;;; The number of events stored. + (result $error (expected $size (error $errno))) + ) + + ;;; Terminate the process normally. An exit code of 0 indicates successful + ;;; termination of the program. The meanings of other values is dependent on + ;;; the environment. + (@interface func (export "proc_exit") + ;;; The exit code returned by the process. + (param $rval $exitcode) + (@witx noreturn) + ) + + ;;; Send a signal to the process of the calling thread. + ;;; Note: This is similar to `raise` in POSIX. + (@interface func (export "proc_raise") + ;;; The signal condition to trigger. + (param $sig $signal) + (result $error (expected (error $errno))) + ) + + ;;; Temporarily yield execution of the calling thread. + ;;; Note: This is similar to `sched_yield` in POSIX. + (@interface func (export "sched_yield") + (result $error (expected (error $errno))) + ) + + ;;; Write high-quality random data into a buffer. + ;;; This function blocks when the implementation is unable to immediately + ;;; provide sufficient high-quality random data. + ;;; This function may execute slowly, so when large mounts of random data are + ;;; required, it's advisable to use this function to seed a pseudo-random + ;;; number generator, rather than to provide the random data directly. + (@interface func (export "random_get") + ;;; The buffer to fill with random data. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Accept a new incoming connection. + ;;; Note: This is similar to `accept` in POSIX. + (@interface func (export "sock_accept") + ;;; The listening socket. + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + ;;; New socket connection + (result $error (expected $fd (error $errno))) + ) + + ;;; Receive a message from a socket. + ;;; Note: This is similar to `recv` in POSIX, though it also supports reading + ;;; the data into multiple buffers in the manner of `readv`. + (@interface func (export "sock_recv") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $ri_data $iovec_array) + ;;; Message flags. + (param $ri_flags $riflags) + ;;; Number of bytes stored in ri_data and message flags. + (result $error (expected (tuple $size $roflags) (error $errno))) + ) + + ;;; Send a message on a socket. + ;;; Note: This is similar to `send` in POSIX, though it also supports writing + ;;; the data from multiple buffers in the manner of `writev`. + (@interface func (export "sock_send") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to retrieve data + (param $si_data $ciovec_array) + ;;; Message flags. + (param $si_flags $siflags) + ;;; Number of bytes transmitted. + (result $error (expected $size (error $errno))) + ) + + ;;; Shut down socket send and receive channels. + ;;; Note: This is similar to `shutdown` in POSIX. + (@interface func (export "sock_shutdown") + (param $fd $fd) + ;;; Which channels on the socket to shut down. + (param $how $sdflags) + (result $error (expected (error $errno))) + ) +) diff --git a/vfs/tests/preview1.rs b/vfs/tests/preview1.rs new file mode 100644 index 0000000..906e154 --- /dev/null +++ b/vfs/tests/preview1.rs @@ -0,0 +1,360 @@ +//! Core-module tests for preview1 filesystem routing and real userland programs. +use sausage_vfs::{ + preview1::{self, State}, + *, +}; +use std::{ + hash::{Hash, Hasher}, + path::Path, + sync::{Arc, OnceLock}, +}; +use wasmtime::{Engine, Linker, Module, Store}; + +fn engine() -> &'static Engine { + static E: OnceLock = OnceLock::new(); + E.get_or_init(Engine::default) +} +fn module(path: &str) -> Module { + let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("required guest {path}: {e}")); + let mut hash = std::collections::hash_map::DefaultHasher::new(); + bytes.hash(&mut hash); + engine().precompile_compatibility_hash().hash(&mut hash); + let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("preview1-guests"); + std::fs::create_dir_all(&dir).unwrap(); + let cache = dir.join(format!("{:x}.cwasm", hash.finish())); + if !cache.exists() { + std::fs::write(&cache, engine().precompile_module(&bytes).unwrap()).unwrap(); + } + // This cache contains only modules compiled locally by this test with this engine. + unsafe { Module::deserialize_file(engine(), cache) }.unwrap() +} +async fn run(module: &Module, root: Arc, cwd: &str, args: &[&str]) -> (String, String) { + let (stdout, mut out) = tokio::io::duplex(1024 * 1024); + let (stderr, mut err) = tokio::io::duplex(1024 * 1024); + let mut linker = Linker::new(engine()); + preview1::add_to_linker(&mut linker, |s: &mut State| s).unwrap(); + let mut store = Store::new( + engine(), + State::new( + root, + cwd, + args.iter().map(|s| (*s).into()).collect(), + vec![], + Box::new(tokio::io::empty()), + Box::new(stdout), + Box::new(stderr), + ), + ); + let instance = linker.instantiate_async(&mut store, module).await.unwrap(); + let start = instance + .get_typed_func::<(), ()>(&mut store, "_start") + .unwrap(); + let result = start.call_async(&mut store, ()).await; + drop(store); + use tokio::io::AsyncReadExt; + let mut stdout = String::new(); + out.read_to_string(&mut stdout).await.unwrap(); + let mut stderr = String::new(); + err.read_to_string(&mut stderr).await.unwrap(); + if let Err(e) = result { + assert_eq!( + e.downcast_ref::().map(|e| e.0), + Some(0), + "guest failed: {e:?}\n{stderr}" + ); + } + (stdout, stderr) +} +fn wat(path: &str, body: &str) -> Module { + Module::new(engine(), format!(r#"(module + (import "wasi_snapshot_preview1" "path_filestat_get" (func $stat (param i32 i32 i32 i32 i32) (result i32))) + (memory (export "memory") 1) + (data (i32.const 0) "{path}") + (func (export "_start") {body}))"#)).unwrap() +} +#[tokio::test] +async fn cwd_and_parent_across_mounts() { + let base = Arc::new(Memory::default()); + base.mkdir("/work").await.unwrap(); + let shared = Arc::new(Memory::default()); + shared.mkdir("/inner").await.unwrap(); + base.open( + "/value", + Open { + create: true, + write: true, + ..Open::default() + }, + ) + .await + .unwrap(); + let file = shared + .open( + "/value", + Open { + create: true, + write: true, + ..Open::default() + }, + ) + .await + .unwrap(); + file.write(0, b"hello".to_vec()).await.unwrap(); + let mut mounts = Mounts::default(); + mounts.mount("/", base, false); + mounts.mount("/shared", shared, false); + let mounts = Arc::new(mounts); + let guest = wat( + "../shared/value", + "i32.const 3 i32.const 1 i32.const 0 i32.const 15 i32.const 64 call $stat if unreachable end i32.const 96 i64.load i64.const 5 i64.ne if unreachable end", + ); + run(&guest, mounts.clone(), "/work", &["probe"]).await; + let parent = wat( + "../../value", + "i32.const 3 i32.const 1 i32.const 0 i32.const 11 i32.const 64 call $stat if unreachable end i32.const 96 i64.load i64.const 0 i64.ne if unreachable end", + ); + run(&parent, mounts, "/shared/inner", &["probe"]).await; + println!("PASS cwd and .. across mounts"); +} +#[tokio::test] +async fn missing_file_errno() { + let guest = wat( + "../shared/value", + "i32.const 3 i32.const 1 i32.const 0 i32.const 15 i32.const 64 call $stat i32.const 44 i32.ne if unreachable end", + ); + run(&guest, Arc::new(Memory::default()), "/work", &["probe"]).await; + println!("PASS missing path returns NOENT=44"); +} +#[tokio::test] +async fn poll_preserves_input_and_returns_clock() { + use preview1::{types::*, wasi_snapshot_preview1::WasiSnapshotPreview1}; + use wiggle::{GuestMemory, GuestPtr}; + let mut state = State::new( + Arc::new(Memory::default()), + "/", + vec![], + vec![], + Box::new(&b"x"[..]), + Box::new(tokio::io::sink()), + Box::new(tokio::io::sink()), + ); + let mut bytes = [0; 256]; + let mut memory = GuestMemory::Unshared(&mut bytes); + let sub = GuestPtr::new(0); + let event = GuestPtr::new(64); + memory + .write( + sub, + Subscription { + userdata: 7, + u: SubscriptionU::FdRead(SubscriptionFdReadwrite { + file_descriptor: 0.into(), + }), + }, + ) + .unwrap(); + assert_eq!( + state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), + 1 + ); + assert_eq!(memory.read(event).unwrap().userdata, 7); + let iov = GuestPtr::new(128); + memory + .write( + iov, + Iovec { + buf: GuestPtr::new(192), + buf_len: 100, + }, + ) + .unwrap(); + assert!( + state + .fd_read(&mut memory, 0.into(), iov.as_array(1)) + .await + .is_err() + ); + memory + .write( + iov, + Iovec { + buf: GuestPtr::new(192), + buf_len: 1, + }, + ) + .unwrap(); + assert_eq!( + state + .fd_read(&mut memory, 0.into(), iov.as_array(1)) + .await + .unwrap(), + 1 + ); + assert_eq!(memory.read(GuestPtr::::new(192)).unwrap(), b'x'); + memory + .write( + sub, + Subscription { + userdata: 8, + u: SubscriptionU::Clock(SubscriptionClock { + id: Clockid::Monotonic, + timeout: 0, + precision: 0, + flags: Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME, + }), + }, + ) + .unwrap(); + assert_eq!( + state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), + 1 + ); + assert_eq!(memory.read(event).unwrap().type_, Eventtype::Clock); + memory + .write( + sub, + Subscription { + userdata: 9, + u: SubscriptionU::FdWrite(SubscriptionFdReadwrite { + file_descriptor: 1.into(), + }), + }, + ) + .unwrap(); + assert_eq!( + state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), + 1 + ); + assert_eq!(memory.read(event).unwrap().type_, Eventtype::FdWrite); +} + +#[tokio::test(flavor = "multi_thread")] +async fn userland_files() { + let guest = module("/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm"); + let root = Arc::new(Memory::default()); + run(&guest, root.clone(), "/", &["coreutils", "mkdir", "/work"]).await; + run( + &guest, + root.clone(), + "/work", + &["coreutils", "touch", "input"], + ) + .await; + let input = root + .open( + "/work/input", + Open { + write: true, + ..Open::default() + }, + ) + .await + .unwrap(); + input.write(0, b"hello preview1\n".to_vec()).await.unwrap(); + run( + &guest, + root.clone(), + "/work", + &["coreutils", "cp", "input", "copy"], + ) + .await; + let (cat, _) = run(&guest, root.clone(), "/work", &["coreutils", "cat", "copy"]).await; + assert_eq!(cat, "hello preview1\n"); + let (ls, _) = run( + &guest, + root.clone(), + "/work", + &["coreutils", "ls", "-la", "."], + ) + .await; + assert!(ls.contains("copy") && ls.contains("input"), "{ls}"); + run(&guest, root.clone(), "/work", &["coreutils", "rm", "copy"]).await; + assert_eq!( + root.stat("/work/copy", true).await.unwrap_err().kind(), + std::io::ErrorKind::NotFound + ); + println!("PASS uutils mkdir touch cp cat ls -la rm"); + let qjs = module("/tmp/wasi-userland-lab/qjs-wasi.wasm"); + let script = b"let f = std.open('input', 'r'); let text = f.readAsString(); f.close(); let out = std.open('js-output', 'w'); out.puts(text.toUpperCase()); out.close(); console.log('QuickJS files ok');"; + let file = root + .open( + "/work/check.mjs", + Open { + create: true, + write: true, + ..Open::default() + }, + ) + .await + .unwrap(); + file.write(0, script.to_vec()).await.unwrap(); + let (out, _) = run( + &qjs, + root.clone(), + "/work", + &["qjs", "--std", "--module", "check.mjs"], + ) + .await; + assert!(out.contains("QuickJS files ok")); + let output = root.open("/work/js-output", Open::default()).await.unwrap(); + assert_eq!(output.read(0, 100).await.unwrap(), b"HELLO PREVIEW1\n"); + println!("PASS QuickJS reads and writes files"); +} +#[tokio::test(flavor = "multi_thread")] +async fn esbuild_bundle() { + let dir = tempfile::tempdir().unwrap(); + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../spikes/s6-esbuild"); + for name in ["app.tsx", "check.mjs", "package.json", "package-lock.json"] { + std::fs::copy(fixture.join(name), dir.path().join(name)).unwrap(); + } + assert!( + std::process::Command::new("cp") + .args(["-r", "/tmp/s6-esbuild/app/node_modules"]) + .arg(dir.path()) + .status() + .unwrap() + .success() + ); + let flags = [ + "app.tsx", + "--bundle", + "--minify", + "--format=iife", + "--platform=browser", + "--jsx=automatic", + "--jsx-import-source=preact", + "--legal-comments=none", + "--log-level=error", + ]; + assert!( + std::process::Command::new("/tmp/s6-esbuild/esbuild-native") + .current_dir(dir.path()) + .args(flags) + .arg("--outfile=native.js") + .status() + .unwrap() + .success() + ); + let root = Arc::new(Host::new(dir.path(), Process::default()).unwrap()); + let guest = module("/tmp/s6-esbuild/esbuild.wasm"); + let mut args = vec!["esbuild"]; + args.extend(flags); + args.push("--outfile=host.js"); + run(&guest, root, "/", &args).await; + assert_eq!( + std::fs::read(dir.path().join("native.js")).unwrap(), + std::fs::read(dir.path().join("host.js")).unwrap() + ); + let render = std::process::Command::new("node") + .current_dir(dir.path()) + .args(["check.mjs", "host.js"]) + .output() + .unwrap(); + assert!( + render.status.success(), + "{}", + String::from_utf8_lossy(&render.stderr) + ); + println!("{}", String::from_utf8_lossy(&render.stdout)); + println!("PASS esbuild through sausage-vfs: byte-identical to native"); +}