From 4b1a4a0419ab84559f03595ff254185079fea799 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 7 Sep 2024 19:17:54 -0400 Subject: [PATCH] Start work on a custom wasi implementation that will allow remote proxying --- Cargo.lock | 22 + Cargo.toml | 2 + crates/extension/Cargo.toml | 1 + .../src/extension_indexed_docs_provider.rs | 1 - crates/extension/src/extension_lsp_adapter.rs | 1 - .../extension/src/extension_slash_command.rs | 1 - crates/extension/src/wasm_host.rs | 39 +- crates/extension/src/wasm_host/wit.rs | 2 +- crates/wasi_fs/Cargo.toml | 34 + crates/wasi_fs/src/wasi_fs.rs | 395 +++++++++++ .../wit/deps/clocks/monotonic-clock.wit | 45 ++ crates/wasi_fs/wit/deps/clocks/wall-clock.wit | 42 ++ crates/wasi_fs/wit/deps/clocks/world.wit | 6 + .../wasi_fs/wit/deps/filesystem/preopens.wit | 8 + crates/wasi_fs/wit/deps/filesystem/types.wit | 634 ++++++++++++++++++ crates/wasi_fs/wit/deps/filesystem/world.wit | 6 + crates/wasi_fs/wit/deps/io/error.wit | 34 + crates/wasi_fs/wit/deps/io/poll.wit | 41 ++ crates/wasi_fs/wit/deps/io/streams.wit | 262 ++++++++ crates/wasi_fs/wit/deps/io/world.wit | 6 + crates/wasi_fs/wit/wasi.wit | 1 + 21 files changed, 1549 insertions(+), 34 deletions(-) create mode 100644 crates/wasi_fs/Cargo.toml create mode 100644 crates/wasi_fs/src/wasi_fs.rs create mode 100644 crates/wasi_fs/wit/deps/clocks/monotonic-clock.wit create mode 100644 crates/wasi_fs/wit/deps/clocks/wall-clock.wit create mode 100644 crates/wasi_fs/wit/deps/clocks/world.wit create mode 100644 crates/wasi_fs/wit/deps/filesystem/preopens.wit create mode 100644 crates/wasi_fs/wit/deps/filesystem/types.wit create mode 100644 crates/wasi_fs/wit/deps/filesystem/world.wit create mode 100644 crates/wasi_fs/wit/deps/io/error.wit create mode 100644 crates/wasi_fs/wit/deps/io/poll.wit create mode 100644 crates/wasi_fs/wit/deps/io/streams.wit create mode 100644 crates/wasi_fs/wit/deps/io/world.wit create mode 100644 crates/wasi_fs/wit/wasi.wit diff --git a/Cargo.lock b/Cargo.lock index b7ca5f7c5f..f015d2ff00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4066,6 +4066,7 @@ dependencies = [ "ui", "url", "util", + "wasi_fs", "wasm-encoder 0.201.0", "wasmparser 0.201.0", "wasmtime", @@ -12702,6 +12703,27 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi_fs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collections", + "fs", + "futures 0.3.30", + "gpui", + "log", + "parking_lot", + "serde", + "serde_json", + "util", + "wasmparser 0.201.0", + "wasmtime", + "wasmtime-wasi", + "wit-component", +] + [[package]] name = "wasite" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bd0a665587..2ea371a807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,7 @@ members = [ "crates/util", "crates/vcs_menu", "crates/vim", + "crates/wasi_fs", "crates/welcome", "crates/workspace", "crates/worktree", @@ -297,6 +298,7 @@ ui_input = { path = "crates/ui_input" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } +wasi_fs = { path = "crates/wasi_fs" } welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 0371b1866d..66851e6015 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -50,6 +50,7 @@ util.workspace = true wasm-encoder.workspace = true wasmtime.workspace = true wasmtime-wasi.workspace = true +wasi_fs.workspace = true wasmparser.workspace = true wit-component.workspace = true workspace.workspace = true diff --git a/crates/extension/src/extension_indexed_docs_provider.rs b/crates/extension/src/extension_indexed_docs_provider.rs index 957af63afd..9aedbcfc1d 100644 --- a/crates/extension/src/extension_indexed_docs_provider.rs +++ b/crates/extension/src/extension_indexed_docs_provider.rs @@ -5,7 +5,6 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::FutureExt; use indexed_docs::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; -use wasmtime_wasi::WasiView; use crate::wasm_host::{WasmExtension, WasmHost}; diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index bc18843d63..48d05a1301 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -21,7 +21,6 @@ use std::{ sync::Arc, }; use util::{maybe, ResultExt}; -use wasmtime_wasi::WasiView as _; pub struct ExtensionLspAdapter { pub(crate) extension: WasmExtension, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 60b027ef9d..351260ffb4 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -8,7 +8,6 @@ use futures::FutureExt; use gpui::{Task, WeakView, WindowContext}; use language::LspAdapterDelegate; use ui::prelude::*; -use wasmtime_wasi::WasiView; use workspace::Workspace; use crate::wasm_host::{WasmExtension, WasmHost}; diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs index d39b37da23..192f21536b 100644 --- a/crates/extension/src/wasm_host.rs +++ b/crates/extension/src/wasm_host.rs @@ -26,7 +26,6 @@ use wasmtime::{ component::{Component, ResourceTable}, Engine, Store, }; -use wasmtime_wasi as wasi; use wit::Extension; pub(crate) struct WasmHost { @@ -52,7 +51,7 @@ pub struct WasmExtension { pub(crate) struct WasmState { manifest: Arc, pub(crate) table: ResourceTable, - ctx: wasi::WasiCtx, + fs: Arc, pub(crate) host: Arc, } @@ -120,7 +119,7 @@ impl WasmHost { let mut store = wasmtime::Store::new( &this.engine, WasmState { - ctx: this.build_wasi_ctx(&manifest).await?, + fs: this.fs.clone(), manifest: manifest.clone(), table: ResourceTable::new(), host: this.clone(), @@ -158,30 +157,6 @@ impl WasmHost { }) } - async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { - let extension_work_dir = self.work_dir.join(manifest.id.as_ref()); - self.fs - .create_dir(&extension_work_dir) - .await - .context("failed to create extension work dir")?; - - let file_perms = wasi::FilePerms::all(); - let dir_perms = wasi::DirPerms::all(); - - Ok(wasi::WasiCtxBuilder::new() - .inherit_stdio() - .preopened_dir(&extension_work_dir, ".", dir_perms, file_perms)? - .preopened_dir( - &extension_work_dir, - &extension_work_dir.to_string_lossy(), - dir_perms, - file_perms, - )? - .env("PWD", &extension_work_dir.to_string_lossy()) - .env("RUST_BACKTRACE", "full") - .build()) - } - pub fn path_from_extension(&self, id: &Arc, path: &Path) -> PathBuf { let extension_work_dir = self.work_dir.join(id.as_ref()); normalize_path(&extension_work_dir.join(path)) @@ -288,14 +263,18 @@ impl WasmState { fn work_dir(&self) -> PathBuf { self.host.work_dir.join(self.manifest.id.as_ref()) } + + pub fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } } -impl wasi::WasiView for WasmState { +impl wasi_fs::WasiFsView for WasmState { fn table(&mut self) -> &mut ResourceTable { &mut self.table } - fn ctx(&mut self) -> &mut wasi::WasiCtx { - &mut self.ctx + fn fs(&self) -> &Arc { + &self.fs } } diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 8b72d49a7d..e5489d8125 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -29,7 +29,7 @@ pub fn new_linker( f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, ) -> Linker { let mut linker = Linker::new(&wasm_engine()); - wasmtime_wasi::add_to_linker_async(&mut linker).unwrap(); + wasi_fs::add_to_linker(&mut linker).unwrap(); f(&mut linker, wasi_view).unwrap(); linker } diff --git a/crates/wasi_fs/Cargo.toml b/crates/wasi_fs/Cargo.toml new file mode 100644 index 0000000000..56d5a02350 --- /dev/null +++ b/crates/wasi_fs/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "wasi_fs" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/wasi_fs.rs" +doctest = false + +[dependencies] +async-trait.workspace = true +anyhow.workspace = true +collections.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +log.workspace = true +parking_lot.workspace = true +serde.workspace = true +serde_json.workspace = true +util.workspace = true +wasmtime.workspace = true +wasmtime-wasi.workspace = true +wasmparser.workspace = true +wit-component.workspace = true + +[dev-dependencies] +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/wasi_fs/src/wasi_fs.rs b/crates/wasi_fs/src/wasi_fs.rs new file mode 100644 index 0000000000..ee011821c4 --- /dev/null +++ b/crates/wasi_fs/src/wasi_fs.rs @@ -0,0 +1,395 @@ +use anyhow::Result; +use fs::Fs; +use std::sync::Arc; +use wasi::{ + filesystem::{ + preopens, + types::{self, ErrorCode, HostDescriptor, HostDirectoryEntryStream}, + }, + io::streams, +}; +use wasmtime::component::{Linker, Resource, ResourceTable}; + +pub trait WasiFsView: Send { + fn table(&mut self) -> &mut ResourceTable; + fn fs(&self) -> &Arc; +} + +wasmtime::component::bindgen!({ + path: "wit", + world: "wasi:filesystem/imports", + trappable_imports: true, + async: { + only_imports: [ + "[method]descriptor.access-at", + "[method]descriptor.advise", + "[method]descriptor.change-directory-permissions-at", + "[method]descriptor.change-file-permissions-at", + "[method]descriptor.create-directory-at", + "[method]descriptor.get-flags", + "[method]descriptor.get-type", + "[method]descriptor.is-same-object", + "[method]descriptor.link-at", + "[method]descriptor.lock-exclusive", + "[method]descriptor.lock-shared", + "[method]descriptor.metadata-hash", + "[method]descriptor.metadata-hash-at", + "[method]descriptor.open-at", + "[method]descriptor.read", + "[method]descriptor.read-directory", + "[method]descriptor.readlink-at", + "[method]descriptor.remove-directory-at", + "[method]descriptor.rename-at", + "[method]descriptor.set-size", + "[method]descriptor.set-times", + "[method]descriptor.set-times-at", + "[method]descriptor.stat", + "[method]descriptor.stat-at", + "[method]descriptor.symlink-at", + "[method]descriptor.sync", + "[method]descriptor.sync-data", + "[method]descriptor.try-lock-exclusive", + "[method]descriptor.try-lock-shared", + "[method]descriptor.unlink-file-at", + "[method]descriptor.unlock", + "[method]descriptor.write", + "[method]input-stream.read", + "[method]input-stream.blocking-read", + "[method]input-stream.blocking-skip", + "[method]input-stream.skip", + "[method]output-stream.forward", + "[method]output-stream.splice", + "[method]output-stream.blocking-splice", + "[method]output-stream.blocking-flush", + "[method]output-stream.blocking-write", + "[method]output-stream.blocking-write-and-flush", + "[method]output-stream.blocking-write-zeroes-and-flush", + "[method]directory-entry-stream.read-directory-entry", + "poll", + "[method]pollable.block", + "[method]pollable.ready", + ], + }, + trappable_error_type: { + "wasi:io/streams/stream-error" => StreamError, + "wasi:filesystem/types/error-code" => FsError, + }, + with: { + "wasi:filesystem/types/directory-entry-stream": ReaddirIterator, + "wasi:filesystem/types/descriptor": Descriptor, + "wasi:io/streams/input-stream": InputStream, + "wasi:io/streams/output-stream": OutputStream, + "wasi:io/error/error": StreamError, + "wasi:io/poll/pollable": Pollable, + }, + skip_mut_forwarding_impls: true, +}); + +pub fn add_to_linker(linker: &mut Linker) -> Result<()> { + fn id<'a, T>(state: &'a mut T) -> &'a mut T { + state + } + + wasi::filesystem::types::add_to_linker_get_host(linker, id)?; + wasi::filesystem::preopens::add_to_linker_get_host(linker, id)?; + wasi::io::streams::add_to_linker_get_host(linker, id)?; + + Ok(()) +} + +impl WasiFsView for &mut T { + fn table(&mut self) -> &mut ResourceTable { + T::table(self) + } + + fn fs(&self) -> &Arc { + T::fs(self) + } +} + +#[async_trait::async_trait] +impl HostDescriptor for T { + async fn advise( + &mut self, + _fd: Resource, + _offset: types::Filesize, + _len: types::Filesize, + _advice: types::Advice, + ) -> FsResult<()> { + unimplemented!() + } + + async fn sync_data(&mut self, _fd: Resource) -> FsResult<()> { + unimplemented!() + } + + async fn get_flags(&mut self, _fd: Resource) -> FsResult { + unimplemented!() + } + + async fn get_type(&mut self, _fd: Resource) -> FsResult { + unimplemented!() + } + + async fn set_size( + &mut self, + _fd: Resource, + _size: types::Filesize, + ) -> FsResult<()> { + unimplemented!() + } + + async fn set_times( + &mut self, + _fd: Resource, + _atim: types::NewTimestamp, + _mtim: types::NewTimestamp, + ) -> FsResult<()> { + unimplemented!() + } + + async fn read( + &mut self, + _fd: Resource, + _len: types::Filesize, + _offset: types::Filesize, + ) -> FsResult<(Vec, bool)> { + unimplemented!() + } + + async fn write( + &mut self, + _fd: Resource, + _buf: Vec, + _offset: types::Filesize, + ) -> FsResult { + unimplemented!() + } + + async fn read_directory( + &mut self, + _fd: Resource, + ) -> FsResult> { + unimplemented!() + } + + async fn sync(&mut self, _fd: Resource) -> FsResult<()> { + unimplemented!() + } + + async fn create_directory_at( + &mut self, + _fd: Resource, + _path: String, + ) -> FsResult<()> { + unimplemented!() + } + + async fn stat(&mut self, _fd: Resource) -> FsResult { + unimplemented!() + } + + async fn stat_at( + &mut self, + _fd: Resource, + _path_flags: types::PathFlags, + _path: String, + ) -> FsResult { + unimplemented!() + } + + async fn set_times_at( + &mut self, + _fd: Resource, + _path_flags: types::PathFlags, + _path: String, + _atim: types::NewTimestamp, + _mtim: types::NewTimestamp, + ) -> FsResult<()> { + unimplemented!() + } + + async fn link_at( + &mut self, + _fd: Resource, + _old_path_flags: types::PathFlags, + _old_path: String, + _new_descriptor: Resource, + _new_path: String, + ) -> FsResult<()> { + unimplemented!() + } + + async fn open_at( + &mut self, + _fd: Resource, + _path_flags: types::PathFlags, + _path: String, + _oflags: types::OpenFlags, + _flags: types::DescriptorFlags, + ) -> FsResult> { + unimplemented!() + } + + fn drop(&mut self, _fd: Resource) -> anyhow::Result<()> { + unimplemented!() + } + + async fn readlink_at( + &mut self, + _fd: Resource, + _path: String, + ) -> FsResult { + unimplemented!() + } + + async fn remove_directory_at( + &mut self, + _fd: Resource, + _path: String, + ) -> FsResult<()> { + unimplemented!() + } + + async fn rename_at( + &mut self, + _fd: Resource, + _old_path: String, + _new_fd: Resource, + _new_path: String, + ) -> FsResult<()> { + unimplemented!() + } + + async fn symlink_at( + &mut self, + _fd: Resource, + _src_path: String, + _dest_path: String, + ) -> FsResult<()> { + unimplemented!() + } + + async fn unlink_file_at( + &mut self, + _fd: Resource, + _path: String, + ) -> FsResult<()> { + unimplemented!() + } + + fn read_via_stream( + &mut self, + _fd: Resource, + _offset: types::Filesize, + ) -> FsResult> { + unimplemented!() + } + + fn write_via_stream( + &mut self, + _fd: Resource, + _offset: types::Filesize, + ) -> FsResult> { + unimplemented!() + } + + fn append_via_stream( + &mut self, + _fd: Resource, + ) -> FsResult> { + unimplemented!() + } + + async fn is_same_object( + &mut self, + _a: Resource, + _b: Resource, + ) -> anyhow::Result { + unimplemented!() + } + + async fn metadata_hash( + &mut self, + _fd: Resource, + ) -> FsResult { + unimplemented!() + } + + async fn metadata_hash_at( + &mut self, + _fd: Resource, + _path_flags: types::PathFlags, + _path: String, + ) -> FsResult { + unimplemented!() + } +} + +#[async_trait::async_trait] +impl HostDirectoryEntryStream for T { + async fn read_directory_entry( + &mut self, + _stream: Resource, + ) -> FsResult> { + unimplemented!() + } + + fn drop(&mut self, _stream: Resource) -> anyhow::Result<()> { + unimplemented!() + } +} + +#[async_trait::async_trait] +impl types::Host for T { + fn convert_error_code(&mut self, _err: FsError) -> anyhow::Result { + unimplemented!() + } + + fn filesystem_error_code( + &mut self, + _err: Resource, + ) -> anyhow::Result> { + unimplemented!() + } +} + +#[async_trait::async_trait] +impl streams::Host for T { + fn convert_stream_error(&mut self, err: StreamError) -> anyhow::Result { + unimplemented!() + } +} + +#[async_trait::async_trait] +impl streams::HostOutputStream for T {} + +#[async_trait::async_trait] +impl streams::HostInputStream for T {} + +#[async_trait::async_trait] +impl preopens::Host for T { + fn get_directories( + &mut self, + ) -> Result, String)>, anyhow::Error> { + unimplemented!() + } +} + +pub struct InputStream {} + +pub struct OutputStream {} + +pub struct Descriptor {} + +pub struct ReaddirIterator {} + +pub struct StreamError {} + +pub struct IoError {} + +pub struct Pollable {} + +pub type FsResult = Result; + +pub struct FsError {} diff --git a/crates/wasi_fs/wit/deps/clocks/monotonic-clock.wit b/crates/wasi_fs/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000000..4e4dc3a199 --- /dev/null +++ b/crates/wasi_fs/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.2.0; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +/// +/// It is intended for measuring elapsed time. +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + type instant = u64; + + /// A duration of time, in nanoseconds. + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// occured. + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` which will resolve once the given duration has + /// elapsed, starting at the time at which this function was called. + /// occured. + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/crates/wasi_fs/wit/deps/clocks/wall-clock.wit b/crates/wasi_fs/wit/deps/clocks/wall-clock.wit new file mode 100644 index 0000000000..440ca0f336 --- /dev/null +++ b/crates/wasi_fs/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,42 @@ +package wasi:clocks@0.2.0; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func() -> datetime; +} diff --git a/crates/wasi_fs/wit/deps/clocks/world.wit b/crates/wasi_fs/wit/deps/clocks/world.wit new file mode 100644 index 0000000000..c0224572a5 --- /dev/null +++ b/crates/wasi_fs/wit/deps/clocks/world.wit @@ -0,0 +1,6 @@ +package wasi:clocks@0.2.0; + +world imports { + import monotonic-clock; + import wall-clock; +} diff --git a/crates/wasi_fs/wit/deps/filesystem/preopens.wit b/crates/wasi_fs/wit/deps/filesystem/preopens.wit new file mode 100644 index 0000000000..da801f6d60 --- /dev/null +++ b/crates/wasi_fs/wit/deps/filesystem/preopens.wit @@ -0,0 +1,8 @@ +package wasi:filesystem@0.2.0; + +interface preopens { + use types.{descriptor}; + + /// Return the set of preopened directories, and their path. + get-directories: func() -> list>; +} diff --git a/crates/wasi_fs/wit/deps/filesystem/types.wit b/crates/wasi_fs/wit/deps/filesystem/types.wit new file mode 100644 index 0000000000..11108fcda2 --- /dev/null +++ b/crates/wasi_fs/wit/deps/filesystem/types.wit @@ -0,0 +1,634 @@ +package wasi:filesystem@0.2.0; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// File size or length of a region within a file. + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// 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. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// 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. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// 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 descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encourated to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/crates/wasi_fs/wit/deps/filesystem/world.wit b/crates/wasi_fs/wit/deps/filesystem/world.wit new file mode 100644 index 0000000000..663f57920d --- /dev/null +++ b/crates/wasi_fs/wit/deps/filesystem/world.wit @@ -0,0 +1,6 @@ +package wasi:filesystem@0.2.0; + +world imports { + import types; + import preopens; +} diff --git a/crates/wasi_fs/wit/deps/io/error.wit b/crates/wasi_fs/wit/deps/io/error.wit new file mode 100644 index 0000000000..22e5b64894 --- /dev/null +++ b/crates/wasi_fs/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.0; + + +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// provide functions to further "downcast" this error into more specific + /// error information. For example, `error`s returned in streams derived + /// from filesystem types to be described using the filesystem's own + /// error-code type, using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter + /// `borrow` and returns + /// `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + to-debug-string: func() -> string; + } +} diff --git a/crates/wasi_fs/wit/deps/io/poll.wit b/crates/wasi_fs/wit/deps/io/poll.wit new file mode 100644 index 0000000000..ddc67f8b7a --- /dev/null +++ b/crates/wasi_fs/wit/deps/io/poll.wit @@ -0,0 +1,41 @@ +package wasi:io@0.2.0; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// If the list contains more elements than can be indexed with a `u32` + /// value, this function traps. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being reaedy for I/O. + poll: func(in: list>) -> list; +} diff --git a/crates/wasi_fs/wit/deps/io/streams.wit b/crates/wasi_fs/wit/deps/io/streams.wit new file mode 100644 index 0000000000..6d2f871e3b --- /dev/null +++ b/crates/wasi_fs/wit/deps/io/streams.wit @@ -0,0 +1,262 @@ +package wasi:io@0.2.0; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +interface streams { + use error.{error}; + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occured. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivelant to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/crates/wasi_fs/wit/deps/io/world.wit b/crates/wasi_fs/wit/deps/io/world.wit new file mode 100644 index 0000000000..5f0b43fe50 --- /dev/null +++ b/crates/wasi_fs/wit/deps/io/world.wit @@ -0,0 +1,6 @@ +package wasi:io@0.2.0; + +world imports { + import streams; + import poll; +} diff --git a/crates/wasi_fs/wit/wasi.wit b/crates/wasi_fs/wit/wasi.wit new file mode 100644 index 0000000000..c223b458e8 --- /dev/null +++ b/crates/wasi_fs/wit/wasi.wit @@ -0,0 +1 @@ +package wasmtime:wasi;