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/.
This commit is contained in:
2026-11-06 16:34:33 +00:00
parent 4b27e9afd5
commit 5ce2a2f2bd
14 changed files with 3038 additions and 1 deletions
+53
View File
@@ -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.
+29
View File
@@ -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'
}
@@ -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 {
@@ -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
}
+43
View File
@@ -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<string>] {
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'
}
+3 -1
View File
@@ -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"
+1
View File
@@ -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;
+43
View File
@@ -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<dyn Layer>` 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
```
+605
View File
@@ -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<GuestPtr<u8>>,
buf: GuestPtr<u8>,
) -> 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<GuestPtr<u8>>,
buf: GuestPtr<u8>,
) -> 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<Timestamp, Error> {
self.clock(id)?;
Ok(1)
}
fn clock_time_get(
&mut self,
_: &mut GuestMemory<'_>,
id: Clockid,
_: Timestamp,
) -> Result<Timestamp, Error> {
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<Fdstat, Error> {
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<Filestat, Error> {
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<Size, Error> {
self.read(m, fd, iovs, None).await
}
async fn fd_pread(
&mut self,
m: &mut GuestMemory<'_>,
fd: Fd,
iovs: IovecArray,
offset: Filesize,
) -> Result<Size, Error> {
self.read(m, fd, iovs, Some(offset)).await
}
async fn fd_write(
&mut self,
m: &mut GuestMemory<'_>,
fd: Fd,
iovs: CiovecArray,
) -> Result<Size, Error> {
self.write(m, fd, iovs, None).await
}
async fn fd_pwrite(
&mut self,
m: &mut GuestMemory<'_>,
fd: Fd,
iovs: CiovecArray,
offset: Filesize,
) -> Result<Size, Error> {
self.write(m, fd, iovs, Some(offset)).await
}
fn fd_prestat_get(&mut self, _: &mut GuestMemory<'_>, fd: Fd) -> Result<Prestat, Error> {
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<u8>,
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<Filesize, Error> {
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<Filesize, Error> {
Ok(self.fd(fd)?.offset)
}
async fn fd_readdir(
&mut self,
m: &mut GuestMemory<'_>,
fd: Fd,
buf: GuestPtr<u8>,
len: Size,
cookie: Dircookie,
) -> Result<Size, Error> {
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<str>,
) -> 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<str>,
) -> Result<Filestat, Error> {
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<str>,
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<str>,
oflags: Oflags,
rights: Rights,
_: Rights,
fdflags: Fdflags,
) -> Result<Fd, Error> {
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<str>,
buf: GuestPtr<u8>,
len: Size,
) -> Result<Size, Error> {
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<str>,
) -> 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<str>,
) -> 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<str>,
to: Fd,
dst: GuestPtr<str>,
) -> 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<str>,
_: Fd,
_: GuestPtr<str>,
) -> Result<(), Error> {
Err(Errno::Notsup.into())
}
async fn path_symlink(
&mut self,
_: &mut GuestMemory<'_>,
_: GuestPtr<str>,
_: Fd,
_: GuestPtr<str>,
) -> Result<(), Error> {
Err(Errno::Notsup.into())
}
async fn poll_oneoff(
&mut self,
m: &mut GuestMemory<'_>,
subs: GuestPtr<Subscription>,
events: GuestPtr<Event>,
count: Size,
) -> Result<Size, Error> {
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<u8>,
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<Fd, Error> {
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<Size, Error> {
Err(Errno::Notsup.into())
}
fn sock_shutdown(&mut self, _: &mut GuestMemory<'_>, _: Fd, _: Sdflags) -> Result<(), Error> {
Err(Errno::Notsup.into())
}
}
+342
View File
@@ -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<dyn AsyncRead + Unpin + Send + Sync>;
/// Async output supplied by the process table.
pub type Output = Box<dyn AsyncWrite + Unpin + Send + Sync>;
/// A process context with one preopen `/` and a virtual current directory.
pub struct State {
root: Arc<dyn Layer>,
cwd: String,
argv: Vec<String>,
env: Vec<String>,
stdin: DynInputStream,
stdout: DynOutputStream,
stderr: DynOutputStream,
descriptors: BTreeMap<u32, Descriptor>,
start: Instant,
}
#[derive(Clone)]
struct Descriptor {
path: String,
file: Option<Arc<dyn File>>,
offset: u64,
flags: types::Fdflags,
directory: bool,
}
impl State {
pub fn new(
root: Arc<dyn Layer>,
cwd: &str,
argv: Vec<String>,
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<Arc<dyn File>, types::Error> {
self.fd(fd)?
.file
.clone()
.ok_or_else(|| types::Errno::Isdir.into())
}
fn path(
&self,
memory: &GuestMemory<'_>,
fd: types::Fd,
path: GuestPtr<str>,
) -> Result<String, types::Error> {
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<u64, types::Error> {
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<u64>,
) -> Result<u32, types::Error> {
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<u64>,
) -> Result<u32, types::Error> {
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<T: Send + 'static>(
linker: &mut wasmtime::Linker<T>,
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<wiggle::GuestError> for types::Error {
fn from(_: wiggle::GuestError) -> Self {
types::Errno::Fault.into()
}
}
impl From<StreamError> for types::Error {
fn from(error: StreamError) -> Self {
match error {
StreamError::Closed => types::Errno::Pipe.into(),
StreamError::LastOperationFailed(error) => match error.downcast::<io::Error>() {
Ok(error) => error.into(),
Err(error) => types::Error::trap(error),
},
StreamError::Trap(error) => types::Error::trap(error),
}
}
}
impl From<io::Error> 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<SystemTime>) -> 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<SystemTime>, Option<SystemTime>) {
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<GuestPtr<u8>>,
mut buf: GuestPtr<u8>,
) -> 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::<usize>()
.try_into()
.map_err(|_| types::Errno::Overflow)?,
))
}
+220
View File
@@ -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.
+750
View File
@@ -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
)
)
@@ -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)))
)
)
+360
View File
@@ -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<Engine> = 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<dyn Layer>, 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::<wasmtime_wasi::I32Exit>().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::<u8>::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");
}