From f7dc5c3e62d95bd57285f929cefc96e18b0ef050 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 21 Jan 2025 15:42:54 +0100 Subject: [PATCH 1/3] ci: include version in WIT deps Signed-off-by: Roman Volosatovs --- ci/vendor-wit.sh | 4 +- .../p2/wit/deps/config@f4d699b}/store.wit | 0 .../p2/wit/deps/config@f4d699b}/world.wit | 0 .../wit/deps/config@f4d699b/store.wit | 30 + .../wit/deps/config@f4d699b/world.wit | 6 + .../p2/wit/deps/cli@v0.2.3}/command.wit | 0 .../p2/wit/deps/cli@v0.2.3}/environment.wit | 0 .../p2/wit/deps/cli@v0.2.3}/exit.wit | 0 .../p2/wit/deps/cli@v0.2.3}/imports.wit | 0 .../p2/wit/deps/cli@v0.2.3}/run.wit | 0 .../p2/wit/deps/cli@v0.2.3}/stdio.wit | 0 .../p2/wit/deps/cli@v0.2.3}/terminal.wit | 0 .../deps/clocks@v0.2.3}/monotonic-clock.wit | 0 .../p2/wit/deps/clocks@v0.2.3}/timezone.wit | 0 .../p2/wit/deps/clocks@v0.2.3}/wall-clock.wit | 0 .../p2/wit/deps/clocks@v0.2.3}/world.wit | 0 .../wit/deps/filesystem@v0.2.3}/preopens.wit | 0 .../p2/wit/deps/filesystem@v0.2.3}/types.wit | 0 .../p2/wit/deps/filesystem@v0.2.3}/world.wit | 0 .../p2/wit/deps/http@v0.2.3}/handler.wit | 0 .../p2/wit/deps/http@v0.2.3}/proxy.wit | 0 .../p2/wit/deps/http@v0.2.3}/types.wit | 0 .../p2/wit/deps/io@v0.2.3}/error.wit | 0 .../io => src/p2/wit/deps/io@v0.2.3}/poll.wit | 0 .../p2/wit/deps/io@v0.2.3}/streams.wit | 0 .../p2/wit/deps/io@v0.2.3}/world.wit | 0 .../wit/deps/random@v0.2.3}/insecure-seed.wit | 0 .../p2/wit/deps/random@v0.2.3}/insecure.wit | 0 .../p2/wit/deps/random@v0.2.3}/random.wit | 0 .../p2/wit/deps/random@v0.2.3}/world.wit | 0 .../deps/sockets@v0.2.3}/instance-network.wit | 0 .../deps/sockets@v0.2.3}/ip-name-lookup.wit | 0 .../p2/wit/deps/sockets@v0.2.3}/network.wit | 0 .../sockets@v0.2.3}/tcp-create-socket.wit | 0 .../p2/wit/deps/sockets@v0.2.3}/tcp.wit | 0 .../sockets@v0.2.3}/udp-create-socket.wit | 0 .../p2/wit/deps/sockets@v0.2.3}/udp.wit | 0 .../p2/wit/deps/sockets@v0.2.3}/world.wit | 0 .../wit/deps/cli@v0.2.3}/command.wit | 0 .../wit/deps/cli@v0.2.3}/environment.wit | 0 .../wit/deps/cli@v0.2.3}/exit.wit | 0 .../wit/deps/cli@v0.2.3}/imports.wit | 0 .../wit/deps/cli@v0.2.3}/run.wit | 0 .../wit/deps/cli@v0.2.3}/stdio.wit | 0 .../wit/deps/cli@v0.2.3}/terminal.wit | 0 .../deps/clocks@v0.2.3}/monotonic-clock.wit | 0 .../wit/deps/clocks@v0.2.3}/timezone.wit | 0 .../wit/deps/clocks@v0.2.3}/wall-clock.wit | 0 .../wit/deps/clocks@v0.2.3}/world.wit | 0 .../wit/deps/filesystem@v0.2.3}/preopens.wit | 0 .../wit/deps/filesystem@v0.2.3}/types.wit | 0 .../wit/deps/filesystem@v0.2.3}/world.wit | 0 .../wit/deps/http@v0.2.3/handler.wit | 49 ++ .../wasi-http/wit/deps/http@v0.2.3/proxy.wit | 50 ++ .../wasi-http/wit/deps/http@v0.2.3/types.wit | 673 ++++++++++++++++++ .../wit/deps/io@v0.2.3}/error.wit | 0 .../wit/deps/io@v0.2.3}/poll.wit | 0 .../wit/deps/io@v0.2.3}/streams.wit | 0 .../wit/deps/io@v0.2.3}/world.wit | 0 .../wit/deps/random@v0.2.3}/insecure-seed.wit | 0 .../wit/deps/random@v0.2.3}/insecure.wit | 0 .../wit/deps/random@v0.2.3}/random.wit | 0 .../wit/deps/random@v0.2.3}/world.wit | 0 .../deps/sockets@v0.2.3}/instance-network.wit | 0 .../deps/sockets@v0.2.3}/ip-name-lookup.wit | 0 .../wit/deps/sockets@v0.2.3}/network.wit | 0 .../sockets@v0.2.3}/tcp-create-socket.wit | 0 .../wit/deps/sockets@v0.2.3}/tcp.wit | 0 .../sockets@v0.2.3}/udp-create-socket.wit | 0 .../wit/deps/sockets@v0.2.3}/udp.wit | 0 .../wit/deps/sockets@v0.2.3}/world.wit | 0 .../wit/deps/io@v0.2.3}/error.wit | 0 .../wit/deps/io@v0.2.3}/poll.wit | 0 .../wit/deps/io@v0.2.3}/streams.wit | 0 .../wit/deps/io@v0.2.3}/world.wit | 0 .../p2/wit/deps/keyvalue@219ea36}/atomic.wit | 0 .../p2/wit/deps/keyvalue@219ea36}/batch.wit | 0 .../p2/wit/deps/keyvalue@219ea36}/store.wit | 0 .../p2/wit/deps/keyvalue@219ea36}/watch.wit | 0 .../p2/wit/deps/keyvalue@219ea36}/world.wit | 0 .../wit/deps/keyvalue@219ea36/atomic.wit | 22 + .../wit/deps/keyvalue@219ea36/batch.wit | 63 ++ .../wit/deps/keyvalue@219ea36/store.wit | 122 ++++ .../wit/deps/keyvalue@219ea36/watch.wit | 16 + .../wit/deps/keyvalue@219ea36/world.wit | 26 + crates/wasi/wit/deps/cli@v0.2.3/command.wit | 10 + .../wasi/wit/deps/cli@v0.2.3/environment.wit | 22 + crates/wasi/wit/deps/cli@v0.2.3/exit.wit | 17 + crates/wasi/wit/deps/cli@v0.2.3/imports.wit | 36 + crates/wasi/wit/deps/cli@v0.2.3/run.wit | 6 + crates/wasi/wit/deps/cli@v0.2.3/stdio.wit | 26 + crates/wasi/wit/deps/cli@v0.2.3/terminal.wit | 62 ++ .../deps/clocks@v0.2.3/monotonic-clock.wit | 50 ++ .../wasi/wit/deps/clocks@v0.2.3/timezone.wit | 55 ++ .../wit/deps/clocks@v0.2.3/wall-clock.wit | 46 ++ crates/wasi/wit/deps/clocks@v0.2.3/world.wit | 11 + .../wit/deps/filesystem@v0.2.3/preopens.wit | 11 + .../wasi/wit/deps/filesystem@v0.2.3/types.wit | 672 +++++++++++++++++ .../wasi/wit/deps/filesystem@v0.2.3/world.wit | 9 + crates/wasi/wit/deps/io@v0.2.3/error.wit | 34 + crates/wasi/wit/deps/io@v0.2.3/poll.wit | 47 ++ crates/wasi/wit/deps/io@v0.2.3/streams.wit | 290 ++++++++ crates/wasi/wit/deps/io@v0.2.3/world.wit | 10 + .../wit/deps/random@v0.2.3/insecure-seed.wit | 27 + .../wasi/wit/deps/random@v0.2.3/insecure.wit | 25 + crates/wasi/wit/deps/random@v0.2.3/random.wit | 29 + crates/wasi/wit/deps/random@v0.2.3/world.wit | 13 + .../deps/sockets@v0.2.3/instance-network.wit | 11 + .../deps/sockets@v0.2.3/ip-name-lookup.wit | 56 ++ .../wasi/wit/deps/sockets@v0.2.3/network.wit | 169 +++++ .../deps/sockets@v0.2.3/tcp-create-socket.wit | 30 + crates/wasi/wit/deps/sockets@v0.2.3/tcp.wit | 387 ++++++++++ .../deps/sockets@v0.2.3/udp-create-socket.wit | 30 + crates/wasi/wit/deps/sockets@v0.2.3/udp.wit | 288 ++++++++ crates/wasi/wit/deps/sockets@v0.2.3/world.wit | 19 + 115 files changed, 3557 insertions(+), 2 deletions(-) rename crates/wasi-config/{wit/deps/config => src/p2/wit/deps/config@f4d699b}/store.wit (100%) rename crates/wasi-config/{wit/deps/config => src/p2/wit/deps/config@f4d699b}/world.wit (100%) create mode 100644 crates/wasi-config/wit/deps/config@f4d699b/store.wit create mode 100644 crates/wasi-config/wit/deps/config@f4d699b/world.wit rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/command.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/environment.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/exit.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/imports.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/run.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/stdio.wit (100%) rename crates/wasi-http/{wit/deps/cli => src/p2/wit/deps/cli@v0.2.3}/terminal.wit (100%) rename crates/wasi-http/{wit/deps/clocks => src/p2/wit/deps/clocks@v0.2.3}/monotonic-clock.wit (100%) rename crates/wasi-http/{wit/deps/clocks => src/p2/wit/deps/clocks@v0.2.3}/timezone.wit (100%) rename crates/wasi-http/{wit/deps/clocks => src/p2/wit/deps/clocks@v0.2.3}/wall-clock.wit (100%) rename crates/wasi-http/{wit/deps/clocks => src/p2/wit/deps/clocks@v0.2.3}/world.wit (100%) rename crates/wasi-http/{wit/deps/filesystem => src/p2/wit/deps/filesystem@v0.2.3}/preopens.wit (100%) rename crates/wasi-http/{wit/deps/filesystem => src/p2/wit/deps/filesystem@v0.2.3}/types.wit (100%) rename crates/wasi-http/{wit/deps/filesystem => src/p2/wit/deps/filesystem@v0.2.3}/world.wit (100%) rename crates/wasi-http/{wit/deps/http => src/p2/wit/deps/http@v0.2.3}/handler.wit (100%) rename crates/wasi-http/{wit/deps/http => src/p2/wit/deps/http@v0.2.3}/proxy.wit (100%) rename crates/wasi-http/{wit/deps/http => src/p2/wit/deps/http@v0.2.3}/types.wit (100%) rename crates/wasi-http/{wit/deps/io => src/p2/wit/deps/io@v0.2.3}/error.wit (100%) rename crates/wasi-http/{wit/deps/io => src/p2/wit/deps/io@v0.2.3}/poll.wit (100%) rename crates/wasi-http/{wit/deps/io => src/p2/wit/deps/io@v0.2.3}/streams.wit (100%) rename crates/wasi-http/{wit/deps/io => src/p2/wit/deps/io@v0.2.3}/world.wit (100%) rename crates/wasi-http/{wit/deps/random => src/p2/wit/deps/random@v0.2.3}/insecure-seed.wit (100%) rename crates/wasi-http/{wit/deps/random => src/p2/wit/deps/random@v0.2.3}/insecure.wit (100%) rename crates/wasi-http/{wit/deps/random => src/p2/wit/deps/random@v0.2.3}/random.wit (100%) rename crates/wasi-http/{wit/deps/random => src/p2/wit/deps/random@v0.2.3}/world.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/instance-network.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/ip-name-lookup.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/network.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/tcp-create-socket.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/tcp.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/udp-create-socket.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/udp.wit (100%) rename crates/wasi-http/{wit/deps/sockets => src/p2/wit/deps/sockets@v0.2.3}/world.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/command.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/environment.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/exit.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/imports.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/run.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/stdio.wit (100%) rename crates/{wasi/wit/deps/cli => wasi-http/wit/deps/cli@v0.2.3}/terminal.wit (100%) rename crates/{wasi/wit/deps/clocks => wasi-http/wit/deps/clocks@v0.2.3}/monotonic-clock.wit (100%) rename crates/{wasi/wit/deps/clocks => wasi-http/wit/deps/clocks@v0.2.3}/timezone.wit (100%) rename crates/{wasi/wit/deps/clocks => wasi-http/wit/deps/clocks@v0.2.3}/wall-clock.wit (100%) rename crates/{wasi/wit/deps/clocks => wasi-http/wit/deps/clocks@v0.2.3}/world.wit (100%) rename crates/{wasi/wit/deps/filesystem => wasi-http/wit/deps/filesystem@v0.2.3}/preopens.wit (100%) rename crates/{wasi/wit/deps/filesystem => wasi-http/wit/deps/filesystem@v0.2.3}/types.wit (100%) rename crates/{wasi/wit/deps/filesystem => wasi-http/wit/deps/filesystem@v0.2.3}/world.wit (100%) create mode 100644 crates/wasi-http/wit/deps/http@v0.2.3/handler.wit create mode 100644 crates/wasi-http/wit/deps/http@v0.2.3/proxy.wit create mode 100644 crates/wasi-http/wit/deps/http@v0.2.3/types.wit rename crates/{wasi-io/wit/deps/io => wasi-http/wit/deps/io@v0.2.3}/error.wit (100%) rename crates/{wasi-io/wit/deps/io => wasi-http/wit/deps/io@v0.2.3}/poll.wit (100%) rename crates/{wasi-io/wit/deps/io => wasi-http/wit/deps/io@v0.2.3}/streams.wit (100%) rename crates/{wasi-io/wit/deps/io => wasi-http/wit/deps/io@v0.2.3}/world.wit (100%) rename crates/{wasi/wit/deps/random => wasi-http/wit/deps/random@v0.2.3}/insecure-seed.wit (100%) rename crates/{wasi/wit/deps/random => wasi-http/wit/deps/random@v0.2.3}/insecure.wit (100%) rename crates/{wasi/wit/deps/random => wasi-http/wit/deps/random@v0.2.3}/random.wit (100%) rename crates/{wasi/wit/deps/random => wasi-http/wit/deps/random@v0.2.3}/world.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/instance-network.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/ip-name-lookup.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/network.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/tcp-create-socket.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/tcp.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/udp-create-socket.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/udp.wit (100%) rename crates/{wasi/wit/deps/sockets => wasi-http/wit/deps/sockets@v0.2.3}/world.wit (100%) rename crates/{wasi/wit/deps/io => wasi-io/wit/deps/io@v0.2.3}/error.wit (100%) rename crates/{wasi/wit/deps/io => wasi-io/wit/deps/io@v0.2.3}/poll.wit (100%) rename crates/{wasi/wit/deps/io => wasi-io/wit/deps/io@v0.2.3}/streams.wit (100%) rename crates/{wasi/wit/deps/io => wasi-io/wit/deps/io@v0.2.3}/world.wit (100%) rename crates/wasi-keyvalue/{wit/deps/keyvalue => src/p2/wit/deps/keyvalue@219ea36}/atomic.wit (100%) rename crates/wasi-keyvalue/{wit/deps/keyvalue => src/p2/wit/deps/keyvalue@219ea36}/batch.wit (100%) rename crates/wasi-keyvalue/{wit/deps/keyvalue => src/p2/wit/deps/keyvalue@219ea36}/store.wit (100%) rename crates/wasi-keyvalue/{wit/deps/keyvalue => src/p2/wit/deps/keyvalue@219ea36}/watch.wit (100%) rename crates/wasi-keyvalue/{wit/deps/keyvalue => src/p2/wit/deps/keyvalue@219ea36}/world.wit (100%) create mode 100644 crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/atomic.wit create mode 100644 crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/batch.wit create mode 100644 crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/store.wit create mode 100644 crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/watch.wit create mode 100644 crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/world.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/command.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/environment.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/exit.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/imports.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/run.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/stdio.wit create mode 100644 crates/wasi/wit/deps/cli@v0.2.3/terminal.wit create mode 100644 crates/wasi/wit/deps/clocks@v0.2.3/monotonic-clock.wit create mode 100644 crates/wasi/wit/deps/clocks@v0.2.3/timezone.wit create mode 100644 crates/wasi/wit/deps/clocks@v0.2.3/wall-clock.wit create mode 100644 crates/wasi/wit/deps/clocks@v0.2.3/world.wit create mode 100644 crates/wasi/wit/deps/filesystem@v0.2.3/preopens.wit create mode 100644 crates/wasi/wit/deps/filesystem@v0.2.3/types.wit create mode 100644 crates/wasi/wit/deps/filesystem@v0.2.3/world.wit create mode 100644 crates/wasi/wit/deps/io@v0.2.3/error.wit create mode 100644 crates/wasi/wit/deps/io@v0.2.3/poll.wit create mode 100644 crates/wasi/wit/deps/io@v0.2.3/streams.wit create mode 100644 crates/wasi/wit/deps/io@v0.2.3/world.wit create mode 100644 crates/wasi/wit/deps/random@v0.2.3/insecure-seed.wit create mode 100644 crates/wasi/wit/deps/random@v0.2.3/insecure.wit create mode 100644 crates/wasi/wit/deps/random@v0.2.3/random.wit create mode 100644 crates/wasi/wit/deps/random@v0.2.3/world.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/instance-network.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/ip-name-lookup.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/network.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/tcp-create-socket.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/tcp.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/udp-create-socket.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/udp.wit create mode 100644 crates/wasi/wit/deps/sockets@v0.2.3/world.wit diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index b71ca579a7..95ad96c0bd 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -20,7 +20,7 @@ make_vendor() { for package in $packages; do IFS='@' read -r repo tag <<< "$package" - mkdir -p $path/$repo + mkdir -p $path/$package cached_extracted_dir="$cache_dir/$repo-$tag" if [[ ! -d $cached_extracted_dir ]]; then @@ -30,7 +30,7 @@ make_vendor() { rm -rf $cached_extracted_dir/wit/deps* fi - cp -r $cached_extracted_dir/wit/* $path/$repo + cp -r $cached_extracted_dir/wit/* $path/$package done } diff --git a/crates/wasi-config/wit/deps/config/store.wit b/crates/wasi-config/src/p2/wit/deps/config@f4d699b/store.wit similarity index 100% rename from crates/wasi-config/wit/deps/config/store.wit rename to crates/wasi-config/src/p2/wit/deps/config@f4d699b/store.wit diff --git a/crates/wasi-config/wit/deps/config/world.wit b/crates/wasi-config/src/p2/wit/deps/config@f4d699b/world.wit similarity index 100% rename from crates/wasi-config/wit/deps/config/world.wit rename to crates/wasi-config/src/p2/wit/deps/config@f4d699b/world.wit diff --git a/crates/wasi-config/wit/deps/config@f4d699b/store.wit b/crates/wasi-config/wit/deps/config@f4d699b/store.wit new file mode 100644 index 0000000000..794379a754 --- /dev/null +++ b/crates/wasi-config/wit/deps/config@f4d699b/store.wit @@ -0,0 +1,30 @@ +interface store { + /// An error type that encapsulates the different errors that can occur fetching configuration values. + variant error { + /// This indicates an error from an "upstream" config source. + /// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc), + /// the error message is a string. + upstream(string), + /// This indicates an error from an I/O operation. + /// As this could be almost _anything_ (such as a file read, network connection, etc), + /// the error message is a string. + /// Depending on how this ends up being consumed, + /// we may consider moving this to use the `wasi:io/error` type instead. + /// For simplicity right now in supporting multiple implementations, it is being left as a string. + io(string), + } + + /// Gets a configuration value of type `string` associated with the `key`. + /// + /// The value is returned as an `option`. If the key is not found, + /// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned. + get: func( + /// A string key to fetch + key: string + ) -> result, error>; + + /// Gets a list of configuration key-value pairs of type `string`. + /// + /// If an error occurs, an `Err(error)` is returned. + get-all: func() -> result>, error>; +} diff --git a/crates/wasi-config/wit/deps/config@f4d699b/world.wit b/crates/wasi-config/wit/deps/config@f4d699b/world.wit new file mode 100644 index 0000000000..f92f080a57 --- /dev/null +++ b/crates/wasi-config/wit/deps/config@f4d699b/world.wit @@ -0,0 +1,6 @@ +package wasi:config@0.2.0-draft; + +world imports { + /// The interface for wasi:config/store + import store; +} \ No newline at end of file diff --git a/crates/wasi-http/wit/deps/cli/command.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/command.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit diff --git a/crates/wasi-http/wit/deps/cli/environment.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/environment.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/environment.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/environment.wit diff --git a/crates/wasi-http/wit/deps/cli/exit.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/exit.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/exit.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/exit.wit diff --git a/crates/wasi-http/wit/deps/cli/imports.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/imports.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit diff --git a/crates/wasi-http/wit/deps/cli/run.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/run.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/run.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/run.wit diff --git a/crates/wasi-http/wit/deps/cli/stdio.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/stdio.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit diff --git a/crates/wasi-http/wit/deps/cli/terminal.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/terminal.wit similarity index 100% rename from crates/wasi-http/wit/deps/cli/terminal.wit rename to crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/terminal.wit diff --git a/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit b/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/monotonic-clock.wit similarity index 100% rename from crates/wasi-http/wit/deps/clocks/monotonic-clock.wit rename to crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/monotonic-clock.wit diff --git a/crates/wasi-http/wit/deps/clocks/timezone.wit b/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/timezone.wit similarity index 100% rename from crates/wasi-http/wit/deps/clocks/timezone.wit rename to crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/timezone.wit diff --git a/crates/wasi-http/wit/deps/clocks/wall-clock.wit b/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/wall-clock.wit similarity index 100% rename from crates/wasi-http/wit/deps/clocks/wall-clock.wit rename to crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/wall-clock.wit diff --git a/crates/wasi-http/wit/deps/clocks/world.wit b/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/world.wit similarity index 100% rename from crates/wasi-http/wit/deps/clocks/world.wit rename to crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/world.wit diff --git a/crates/wasi-http/wit/deps/filesystem/preopens.wit b/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/preopens.wit similarity index 100% rename from crates/wasi-http/wit/deps/filesystem/preopens.wit rename to crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/preopens.wit diff --git a/crates/wasi-http/wit/deps/filesystem/types.wit b/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/types.wit similarity index 100% rename from crates/wasi-http/wit/deps/filesystem/types.wit rename to crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/types.wit diff --git a/crates/wasi-http/wit/deps/filesystem/world.wit b/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit similarity index 100% rename from crates/wasi-http/wit/deps/filesystem/world.wit rename to crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit diff --git a/crates/wasi-http/wit/deps/http/handler.wit b/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit similarity index 100% rename from crates/wasi-http/wit/deps/http/handler.wit rename to crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit diff --git a/crates/wasi-http/wit/deps/http/proxy.wit b/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/proxy.wit similarity index 100% rename from crates/wasi-http/wit/deps/http/proxy.wit rename to crates/wasi-http/src/p2/wit/deps/http@v0.2.3/proxy.wit diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit similarity index 100% rename from crates/wasi-http/wit/deps/http/types.wit rename to crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit diff --git a/crates/wasi-http/wit/deps/io/error.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit similarity index 100% rename from crates/wasi-http/wit/deps/io/error.wit rename to crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit diff --git a/crates/wasi-http/wit/deps/io/poll.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit similarity index 100% rename from crates/wasi-http/wit/deps/io/poll.wit rename to crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit diff --git a/crates/wasi-http/wit/deps/io/streams.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit similarity index 100% rename from crates/wasi-http/wit/deps/io/streams.wit rename to crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit diff --git a/crates/wasi-http/wit/deps/io/world.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit similarity index 100% rename from crates/wasi-http/wit/deps/io/world.wit rename to crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit diff --git a/crates/wasi-http/wit/deps/random/insecure-seed.wit b/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure-seed.wit similarity index 100% rename from crates/wasi-http/wit/deps/random/insecure-seed.wit rename to crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure-seed.wit diff --git a/crates/wasi-http/wit/deps/random/insecure.wit b/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure.wit similarity index 100% rename from crates/wasi-http/wit/deps/random/insecure.wit rename to crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure.wit diff --git a/crates/wasi-http/wit/deps/random/random.wit b/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/random.wit similarity index 100% rename from crates/wasi-http/wit/deps/random/random.wit rename to crates/wasi-http/src/p2/wit/deps/random@v0.2.3/random.wit diff --git a/crates/wasi-http/wit/deps/random/world.wit b/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit similarity index 100% rename from crates/wasi-http/wit/deps/random/world.wit rename to crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit diff --git a/crates/wasi-http/wit/deps/sockets/instance-network.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/instance-network.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit diff --git a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit diff --git a/crates/wasi-http/wit/deps/sockets/network.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/network.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit diff --git a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/tcp.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit diff --git a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/udp-create-socket.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/udp.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit diff --git a/crates/wasi-http/wit/deps/sockets/world.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit similarity index 100% rename from crates/wasi-http/wit/deps/sockets/world.wit rename to crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit diff --git a/crates/wasi/wit/deps/cli/command.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/command.wit similarity index 100% rename from crates/wasi/wit/deps/cli/command.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/command.wit diff --git a/crates/wasi/wit/deps/cli/environment.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/environment.wit similarity index 100% rename from crates/wasi/wit/deps/cli/environment.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/environment.wit diff --git a/crates/wasi/wit/deps/cli/exit.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/exit.wit similarity index 100% rename from crates/wasi/wit/deps/cli/exit.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/exit.wit diff --git a/crates/wasi/wit/deps/cli/imports.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/imports.wit similarity index 100% rename from crates/wasi/wit/deps/cli/imports.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/imports.wit diff --git a/crates/wasi/wit/deps/cli/run.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/run.wit similarity index 100% rename from crates/wasi/wit/deps/cli/run.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/run.wit diff --git a/crates/wasi/wit/deps/cli/stdio.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/stdio.wit similarity index 100% rename from crates/wasi/wit/deps/cli/stdio.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/stdio.wit diff --git a/crates/wasi/wit/deps/cli/terminal.wit b/crates/wasi-http/wit/deps/cli@v0.2.3/terminal.wit similarity index 100% rename from crates/wasi/wit/deps/cli/terminal.wit rename to crates/wasi-http/wit/deps/cli@v0.2.3/terminal.wit diff --git a/crates/wasi/wit/deps/clocks/monotonic-clock.wit b/crates/wasi-http/wit/deps/clocks@v0.2.3/monotonic-clock.wit similarity index 100% rename from crates/wasi/wit/deps/clocks/monotonic-clock.wit rename to crates/wasi-http/wit/deps/clocks@v0.2.3/monotonic-clock.wit diff --git a/crates/wasi/wit/deps/clocks/timezone.wit b/crates/wasi-http/wit/deps/clocks@v0.2.3/timezone.wit similarity index 100% rename from crates/wasi/wit/deps/clocks/timezone.wit rename to crates/wasi-http/wit/deps/clocks@v0.2.3/timezone.wit diff --git a/crates/wasi/wit/deps/clocks/wall-clock.wit b/crates/wasi-http/wit/deps/clocks@v0.2.3/wall-clock.wit similarity index 100% rename from crates/wasi/wit/deps/clocks/wall-clock.wit rename to crates/wasi-http/wit/deps/clocks@v0.2.3/wall-clock.wit diff --git a/crates/wasi/wit/deps/clocks/world.wit b/crates/wasi-http/wit/deps/clocks@v0.2.3/world.wit similarity index 100% rename from crates/wasi/wit/deps/clocks/world.wit rename to crates/wasi-http/wit/deps/clocks@v0.2.3/world.wit diff --git a/crates/wasi/wit/deps/filesystem/preopens.wit b/crates/wasi-http/wit/deps/filesystem@v0.2.3/preopens.wit similarity index 100% rename from crates/wasi/wit/deps/filesystem/preopens.wit rename to crates/wasi-http/wit/deps/filesystem@v0.2.3/preopens.wit diff --git a/crates/wasi/wit/deps/filesystem/types.wit b/crates/wasi-http/wit/deps/filesystem@v0.2.3/types.wit similarity index 100% rename from crates/wasi/wit/deps/filesystem/types.wit rename to crates/wasi-http/wit/deps/filesystem@v0.2.3/types.wit diff --git a/crates/wasi/wit/deps/filesystem/world.wit b/crates/wasi-http/wit/deps/filesystem@v0.2.3/world.wit similarity index 100% rename from crates/wasi/wit/deps/filesystem/world.wit rename to crates/wasi-http/wit/deps/filesystem@v0.2.3/world.wit diff --git a/crates/wasi-http/wit/deps/http@v0.2.3/handler.wit b/crates/wasi-http/wit/deps/http@v0.2.3/handler.wit new file mode 100644 index 0000000000..6a6c62966f --- /dev/null +++ b/crates/wasi-http/wit/deps/http@v0.2.3/handler.wit @@ -0,0 +1,49 @@ +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +@since(version = 0.2.0) +interface incoming-handler { + @since(version = 0.2.0) + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + @since(version = 0.2.0) + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +@since(version = 0.2.0) +interface outgoing-handler { + @since(version = 0.2.0) + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + @since(version = 0.2.0) + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/crates/wasi-http/wit/deps/http@v0.2.3/proxy.wit b/crates/wasi-http/wit/deps/http@v0.2.3/proxy.wit new file mode 100644 index 0000000000..de3bbe8ae0 --- /dev/null +++ b/crates/wasi-http/wit/deps/http@v0.2.3/proxy.wit @@ -0,0 +1,50 @@ +package wasi:http@0.2.3; + +/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// It is intended to be `include`d in other worlds. +@since(version = 0.2.0) +world imports { + /// HTTP proxies have access to time and randomness. + @since(version = 0.2.0) + import wasi:clocks/monotonic-clock@0.2.3; + @since(version = 0.2.0) + import wasi:clocks/wall-clock@0.2.3; + @since(version = 0.2.0) + import wasi:random/random@0.2.3; + + /// Proxies have standard output and error streams which are expected to + /// terminate in a developer-facing console provided by the host. + @since(version = 0.2.0) + import wasi:cli/stdout@0.2.3; + @since(version = 0.2.0) + import wasi:cli/stderr@0.2.3; + + /// TODO: this is a temporary workaround until component tooling is able to + /// gracefully handle the absence of stdin. Hosts must return an eof stream + /// for this import, which is what wasi-libc + tooling will do automatically + /// when this import is properly removed. + @since(version = 0.2.0) + import wasi:cli/stdin@0.2.3; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + @since(version = 0.2.0) + import outgoing-handler; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +@since(version = 0.2.0) +world proxy { + @since(version = 0.2.0) + include imports; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + @since(version = 0.2.0) + export incoming-handler; +} diff --git a/crates/wasi-http/wit/deps/http@v0.2.3/types.wit b/crates/wasi-http/wit/deps/http@v0.2.3/types.wit new file mode 100644 index 0000000000..2498f180ad --- /dev/null +++ b/crates/wasi-http/wit/deps/http@v0.2.3/types.wit @@ -0,0 +1,673 @@ +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/error@0.2.3.{error as io-error}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.2.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.2.0) + variant scheme { + HTTP, + HTTPS, + other(string) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.2.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.2.0) + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.2.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.2.0) + record field-size-payload { + field-name: option, + field-size: option + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + @since(version = 0.2.0) + http-error-code: func(err: borrow) -> option; + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.2.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.2.1) + type field-name = field-key; + + /// Field keys are always strings. + /// + /// Field keys should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + /// + /// # Deprecation + /// + /// This type has been deprecated in favor of the `field-name` type. + @since(version = 0.2.0) + @deprecated(version = 0.2.2) + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.2.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + @since(version = 0.2.0) + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + @since(version = 0.2.0) + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. + /// + /// An error result will be returned if any `field-name` or `field-value` is + /// syntactically invalid, or if a field is forbidden. + @since(version = 0.2.0) + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields` or is syntactically invalid, an empty list is returned. + /// However, if the name is present but empty, this is represented by a list + /// with one or more empty field-values present. + @since(version = 0.2.0) + get: func(name: field-name) -> list; + + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + @since(version = 0.2.0) + has: func(name: field-name) -> bool; + + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or any of + /// the `field-value`s are syntactically invalid. + @since(version = 0.2.0) + set: func(name: field-name, value: list) -> result<_, header-error>; + + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` is + /// syntactically invalid. + @since(version = 0.2.0) + delete: func(name: field-name) -> result<_, header-error>; + + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or + /// `field-value` are syntactically invalid. + @since(version = 0.2.0) + append: func(name: field-name, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + @since(version = 0.2.0) + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + @since(version = 0.2.0) + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.2.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.2.0) + type trailers = fields; + + /// Represents an incoming HTTP Request. + @since(version = 0.2.0) + resource incoming-request { + + /// Returns the method of the incoming request. + @since(version = 0.2.0) + method: func() -> method; + + /// Returns the path with query parameters from the request, as a string. + @since(version = 0.2.0) + path-with-query: func() -> option; + + /// Returns the protocol scheme from the request. + @since(version = 0.2.0) + scheme: func() -> option; + + /// Returns the authority of the Request's target URI, if present. + @since(version = 0.2.0) + authority: func() -> option; + + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + @since(version = 0.2.0) + resource outgoing-request { + + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + @since(version = 0.2.0) + constructor( + headers: headers + ); + + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + + /// Get the Method for the Request. + @since(version = 0.2.0) + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + @since(version = 0.2.0) + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + @since(version = 0.2.0) + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + @since(version = 0.2.0) + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + @since(version = 0.2.0) + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + @since(version = 0.2.0) + set-scheme: func(scheme: option) -> result; + + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + @since(version = 0.2.0) + authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + @since(version = 0.2.0) + set-authority: func(authority: option) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + @since(version = 0.2.0) + resource request-options { + /// Construct a default `request-options` value. + @since(version = 0.2.0) + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + @since(version = 0.2.0) + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-connect-timeout: func(duration: option) -> result; + + /// The timeout for receiving the first byte of the Response body. + @since(version = 0.2.0) + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-first-byte-timeout: func(duration: option) -> result; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + @since(version = 0.2.0) + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + @since(version = 0.2.0) + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + @since(version = 0.2.0) + resource response-outparam { + + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + @since(version = 0.2.0) + set: static func( + param: response-outparam, + response: result, + ); + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.2.0) + type status-code = u16; + + /// Represents an incoming HTTP Response. + @since(version = 0.2.0) + resource incoming-response { + + /// Returns the status code from the incoming response. + @since(version = 0.2.0) + status: func() -> status-code; + + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + @since(version = 0.2.0) + resource incoming-body { + + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + @since(version = 0.2.0) + %stream: func() -> result; + + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + @since(version = 0.2.0) + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventually return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + @since(version = 0.2.0) + resource future-trailers { + + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the contents of the trailers, or an error which occurred, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occurred receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + @since(version = 0.2.0) + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + @since(version = 0.2.0) + resource outgoing-response { + + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + @since(version = 0.2.0) + constructor(headers: headers); + + /// Get the HTTP Status Code for the Response. + @since(version = 0.2.0) + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + @since(version = 0.2.0) + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occurred. The implementation should propagate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + @since(version = 0.2.0) + resource outgoing-body { + + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + @since(version = 0.2.0) + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + @since(version = 0.2.0) + finish: static func( + this: outgoing-body, + trailers: option + ) -> result<_, error-code>; + } + + /// Represents a future which may eventually return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + @since(version = 0.2.0) + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have received successfully, or that an error + /// occurred. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + @since(version = 0.2.0) + get: func() -> option>>; + } +} diff --git a/crates/wasi-io/wit/deps/io/error.wit b/crates/wasi-http/wit/deps/io@v0.2.3/error.wit similarity index 100% rename from crates/wasi-io/wit/deps/io/error.wit rename to crates/wasi-http/wit/deps/io@v0.2.3/error.wit diff --git a/crates/wasi-io/wit/deps/io/poll.wit b/crates/wasi-http/wit/deps/io@v0.2.3/poll.wit similarity index 100% rename from crates/wasi-io/wit/deps/io/poll.wit rename to crates/wasi-http/wit/deps/io@v0.2.3/poll.wit diff --git a/crates/wasi-io/wit/deps/io/streams.wit b/crates/wasi-http/wit/deps/io@v0.2.3/streams.wit similarity index 100% rename from crates/wasi-io/wit/deps/io/streams.wit rename to crates/wasi-http/wit/deps/io@v0.2.3/streams.wit diff --git a/crates/wasi-io/wit/deps/io/world.wit b/crates/wasi-http/wit/deps/io@v0.2.3/world.wit similarity index 100% rename from crates/wasi-io/wit/deps/io/world.wit rename to crates/wasi-http/wit/deps/io@v0.2.3/world.wit diff --git a/crates/wasi/wit/deps/random/insecure-seed.wit b/crates/wasi-http/wit/deps/random@v0.2.3/insecure-seed.wit similarity index 100% rename from crates/wasi/wit/deps/random/insecure-seed.wit rename to crates/wasi-http/wit/deps/random@v0.2.3/insecure-seed.wit diff --git a/crates/wasi/wit/deps/random/insecure.wit b/crates/wasi-http/wit/deps/random@v0.2.3/insecure.wit similarity index 100% rename from crates/wasi/wit/deps/random/insecure.wit rename to crates/wasi-http/wit/deps/random@v0.2.3/insecure.wit diff --git a/crates/wasi/wit/deps/random/random.wit b/crates/wasi-http/wit/deps/random@v0.2.3/random.wit similarity index 100% rename from crates/wasi/wit/deps/random/random.wit rename to crates/wasi-http/wit/deps/random@v0.2.3/random.wit diff --git a/crates/wasi/wit/deps/random/world.wit b/crates/wasi-http/wit/deps/random@v0.2.3/world.wit similarity index 100% rename from crates/wasi/wit/deps/random/world.wit rename to crates/wasi-http/wit/deps/random@v0.2.3/world.wit diff --git a/crates/wasi/wit/deps/sockets/instance-network.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/instance-network.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/instance-network.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/instance-network.wit diff --git a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/ip-name-lookup.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/ip-name-lookup.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/ip-name-lookup.wit diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/network.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/network.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/network.wit diff --git a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/tcp-create-socket.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/tcp-create-socket.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/tcp-create-socket.wit diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/tcp.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/tcp.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/tcp.wit diff --git a/crates/wasi/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/udp-create-socket.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/udp-create-socket.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/udp-create-socket.wit diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/udp.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/udp.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/udp.wit diff --git a/crates/wasi/wit/deps/sockets/world.wit b/crates/wasi-http/wit/deps/sockets@v0.2.3/world.wit similarity index 100% rename from crates/wasi/wit/deps/sockets/world.wit rename to crates/wasi-http/wit/deps/sockets@v0.2.3/world.wit diff --git a/crates/wasi/wit/deps/io/error.wit b/crates/wasi-io/wit/deps/io@v0.2.3/error.wit similarity index 100% rename from crates/wasi/wit/deps/io/error.wit rename to crates/wasi-io/wit/deps/io@v0.2.3/error.wit diff --git a/crates/wasi/wit/deps/io/poll.wit b/crates/wasi-io/wit/deps/io@v0.2.3/poll.wit similarity index 100% rename from crates/wasi/wit/deps/io/poll.wit rename to crates/wasi-io/wit/deps/io@v0.2.3/poll.wit diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi-io/wit/deps/io@v0.2.3/streams.wit similarity index 100% rename from crates/wasi/wit/deps/io/streams.wit rename to crates/wasi-io/wit/deps/io@v0.2.3/streams.wit diff --git a/crates/wasi/wit/deps/io/world.wit b/crates/wasi-io/wit/deps/io@v0.2.3/world.wit similarity index 100% rename from crates/wasi/wit/deps/io/world.wit rename to crates/wasi-io/wit/deps/io@v0.2.3/world.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit b/crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/atomic.wit similarity index 100% rename from crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit rename to crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/atomic.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit b/crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/batch.wit similarity index 100% rename from crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit rename to crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/batch.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit b/crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/store.wit similarity index 100% rename from crates/wasi-keyvalue/wit/deps/keyvalue/store.wit rename to crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/store.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit b/crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/watch.wit similarity index 100% rename from crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit rename to crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/watch.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit b/crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/world.wit similarity index 100% rename from crates/wasi-keyvalue/wit/deps/keyvalue/world.wit rename to crates/wasi-keyvalue/src/p2/wit/deps/keyvalue@219ea36/world.wit diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/atomic.wit b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/atomic.wit new file mode 100644 index 0000000000..059efc4889 --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/atomic.wit @@ -0,0 +1,22 @@ +/// A keyvalue interface that provides atomic operations. +/// +/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to +/// fail, it will appear to the invoker of the atomic operation that the action either completed +/// successfully or did nothing at all. +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface atomics { + use store.{bucket, error}; + + /// Atomically increment the value associated with the key in the store by the given delta. It + /// returns the new value. + /// + /// If the key does not exist in the store, it creates a new key-value pair with the value set + /// to the given delta. + /// + /// If any other error occurs, it returns an `Err(error)`. + increment: func(bucket: borrow, key: string, delta: u64) -> result; +} \ No newline at end of file diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/batch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/batch.wit new file mode 100644 index 0000000000..70c05feb91 --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/batch.wit @@ -0,0 +1,63 @@ +/// A keyvalue interface that provides batch operations. +/// +/// A batch operation is an operation that operates on multiple keys at once. +/// +/// Batch operations are useful for reducing network round-trip time. For example, if you want to +/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1 +/// batch get operation. The batch operation is faster because it only needs to make 1 network call +/// instead of 100. +/// +/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some +/// of the keys may have been modified and some may not. +/// +/// This interface does has the same consistency guarantees as the `store` interface, meaning that +/// you should be able to "read your writes." +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface batch { + use store.{bucket, error}; + + /// Get the key-value pairs associated with the keys in the store. It returns a list of + /// key-value pairs. + /// + /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the + /// list. + /// + /// MAY show an out-of-date value if there are concurrent writes to the store. + /// + /// If any other error occurs, it returns an `Err(error)`. + get-many: func(bucket: borrow, keys: list) -> result>>>, error>; + + /// Set the values associated with the keys in the store. If the key already exists in the + /// store, it overwrites the value. + /// + /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. + /// + /// If any of the keys do not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already set. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be set while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; + + /// Delete the key-value pairs associated with the keys in the store. + /// + /// Note that the key-value pairs are not guaranteed to be deleted in the order they are + /// provided. + /// + /// If any of the keys do not exist in the store, it skips the key. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be deleted while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + delete-many: func(bucket: borrow, keys: list) -> result<_, error>; +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/store.wit b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/store.wit new file mode 100644 index 0000000000..3354ea2f32 --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/store.wit @@ -0,0 +1,122 @@ +/// A keyvalue interface that provides eventually consistent key-value operations. +/// +/// Each of these operations acts on a single key-value pair. +/// +/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is +/// the common denominator for all data types defined by different key-value stores to handle data, +/// ensuring compatibility between different key-value stores. Note: the clients will be expecting +/// serialization/deserialization overhead to be handled by the key-value store. The value could be +/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects. +/// +/// Data consistency in a key value store refers to the guarantee that once a write operation +/// completes, all subsequent read operations will return the value that was written. +/// +/// Any implementation of this interface must have enough consistency to guarantee "reading your +/// writes." In particular, this means that the client should never get a value that is older than +/// the one it wrote, but it MAY get a newer value if one was written around the same time. These +/// guarantees only apply to the same client (which will likely be provided by the host or an +/// external capability of some kind). In this context a "client" is referring to the caller or +/// guest that is consuming this interface. Once a write request is committed by a specific client, +/// all subsequent read requests by the same client will reflect that write or any subsequent +/// writes. Another client running in a different context may or may not immediately see the result +/// due to the replication lag. As an example of all of this, if a value at a given key is A, and +/// the client writes B, then immediately reads, it should get B. If something else writes C in +/// quick succession, then the client may get C. However, a client running in a separate context may +/// still see A or B +interface store { + /// The set of errors which may be raised by functions in this package + variant error { + /// The host does not recognize the store identifier requested. + no-such-store, + + /// The requesting component does not have access to the specified store + /// (which may or may not exist). + access-denied, + + /// Some implementation-specific error has occurred (e.g. I/O) + other(string) + } + + /// A response to a `list-keys` operation. + record key-response { + /// The list of keys returned by the query. + keys: list, + /// The continuation token to use to fetch the next page of keys. If this is `null`, then + /// there are no more keys to fetch. + cursor: option + } + + /// Get the bucket with the specified identifier. + /// + /// `identifier` must refer to a bucket provided by the host. + /// + /// `error::no-such-store` will be raised if the `identifier` is not recognized. + open: func(identifier: string) -> result; + + /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the + /// bucket, and the bucket itself acts as a collection of all these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores can very + /// depending on the specific implementation. For example: + /// + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs + resource bucket { + /// Get the value associated with the specified `key` + /// + /// The value is returned as an option. If the key-value pair exists in the + /// store, it returns `Ok(value)`. If the key does not exist in the + /// store, it returns `Ok(none)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + get: func(key: string) -> result>, error>; + + /// Set the value associated with the key in the store. If the key already + /// exists in the store, it overwrites the value. + /// + /// If the key does not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. + set: func(key: string, value: list) -> result<_, error>; + + /// Delete the key-value pair associated with the key in the store. + /// + /// If the key does not exist in the store, it does nothing. + /// + /// If any other error occurs, it returns an `Err(error)`. + delete: func(key: string) -> result<_, error>; + + /// Check if the key exists in the store. + /// + /// If the key exists in the store, it returns `Ok(true)`. If the key does + /// not exist in the store, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + exists: func(key: string) -> result; + + /// Get all the keys in the store with an optional cursor (for use in pagination). It + /// returns a list of keys. Please note that for most KeyValue implementations, this is a + /// can be a very expensive operation and so it should be used judiciously. Implementations + /// can return any number of keys in a single response, but they should never attempt to + /// send more data than is reasonable (i.e. on a small edge device, this may only be a few + /// KB, while on a large machine this could be several MB). Any response should also return + /// a cursor that can be used to fetch the next page of keys. See the `key-response` record + /// for more information. + /// + /// Note that the keys are not guaranteed to be returned in any particular order. + /// + /// If the store is empty, it returns an empty list. + /// + /// MAY show an out-of-date list of keys if there are concurrent writes to the store. + /// + /// If any error occurs, it returns an `Err(error)`. + list-keys: func(cursor: option) -> result; + } +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/watch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/watch.wit new file mode 100644 index 0000000000..ff13f7523e --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/watch.wit @@ -0,0 +1,16 @@ +/// A keyvalue interface that provides watch operations. +/// +/// This interface is used to provide event-driven mechanisms to handle +/// keyvalue changes. +interface watcher { + /// A keyvalue interface that provides handle-watch operations. + use store.{bucket}; + + /// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket` + /// that can be used to interact with the store. + on-set: func(bucket: bucket, key: string, value: list); + + /// Handle the `delete` event for the given bucket and key. It includes a reference to the + /// `bucket` that can be used to interact with the store. + on-delete: func(bucket: bucket, key: string); +} \ No newline at end of file diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/world.wit b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/world.wit new file mode 100644 index 0000000000..066148c1fa --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue@219ea36/world.wit @@ -0,0 +1,26 @@ +package wasi:keyvalue@0.2.0-draft; + +/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores. +/// Components targeting this world will be able to do: +/// +/// 1. CRUD (create, read, update, delete) operations on key-value stores. +/// 2. Atomic `increment` and CAS (compare-and-swap) operations. +/// 3. Batch operations that can reduce the number of round trips to the network. +world imports { + /// The `store` capability allows the component to perform eventually consistent operations on + /// the key-value store. + import store; + + /// The `atomic` capability allows the component to perform atomic / `increment` and CAS + /// (compare-and-swap) operations. + import atomics; + + /// The `batch` capability allows the component to perform eventually consistent batch + /// operations that can reduce the number of round trips to the network. + import batch; +} + +world watch-service { + include imports; + export watcher; +} \ No newline at end of file diff --git a/crates/wasi/wit/deps/cli@v0.2.3/command.wit b/crates/wasi/wit/deps/cli@v0.2.3/command.wit new file mode 100644 index 0000000000..3a81766d64 --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.3; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/environment.wit b/crates/wasi/wit/deps/cli@v0.2.3/environment.wit new file mode 100644 index 0000000000..2f449bd7c1 --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.2.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.2.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.2.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.2.0) + initial-cwd: func() -> option; +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/exit.wit b/crates/wasi/wit/deps/cli@v0.2.3/exit.wit new file mode 100644 index 0000000000..427935c8d0 --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.2.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.2.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/imports.wit b/crates/wasi/wit/deps/cli@v0.2.3/imports.wit new file mode 100644 index 0000000000..8b4e3975ec --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.3; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.3; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.3; + @since(version = 0.2.0) + include wasi:random/imports@0.2.3; + @since(version = 0.2.0) + include wasi:io/imports@0.2.3; + + @since(version = 0.2.0) + import environment; + @since(version = 0.2.0) + import exit; + @since(version = 0.2.0) + import stdin; + @since(version = 0.2.0) + import stdout; + @since(version = 0.2.0) + import stderr; + @since(version = 0.2.0) + import terminal-input; + @since(version = 0.2.0) + import terminal-output; + @since(version = 0.2.0) + import terminal-stdin; + @since(version = 0.2.0) + import terminal-stdout; + @since(version = 0.2.0) + import terminal-stderr; +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/run.wit b/crates/wasi/wit/deps/cli@v0.2.3/run.wit new file mode 100644 index 0000000000..655346efb6 --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.2.0) +interface run { + /// Run the program. + @since(version = 0.2.0) + run: func() -> result; +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/stdio.wit b/crates/wasi/wit/deps/cli@v0.2.3/stdio.wit new file mode 100644 index 0000000000..1b54f5318a --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/stdio.wit @@ -0,0 +1,26 @@ +@since(version = 0.2.0) +interface stdin { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream}; + + @since(version = 0.2.0) + get-stdin: func() -> input-stream; +} + +@since(version = 0.2.0) +interface stdout { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{output-stream}; + + @since(version = 0.2.0) + get-stdout: func() -> output-stream; +} + +@since(version = 0.2.0) +interface stderr { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{output-stream}; + + @since(version = 0.2.0) + get-stderr: func() -> output-stream; +} diff --git a/crates/wasi/wit/deps/cli@v0.2.3/terminal.wit b/crates/wasi/wit/deps/cli@v0.2.3/terminal.wit new file mode 100644 index 0000000000..d305498c64 --- /dev/null +++ b/crates/wasi/wit/deps/cli@v0.2.3/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.2.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.2.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.2.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.2.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdin { + @since(version = 0.2.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdout { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stderr { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasi/wit/deps/clocks@v0.2.3/monotonic-clock.wit b/crates/wasi/wit/deps/clocks@v0.2.3/monotonic-clock.wit new file mode 100644 index 0000000000..c676fb84d8 --- /dev/null +++ b/crates/wasi/wit/deps/clocks@v0.2.3/monotonic-clock.wit @@ -0,0 +1,50 @@ +package wasi:clocks@0.2.3; +/// 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. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{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. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/crates/wasi/wit/deps/clocks@v0.2.3/timezone.wit b/crates/wasi/wit/deps/clocks@v0.2.3/timezone.wit new file mode 100644 index 0000000000..b43e93b233 --- /dev/null +++ b/crates/wasi/wit/deps/clocks@v0.2.3/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.3; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/wit/deps/clocks@v0.2.3/wall-clock.wit b/crates/wasi/wit/deps/clocks@v0.2.3/wall-clock.wit new file mode 100644 index 0000000000..e00ce08933 --- /dev/null +++ b/crates/wasi/wit/deps/clocks@v0.2.3/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.3; +/// 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. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + 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 + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} diff --git a/crates/wasi/wit/deps/clocks@v0.2.3/world.wit b/crates/wasi/wit/deps/clocks@v0.2.3/world.wit new file mode 100644 index 0000000000..05f04f797d --- /dev/null +++ b/crates/wasi/wit/deps/clocks@v0.2.3/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import monotonic-clock; + @since(version = 0.2.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/wit/deps/filesystem@v0.2.3/preopens.wit b/crates/wasi/wit/deps/filesystem@v0.2.3/preopens.wit new file mode 100644 index 0000000000..cea97495b5 --- /dev/null +++ b/crates/wasi/wit/deps/filesystem@v0.2.3/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.2.3; + +@since(version = 0.2.0) +interface preopens { + @since(version = 0.2.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.2.0) + get-directories: func() -> list>; +} diff --git a/crates/wasi/wit/deps/filesystem@v0.2.3/types.wit b/crates/wasi/wit/deps/filesystem@v0.2.3/types.wit new file mode 100644 index 0000000000..d229a21f48 --- /dev/null +++ b/crates/wasi/wit/deps/filesystem@v0.2.3/types.wit @@ -0,0 +1,672 @@ +package wasi:filesystem@0.2.3; +/// 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 +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.3.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.2.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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 integrity + /// 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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 POSIX. + @since(version = 0.2.0) + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + /// + /// 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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 encouraged 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. + @since(version = 0.2.0) + 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`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/crates/wasi/wit/deps/filesystem@v0.2.3/world.wit b/crates/wasi/wit/deps/filesystem@v0.2.3/world.wit new file mode 100644 index 0000000000..29405bc2cc --- /dev/null +++ b/crates/wasi/wit/deps/filesystem@v0.2.3/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import types; + @since(version = 0.2.0) + import preopens; +} diff --git a/crates/wasi/wit/deps/io@v0.2.3/error.wit b/crates/wasi/wit/deps/io@v0.2.3/error.wit new file mode 100644 index 0000000000..97c6068779 --- /dev/null +++ b/crates/wasi/wit/deps/io@v0.2.3/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 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 + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/crates/wasi/wit/deps/io@v0.2.3/poll.wit b/crates/wasi/wit/deps/io@v0.2.3/poll.wit new file mode 100644 index 0000000000..9bcbe8e036 --- /dev/null +++ b/crates/wasi/wit/deps/io@v0.2.3/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// 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 ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/crates/wasi/wit/deps/io@v0.2.3/streams.wit b/crates/wasi/wit/deps/io@v0.2.3/streams.wit new file mode 100644 index 0000000000..0de0846293 --- /dev/null +++ b/crates/wasi/wit/deps/io@v0.2.3/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// 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. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + 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`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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`. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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 + /// ``` + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + 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 occurred. 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. + @since(version = 0.2.0) + 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. + @since(version = 0.2.0) + 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 + /// ``` + @since(version = 0.2.0) + 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 equivalent 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`. + @since(version = 0.2.0) + 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`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/crates/wasi/wit/deps/io@v0.2.3/world.wit b/crates/wasi/wit/deps/io@v0.2.3/world.wit new file mode 100644 index 0000000000..f1d2102dca --- /dev/null +++ b/crates/wasi/wit/deps/io@v0.2.3/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/crates/wasi/wit/deps/random@v0.2.3/insecure-seed.wit b/crates/wasi/wit/deps/random@v0.2.3/insecure-seed.wit new file mode 100644 index 0000000000..67d024d5bf --- /dev/null +++ b/crates/wasi/wit/deps/random@v0.2.3/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.2.3; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.2.0) + insecure-seed: func() -> tuple; +} diff --git a/crates/wasi/wit/deps/random@v0.2.3/insecure.wit b/crates/wasi/wit/deps/random@v0.2.3/insecure.wit new file mode 100644 index 0000000000..a07dfab327 --- /dev/null +++ b/crates/wasi/wit/deps/random@v0.2.3/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.3; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.2.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.2.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasi/wit/deps/random@v0.2.3/random.wit b/crates/wasi/wit/deps/random@v0.2.3/random.wit new file mode 100644 index 0000000000..91957e6330 --- /dev/null +++ b/crates/wasi/wit/deps/random@v0.2.3/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.3; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.2.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.2.0) + get-random-u64: func() -> u64; +} diff --git a/crates/wasi/wit/deps/random@v0.2.3/world.wit b/crates/wasi/wit/deps/random@v0.2.3/world.wit new file mode 100644 index 0000000000..0c1218f36e --- /dev/null +++ b/crates/wasi/wit/deps/random@v0.2.3/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import random; + + @since(version = 0.2.0) + import insecure; + + @since(version = 0.2.0) + import insecure-seed; +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/instance-network.wit b/crates/wasi/wit/deps/sockets@v0.2.3/instance-network.wit new file mode 100644 index 0000000000..5f6e6c1cc9 --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/instance-network.wit @@ -0,0 +1,11 @@ + +/// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) +interface instance-network { + @since(version = 0.2.0) + use network.{network}; + + /// Get a handle to the default network. + @since(version = 0.2.0) + instance-network: func() -> network; +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/ip-name-lookup.wit b/crates/wasi/wit/deps/sockets@v0.2.3/ip-name-lookup.wit new file mode 100644 index 0000000000..c1d8a47c16 --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/ip-name-lookup.wit @@ -0,0 +1,56 @@ +@since(version = 0.2.0) +interface ip-name-lookup { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-address}; + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + resolve-addresses: func(network: borrow, name: string) -> result; + + @since(version = 0.2.0) + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/network.wit b/crates/wasi/wit/deps/sockets@v0.2.3/network.wit new file mode 100644 index 0000000000..f3f60a3709 --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/network.wit @@ -0,0 +1,169 @@ +@since(version = 0.2.0) +interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.3.{error}; + + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + /// Attempts to extract a network-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 + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.2.0) + type ipv4-address = tuple; + @since(version = 0.2.0) + type ipv6-address = tuple; + + @since(version = 0.2.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.2.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.2.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.2.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/tcp-create-socket.wit b/crates/wasi/wit/deps/sockets@v0.2.3/tcp-create-socket.wit new file mode 100644 index 0000000000..eedbd30768 --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/tcp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface tcp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/tcp.wit b/crates/wasi/wit/deps/sockets@v0.2.3/tcp.wit new file mode 100644 index 0000000000..b4cd87fcef --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/tcp.wit @@ -0,0 +1,387 @@ +@since(version = 0.2.0) +interface tcp { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + @since(version = 0.2.0) + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connected` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) + keep-alive-enabled: func() -> result; + @since(version = 0.2.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-interval: func() -> result; + @since(version = 0.2.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-count: func() -> result; + @since(version = 0.2.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + hop-limit: func() -> result; + @since(version = 0.2.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for more information. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent; shutting down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/udp-create-socket.wit b/crates/wasi/wit/deps/sockets@v0.2.3/udp-create-socket.wit new file mode 100644 index 0000000000..e8eeacbfef --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/udp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface udp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/udp.wit b/crates/wasi/wit/deps/sockets@v0.2.3/udp.wit new file mode 100644 index 0000000000..01901ca27f --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/udp.wit @@ -0,0 +1,288 @@ +@since(version = 0.2.0) +interface udp { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + @since(version = 0.2.0) + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + @since(version = 0.2.0) + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + /// A UDP socket handle. + @since(version = 0.2.0) + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + unicast-hop-limit: func() -> result; + @since(version = 0.2.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasi/wit/deps/sockets@v0.2.3/world.wit b/crates/wasi/wit/deps/sockets@v0.2.3/world.wit new file mode 100644 index 0000000000..2f0ad0d7c9 --- /dev/null +++ b/crates/wasi/wit/deps/sockets@v0.2.3/world.wit @@ -0,0 +1,19 @@ +package wasi:sockets@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import instance-network; + @since(version = 0.2.0) + import network; + @since(version = 0.2.0) + import udp; + @since(version = 0.2.0) + import udp-create-socket; + @since(version = 0.2.0) + import tcp; + @since(version = 0.2.0) + import tcp-create-socket; + @since(version = 0.2.0) + import ip-name-lookup; +} From 76dc8a821d6baa37c559baa04bb63c1371c38856 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 21 Jan 2025 15:49:31 +0100 Subject: [PATCH 2/3] ci: add subdir support Signed-off-by: Roman Volosatovs --- ci/vendor-wit.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index 95ad96c0bd..37a7fae2c9 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -19,18 +19,18 @@ make_vendor() { mkdir -p $path for package in $packages; do - IFS='@' read -r repo tag <<< "$package" - mkdir -p $path/$package + IFS='@' read -r repo tag subdir <<< "$package" + mkdir -p "$path/$package" cached_extracted_dir="$cache_dir/$repo-$tag" if [[ ! -d $cached_extracted_dir ]]; then mkdir -p $cached_extracted_dir curl -sL https://github.com/WebAssembly/wasi-$repo/archive/$tag.tar.gz | \ tar xzf - --strip-components=1 -C $cached_extracted_dir - rm -rf $cached_extracted_dir/wit/deps* + rm -rf $cached_extracted_dir/${subdir:-"wit"}/deps* fi - cp -r $cached_extracted_dir/wit/* $path/$package + cp -r $cached_extracted_dir/${subdir:-"wit"}/* $path/$package done } From 43f26394ede989e89aef30689df36225349370eb Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 3 Feb 2025 19:17:27 +0100 Subject: [PATCH 3/3] feat: implement wasip3 Signed-off-by: Roman Volosatovs --- ci/vendor-wit.sh | 8 + crates/test-programs/artifacts/build.rs | 10 + .../test-programs/src/bin/clocks_0_3_sleep.rs | 70 ++ .../src/bin/random_0_3_imports.rs | 49 ++ .../src/bin/sockets_0_3_ip_name_lookup.rs | 94 +++ .../src/bin/sockets_0_3_tcp_bind.rs | 179 +++++ .../src/bin/sockets_0_3_tcp_connect.rs | 144 ++++ .../bin/sockets_0_3_tcp_sample_application.rs | 91 +++ .../src/bin/sockets_0_3_tcp_sockopts.rs | 234 ++++++ .../src/bin/sockets_0_3_tcp_states.rs | 190 +++++ .../src/bin/sockets_0_3_tcp_streams.rs | 149 ++++ crates/test-programs/src/lib.rs | 1 + crates/test-programs/src/p3/mod.rs | 21 + crates/test-programs/src/p3/sockets.rs | 147 ++++ crates/test-programs/src/sockets.rs | 4 +- crates/wasi/Cargo.toml | 5 +- crates/wasi/src/lib.rs | 2 + crates/wasi/src/p3/bindings.rs | 277 +++++++ crates/wasi/src/p3/cli/host.rs | 19 + crates/wasi/src/p3/cli/mod.rs | 109 +++ crates/wasi/src/p3/clocks/host.rs | 88 +++ crates/wasi/src/p3/clocks/mod.rs | 201 +++++ crates/wasi/src/p3/clocks/p3/bindings.rs | 104 +++ crates/wasi/src/p3/clocks/p3/mod.rs | 14 + .../monotonic-clock.wit | 45 ++ .../timezone.wit | 55 ++ .../wall-clock.wit | 46 ++ .../clocks@3850f9d@wit-0.3.0-draft/world.wit | 11 + crates/wasi/src/p3/clocks/p3/wit/package.wit | 1 + crates/wasi/src/p3/mod.rs | 72 ++ crates/wasi/src/p3/random/host.rs | 47 ++ crates/wasi/src/p3/random/mod.rs | 171 +++++ .../src/p3/sockets/host/ip_name_lookup.rs | 61 ++ crates/wasi/src/p3/sockets/host/mod.rs | 2 + crates/wasi/src/p3/sockets/host/types/mod.rs | 7 + crates/wasi/src/p3/sockets/host/types/tcp.rs | 475 ++++++++++++ crates/wasi/src/p3/sockets/host/types/udp.rs | 123 +++ crates/wasi/src/p3/sockets/mod.rs | 217 ++++++ crates/wasi/src/p3/sockets/tcp.rs | 411 ++++++++++ crates/wasi/src/p3/sockets/util.rs | 263 +++++++ .../cli@01e1d4b@wit-0.3.0-draft/command.wit | 10 + .../environment.wit | 22 + .../deps/cli@01e1d4b@wit-0.3.0-draft/exit.wit | 17 + .../cli@01e1d4b@wit-0.3.0-draft/imports.wit | 34 + .../deps/cli@01e1d4b@wit-0.3.0-draft/run.wit | 6 + .../cli@01e1d4b@wit-0.3.0-draft/stdio.wit | 17 + .../cli@01e1d4b@wit-0.3.0-draft/terminal.wit | 62 ++ .../monotonic-clock.wit | 45 ++ .../timezone.wit | 55 ++ .../wall-clock.wit | 46 ++ .../clocks@3850f9d@wit-0.3.0-draft/world.wit | 11 + .../preopens.wit | 11 + .../types.wit | 629 +++++++++++++++ .../world.wit | 9 + .../insecure-seed.wit | 27 + .../insecure.wit | 25 + .../random@3e99124@wit-0.3.0-draft/random.wit | 29 + .../random@3e99124@wit-0.3.0-draft/world.wit | 13 + .../ip-name-lookup.wit | 62 ++ .../sockets@8069eb9@wit-0.3.0-draft/types.wit | 726 ++++++++++++++++++ .../sockets@8069eb9@wit-0.3.0-draft/world.wit | 9 + crates/wasi/src/p3/wit/package.wit | 1 + crates/wasi/tests/all/main.rs | 2 + crates/wasi/tests/all/p3/clocks.rs | 9 + crates/wasi/tests/all/p3/mod.rs | 123 +++ crates/wasi/tests/all/p3/random.rs | 9 + crates/wasi/tests/all/p3/sockets.rs | 45 ++ 67 files changed, 6268 insertions(+), 3 deletions(-) create mode 100644 crates/test-programs/src/bin/clocks_0_3_sleep.rs create mode 100644 crates/test-programs/src/bin/random_0_3_imports.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_ip_name_lookup.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_bind.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_connect.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_sample_application.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_sockopts.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_states.rs create mode 100644 crates/test-programs/src/bin/sockets_0_3_tcp_streams.rs create mode 100644 crates/test-programs/src/p3/mod.rs create mode 100644 crates/test-programs/src/p3/sockets.rs create mode 100644 crates/wasi/src/p3/bindings.rs create mode 100644 crates/wasi/src/p3/cli/host.rs create mode 100644 crates/wasi/src/p3/cli/mod.rs create mode 100644 crates/wasi/src/p3/clocks/host.rs create mode 100644 crates/wasi/src/p3/clocks/mod.rs create mode 100644 crates/wasi/src/p3/clocks/p3/bindings.rs create mode 100644 crates/wasi/src/p3/clocks/p3/mod.rs create mode 100644 crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit create mode 100644 crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit create mode 100644 crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit create mode 100644 crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi/src/p3/clocks/p3/wit/package.wit create mode 100644 crates/wasi/src/p3/mod.rs create mode 100644 crates/wasi/src/p3/random/host.rs create mode 100644 crates/wasi/src/p3/random/mod.rs create mode 100644 crates/wasi/src/p3/sockets/host/ip_name_lookup.rs create mode 100644 crates/wasi/src/p3/sockets/host/mod.rs create mode 100644 crates/wasi/src/p3/sockets/host/types/mod.rs create mode 100644 crates/wasi/src/p3/sockets/host/types/tcp.rs create mode 100644 crates/wasi/src/p3/sockets/host/types/udp.rs create mode 100644 crates/wasi/src/p3/sockets/mod.rs create mode 100644 crates/wasi/src/p3/sockets/tcp.rs create mode 100644 crates/wasi/src/p3/sockets/util.rs create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/command.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/environment.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/exit.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/imports.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/run.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/stdio.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/terminal.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/preopens.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/types.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure-seed.wit create mode 100644 crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure.wit create mode 100644 crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/random.wit create mode 100644 crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/ip-name-lookup.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/types.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi/src/p3/wit/package.wit create mode 100644 crates/wasi/tests/all/p3/clocks.rs create mode 100644 crates/wasi/tests/all/p3/mod.rs create mode 100644 crates/wasi/tests/all/p3/random.rs create mode 100644 crates/wasi/tests/all/p3/sockets.rs diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index 37a7fae2c9..abafba08d3 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -63,6 +63,14 @@ make_vendor "wasi-config" "config@f4d699b" make_vendor "wasi-keyvalue" "keyvalue@219ea36" +make_vendor "wasi/src/p3" " + cli@01e1d4b@wit-0.3.0-draft + clocks@3850f9d@wit-0.3.0-draft + filesystem@a6ea03a@wit-0.3.0-draft + random@3e99124@wit-0.3.0-draft + sockets@8069eb9@wit-0.3.0-draft +" + rm -rf $cache_dir # Separately (for now), vendor the `wasi-nn` WIT files since their retrieval is diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs index 5c3f3300e2..73a4bf2d27 100644 --- a/crates/test-programs/artifacts/build.rs +++ b/crates/test-programs/artifacts/build.rs @@ -69,6 +69,11 @@ fn build_and_generate_tests() { // Bucket, based on the name of the test, into a "kind" which generates // a `foreach_*` macro below. let kind = match target.as_str() { + s if s.starts_with("cli_0_3") => "cli_0_3", + s if s.starts_with("clocks_0_3") => "clocks_0_3", + s if s.starts_with("filesystem_0_3") => "filesystem_0_3", + s if s.starts_with("random_0_3") => "random_0_3", + s if s.starts_with("sockets_0_3") => "sockets_0_3", s if s.starts_with("http_") => "http", s if s.starts_with("preview1_") => "preview1", s if s.starts_with("preview2_") => "preview2", @@ -102,6 +107,11 @@ fn build_and_generate_tests() { } let adapter = match target.as_str() { "reactor" => &reactor_adapter, + s if s.starts_with("cli_0_3") => &reactor_adapter, + s if s.starts_with("clocks_0_3") => &reactor_adapter, + s if s.starts_with("filesystem_0_3") => &reactor_adapter, + s if s.starts_with("random_0_3") => &reactor_adapter, + s if s.starts_with("sockets_0_3") => &reactor_adapter, s if s.starts_with("async_") => &reactor_adapter, s if s.starts_with("api_proxy") => &proxy_adapter, _ => &command_adapter, diff --git a/crates/test-programs/src/bin/clocks_0_3_sleep.rs b/crates/test-programs/src/bin/clocks_0_3_sleep.rs new file mode 100644 index 0000000000..2aae6c5386 --- /dev/null +++ b/crates/test-programs/src/bin/clocks_0_3_sleep.rs @@ -0,0 +1,70 @@ +use core::future::Future as _; +use core::pin::pin; +use core::ptr; +use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + +use test_programs::p3::wasi::clocks::monotonic_clock; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + sleep_10ms().await; + sleep_0ms(); + sleep_backwards_in_time(); + Ok(()) + } +} + +// Adapted from https://github.com/rust-lang/rust/blob/cd805f09ffbfa3896c8f50a619de9b67e1d9f3c3/library/core/src/task/wake.rs#L63-L77 +// TODO: Replace by `Waker::noop` once MSRV is raised to 1.85 +const NOOP_RAW_WAKER: RawWaker = { + const VTABLE: RawWakerVTable = RawWakerVTable::new( + // Cloning just returns a new no-op raw waker + |_| NOOP_RAW_WAKER, + // `wake` does nothing + |_| {}, + // `wake_by_ref` does nothing + |_| {}, + // Dropping does nothing as we don't allocate anything + |_| {}, + ); + RawWaker::new(ptr::null(), &VTABLE) +}; + +const NOOP_WAKER: &'static Waker = &unsafe { Waker::from_raw(NOOP_RAW_WAKER) }; + +async fn sleep_10ms() { + let dur = 10_000_000; + monotonic_clock::wait_until(monotonic_clock::now() + dur).await; + monotonic_clock::wait_for(dur).await; +} + +fn sleep_0ms() { + let mut cx = Context::from_waker(NOOP_WAKER); + + assert_eq!( + pin!(monotonic_clock::wait_until(monotonic_clock::now())).poll(&mut cx), + Poll::Ready(()), + "waiting until now() is ready immediately", + ); + assert_eq!( + pin!(monotonic_clock::wait_for(0)).poll(&mut cx), + Poll::Ready(()), + "waiting for 0 is ready immediately", + ); +} + +fn sleep_backwards_in_time() { + let mut cx = Context::from_waker(NOOP_WAKER); + + assert_eq!( + pin!(monotonic_clock::wait_until(monotonic_clock::now() - 1)).poll(&mut cx), + Poll::Ready(()), + "waiting until instant which has passed is ready immediately", + ); +} + +fn main() {} diff --git a/crates/test-programs/src/bin/random_0_3_imports.rs b/crates/test-programs/src/bin/random_0_3_imports.rs new file mode 100644 index 0000000000..8e3549da41 --- /dev/null +++ b/crates/test-programs/src/bin/random_0_3_imports.rs @@ -0,0 +1,49 @@ +use test_programs::p3::wasi::random; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let mut bytes = [0_u8; 256]; + getrandom::getrandom(&mut bytes).unwrap(); + + assert!(bytes.iter().any(|x| *x != 0)); + + // Acquired random bytes should be of the expected length. + let array = random::random::get_random_bytes(100); + assert_eq!(array.len(), 100); + + // It shouldn't take 100+ tries to get a nonzero random integer. + for i in 0.. { + if random::random::get_random_u64() == 0 { + continue; + } + assert!(i < 100); + break; + } + + // The `insecure_seed` API should return the same result each time. + let (a1, b1) = random::insecure_seed::insecure_seed(); + let (a2, b2) = random::insecure_seed::insecure_seed(); + assert_eq!(a1, a2); + assert_eq!(b1, b2); + + // Acquired random bytes should be of the expected length. + let array = random::insecure::get_insecure_random_bytes(100); + assert_eq!(array.len(), 100); + + // It shouldn't take 100+ tries to get a nonzero random integer. + for i in 0.. { + if random::insecure::get_insecure_random_u64() == 0 { + continue; + } + assert!(i < 100); + break; + } + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_ip_name_lookup.rs b/crates/test-programs/src/bin/sockets_0_3_ip_name_lookup.rs new file mode 100644 index 0000000000..7e9391b64e --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_ip_name_lookup.rs @@ -0,0 +1,94 @@ +use futures::try_join; +use test_programs::p3::wasi::sockets::ip_name_lookup::{resolve_addresses, ErrorCode}; +use test_programs::p3::wasi::sockets::types::IpAddress; + +struct Component; + +test_programs::p3::export!(Component); + +async fn resolve_one(name: &str) -> Result { + Ok(resolve_addresses(name).await?.first().unwrap().to_owned()) +} + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + // Valid domains + try_join!( + resolve_addresses("localhost"), + resolve_addresses("example.com") + ) + .unwrap(); + + // NB: this is an actual real resolution, so it might time out, might cause + // issues, etc. This result is ignored to prevent flaky failures in CI. + let _ = resolve_addresses("münchen.de").await; + + // Valid IP addresses + assert_eq!( + resolve_one("0.0.0.0").await.unwrap(), + IpAddress::IPV4_UNSPECIFIED + ); + assert_eq!( + resolve_one("127.0.0.1").await.unwrap(), + IpAddress::IPV4_LOOPBACK + ); + assert_eq!( + resolve_one("192.0.2.0").await.unwrap(), + IpAddress::Ipv4((192, 0, 2, 0)) + ); + assert_eq!( + resolve_one("::").await.unwrap(), + IpAddress::IPV6_UNSPECIFIED + ); + assert_eq!(resolve_one("::1").await.unwrap(), IpAddress::IPV6_LOOPBACK); + assert_eq!( + resolve_one("[::]").await.unwrap(), + IpAddress::IPV6_UNSPECIFIED + ); + assert_eq!( + resolve_one("2001:0db8:0:0:0:0:0:0").await.unwrap(), + IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)) + ); + assert_eq!( + resolve_one("dead:beef::").await.unwrap(), + IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0)) + ); + assert_eq!( + resolve_one("dead:beef::0").await.unwrap(), + IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0)) + ); + assert_eq!( + resolve_one("DEAD:BEEF::0").await.unwrap(), + IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0)) + ); + + // Invalid inputs + assert_eq!( + resolve_addresses("").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + assert_eq!( + resolve_addresses(" ").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + assert_eq!( + resolve_addresses("a.b<&>").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + assert_eq!( + resolve_addresses("127.0.0.1:80").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + assert_eq!( + resolve_addresses("[::]:80").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + assert_eq!( + resolve_addresses("http://example.com/").await.unwrap_err(), + ErrorCode::InvalidArgument + ); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_bind.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_bind.rs new file mode 100644 index 0000000000..cc8ebbc142 --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_bind.rs @@ -0,0 +1,179 @@ +use futures::{SinkExt as _, StreamExt as _}; +use test_programs::p3::sockets::attempt_random_port; +use test_programs::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket, +}; +use test_programs::p3::wit_stream; + +struct Component; + +test_programs::p3::export!(Component); + +/// Bind a socket and let the system determine a port. +fn test_tcp_bind_ephemeral_port(ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock = TcpSocket::new(ip.family()); + sock.bind(bind_addr).unwrap(); + + let bound_addr = sock.local_address().unwrap(); + + assert_eq!(bind_addr.ip(), bound_addr.ip()); + assert_ne!(bind_addr.port(), bound_addr.port()); +} + +/// Bind a socket on a specified port. +fn test_tcp_bind_specific_port(ip: IpAddress) { + let sock = TcpSocket::new(ip.family()); + + let bind_addr = attempt_random_port(ip, |bind_addr| sock.bind(bind_addr)).unwrap(); + + let bound_addr = sock.local_address().unwrap(); + + assert_eq!(bind_addr.ip(), bound_addr.ip()); + assert_eq!(bind_addr.port(), bound_addr.port()); +} + +/// Two sockets may not be actively bound to the same address at the same time. +fn test_tcp_bind_addrinuse(ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock1 = TcpSocket::new(ip.family()); + sock1.bind(bind_addr).unwrap(); + sock1.listen().unwrap(); + + let bound_addr = sock1.local_address().unwrap(); + + let sock2 = TcpSocket::new(ip.family()); + assert_eq!(sock2.bind(bound_addr), Err(ErrorCode::AddressInUse)); +} + +// The WASI runtime should set SO_REUSEADDR for us +async fn test_tcp_bind_reuseaddr(ip: IpAddress) { + let client = TcpSocket::new(ip.family()); + + let bind_addr = { + let listener1 = TcpSocket::new(ip.family()); + + let bind_addr = attempt_random_port(ip, |bind_addr| listener1.bind(bind_addr)).unwrap(); + + let mut accept = listener1.listen().unwrap(); + + let connect_addr = + IpSocketAddress::new(IpAddress::new_loopback(ip.family()), bind_addr.port()); + client.connect(connect_addr).await.unwrap(); + + let mut sock = accept.next().await.unwrap(); + assert_eq!(sock.len(), 1); + let sock = sock.pop().unwrap(); + let (mut data_tx, data_rx) = wit_stream::new(); + sock.send(data_rx).unwrap(); + data_tx.send(vec![0; 10]).await.unwrap(); + + bind_addr + }; + + { + let listener2 = TcpSocket::new(ip.family()); + + // If SO_REUSEADDR was configured correctly, the following lines shouldn't be + // affected by the TIME_WAIT state of the just closed `listener1` socket: + listener2.bind(bind_addr).unwrap(); + listener2.listen().unwrap(); + } + + drop(client); +} + +// Try binding to an address that is not configured on the system. +fn test_tcp_bind_addrnotavail(ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock = TcpSocket::new(ip.family()); + + assert_eq!(sock.bind(bind_addr), Err(ErrorCode::AddressNotBindable)); +} + +/// Bind should validate the address family. +fn test_tcp_bind_wrong_family(family: IpAddressFamily) { + let wrong_ip = match family { + IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK, + IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK, + }; + + let sock = TcpSocket::new(family); + let result = sock.bind(IpSocketAddress::new(wrong_ip, 0)); + + assert!(matches!(result, Err(ErrorCode::InvalidArgument))); +} + +/// Bind only works on unicast addresses. +fn test_tcp_bind_non_unicast() { + let ipv4_broadcast = IpSocketAddress::new(IpAddress::IPV4_BROADCAST, 0); + let ipv4_multicast = IpSocketAddress::new(IpAddress::Ipv4((224, 254, 0, 0)), 0); + let ipv6_multicast = IpSocketAddress::new(IpAddress::Ipv6((0xff00, 0, 0, 0, 0, 0, 0, 0)), 0); + + let sock_v4 = TcpSocket::new(IpAddressFamily::Ipv4); + let sock_v6 = TcpSocket::new(IpAddressFamily::Ipv6); + + assert!(matches!( + sock_v4.bind(ipv4_broadcast), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!( + sock_v4.bind(ipv4_multicast), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!( + sock_v6.bind(ipv6_multicast), + Err(ErrorCode::InvalidArgument) + )); +} + +fn test_tcp_bind_dual_stack() { + let sock = TcpSocket::new(IpAddressFamily::Ipv6); + let addr = IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, 0); + + // Binding an IPv4-mapped-IPv6 address on a ipv6-only socket should fail: + assert!(matches!(sock.bind(addr), Err(ErrorCode::InvalidArgument))); +} + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + const RESERVED_IPV4_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 0)); // Reserved for documentation and examples. + const RESERVED_IPV6_ADDRESS: IpAddress = + IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)); // Reserved for documentation and examples. + + test_tcp_bind_ephemeral_port(IpAddress::IPV4_LOOPBACK); + test_tcp_bind_ephemeral_port(IpAddress::IPV6_LOOPBACK); + test_tcp_bind_ephemeral_port(IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_ephemeral_port(IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_specific_port(IpAddress::IPV4_LOOPBACK); + test_tcp_bind_specific_port(IpAddress::IPV6_LOOPBACK); + test_tcp_bind_specific_port(IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_specific_port(IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_reuseaddr(IpAddress::IPV4_LOOPBACK).await; + test_tcp_bind_reuseaddr(IpAddress::IPV6_LOOPBACK).await; + + test_tcp_bind_addrinuse(IpAddress::IPV4_LOOPBACK); + test_tcp_bind_addrinuse(IpAddress::IPV6_LOOPBACK); + test_tcp_bind_addrinuse(IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_addrinuse(IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_addrnotavail(RESERVED_IPV4_ADDRESS); + test_tcp_bind_addrnotavail(RESERVED_IPV6_ADDRESS); + + test_tcp_bind_wrong_family(IpAddressFamily::Ipv4); + test_tcp_bind_wrong_family(IpAddressFamily::Ipv6); + + test_tcp_bind_non_unicast(); + + test_tcp_bind_dual_stack(); + + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_connect.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_connect.rs new file mode 100644 index 0000000000..85d8d4860f --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_connect.rs @@ -0,0 +1,144 @@ +use test_programs::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket, +}; + +struct Component; + +test_programs::p3::export!(Component); + +const SOME_PORT: u16 = 47; // If the tests pass, this will never actually be connected to. + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + test_tcp_connect_unspec(IpAddressFamily::Ipv4).await; + test_tcp_connect_unspec(IpAddressFamily::Ipv6).await; + + test_tcp_connect_port_0(IpAddressFamily::Ipv4).await; + test_tcp_connect_port_0(IpAddressFamily::Ipv6).await; + + test_tcp_connect_wrong_family(IpAddressFamily::Ipv4).await; + test_tcp_connect_wrong_family(IpAddressFamily::Ipv6).await; + + test_tcp_connect_non_unicast().await; + + test_tcp_connect_dual_stack().await; + + test_tcp_connect_explicit_bind(IpAddressFamily::Ipv4).await; + test_tcp_connect_explicit_bind(IpAddressFamily::Ipv6).await; + Ok(()) + } +} + +/// `0.0.0.0` / `::` is not a valid remote address in WASI. +async fn test_tcp_connect_unspec(family: IpAddressFamily) { + let addr = IpSocketAddress::new(IpAddress::new_unspecified(family), SOME_PORT); + let sock = TcpSocket::new(family); + + assert_eq!( + sock.connect(addr).await, + Err(ErrorCode::InvalidArgument) + ); +} + +/// 0 is not a valid remote port. +async fn test_tcp_connect_port_0(family: IpAddressFamily) { + let addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocket::new(family); + + assert_eq!( + sock.connect(addr).await, + Err(ErrorCode::InvalidArgument) + ); +} + +/// Connect should validate the address family. +async fn test_tcp_connect_wrong_family(family: IpAddressFamily) { + let wrong_ip = match family { + IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK, + IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK, + }; + let remote_addr = IpSocketAddress::new(wrong_ip, SOME_PORT); + + let sock = TcpSocket::new(family); + + assert_eq!( + sock.connect(remote_addr).await, + Err(ErrorCode::InvalidArgument) + ); +} + +/// Can only connect to unicast addresses. +async fn test_tcp_connect_non_unicast() { + let ipv4_broadcast = IpSocketAddress::new(IpAddress::IPV4_BROADCAST, SOME_PORT); + let ipv4_multicast = IpSocketAddress::new(IpAddress::Ipv4((224, 254, 0, 0)), SOME_PORT); + let ipv6_multicast = + IpSocketAddress::new(IpAddress::Ipv6((0xff00, 0, 0, 0, 0, 0, 0, 0)), SOME_PORT); + + let sock_v4 = TcpSocket::new(IpAddressFamily::Ipv4); + let sock_v6 = TcpSocket::new(IpAddressFamily::Ipv6); + + assert_eq!( + sock_v4.connect(ipv4_broadcast).await, + Err(ErrorCode::InvalidArgument) + ); + assert_eq!( + sock_v4.connect(ipv4_multicast).await, + Err(ErrorCode::InvalidArgument) + ); + assert_eq!( + sock_v6.connect(ipv6_multicast).await, + Err(ErrorCode::InvalidArgument) + ); +} + +async fn test_tcp_connect_dual_stack() { + // Set-up: + let v4_listener = TcpSocket::new(IpAddressFamily::Ipv4); + v4_listener + .bind(IpSocketAddress::new(IpAddress::IPV4_LOOPBACK, 0)) + .unwrap(); + v4_listener.listen().unwrap(); + + let v4_listener_addr = v4_listener.local_address().unwrap(); + let v6_listener_addr = + IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, v4_listener_addr.port()); + + let v6_client = TcpSocket::new(IpAddressFamily::Ipv6); + + // Tests: + + // Connecting to an IPv4 address on an IPv6 socket should fail: + assert_eq!( + v6_client.connect(v4_listener_addr).await, + Err(ErrorCode::InvalidArgument) + ); + // Connecting to an IPv4-mapped-IPv6 address on an IPv6 socket should fail: + assert_eq!( + v6_client.connect(v6_listener_addr).await, + Err(ErrorCode::InvalidArgument) + ); +} + +/// Client sockets can be explicitly bound. +async fn test_tcp_connect_explicit_bind(family: IpAddressFamily) { + let ip = IpAddress::new_loopback(family); + + let listener = { + let bind_address = IpSocketAddress::new(ip, 0); + let listener = TcpSocket::new(family); + listener.bind(bind_address).unwrap(); + listener.listen().unwrap(); + listener + }; + + let listener_address = listener.local_address().unwrap(); + let client = TcpSocket::new(family); + + // Manually bind the client: + client.bind(IpSocketAddress::new(ip, 0)).unwrap(); + + // Connect should work: + client.connect(listener_address).await.unwrap(); +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_sample_application.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_sample_application.rs new file mode 100644 index 0000000000..13ebdecfb6 --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_sample_application.rs @@ -0,0 +1,91 @@ +use futures::{SinkExt as _, StreamExt as _}; +use test_programs::p3::wasi::sockets::types::{ + IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, TcpSocket, +}; +use test_programs::p3::wit_stream; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + test_tcp_sample_application( + IpAddressFamily::Ipv4, + IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, // use any free port + address: (127, 0, 0, 1), // localhost + }), + ) + .await; + test_tcp_sample_application( + IpAddressFamily::Ipv6, + IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: 0, // use any free port + address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost + flow_info: 0, + scope_id: 0, + }), + ) + .await; + Ok(()) + } +} + +async fn test_tcp_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let listener = TcpSocket::new(family); + + listener.bind(bind_address).unwrap(); + listener.set_listen_backlog_size(32).unwrap(); + let mut accept = listener.listen().unwrap(); + + let addr = listener.local_address().unwrap(); + + { + let client = TcpSocket::new(family); + client.connect(addr).await.unwrap(); + let (mut data_tx, data_rx) = wit_stream::new(); + client.send(data_rx).unwrap(); + data_tx.send(vec![]).await.unwrap(); + data_tx.send(first_message.into()).await.unwrap(); + } + + { + let mut sock = accept.next().await.unwrap(); + assert_eq!(sock.len(), 1); + let sock = sock.pop().unwrap(); + + let (mut data_rx, fut) = sock.receive(); + let data = data_rx.next().await.unwrap(); + + // Check that we sent and received our message! + assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + fut.await.unwrap().unwrap() + } + + // Another client + { + let client = TcpSocket::new(family); + client.connect(addr).await.unwrap(); + let (mut data_tx, data_rx) = wit_stream::new(); + client.send(data_rx).unwrap(); + data_tx.send(second_message.into()).await.unwrap(); + } + + { + let mut sock = accept.next().await.unwrap(); + assert_eq!(sock.len(), 1); + let sock = sock.pop().unwrap(); + let (mut data_rx, fut) = sock.receive(); + let data = data_rx.next().await.unwrap(); + + // Check that we sent and received our message! + assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + fut.await.unwrap().unwrap() + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_sockopts.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_sockopts.rs new file mode 100644 index 0000000000..4130929717 --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_sockopts.rs @@ -0,0 +1,234 @@ +use futures::StreamExt as _; +use test_programs::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket, +}; + +struct Component; + +test_programs::p3::export!(Component); + +const SECOND: u64 = 1_000_000_000; + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + test_tcp_sockopt_defaults(IpAddressFamily::Ipv4); + test_tcp_sockopt_defaults(IpAddressFamily::Ipv6); + + test_tcp_sockopt_input_ranges(IpAddressFamily::Ipv4); + test_tcp_sockopt_input_ranges(IpAddressFamily::Ipv6); + + test_tcp_sockopt_readback(IpAddressFamily::Ipv4); + test_tcp_sockopt_readback(IpAddressFamily::Ipv6); + + test_tcp_sockopt_inheritance(IpAddressFamily::Ipv4).await; + test_tcp_sockopt_inheritance(IpAddressFamily::Ipv6).await; + + test_tcp_sockopt_after_listen(IpAddressFamily::Ipv4).await; + test_tcp_sockopt_after_listen(IpAddressFamily::Ipv6).await; + Ok(()) + } +} + +fn test_tcp_sockopt_defaults(family: IpAddressFamily) { + let sock = TcpSocket::new(family); + + assert_eq!(sock.address_family(), family); + + sock.keep_alive_enabled().unwrap(); // Only verify that it has a default value at all, but either value is valid. + assert!(sock.keep_alive_idle_time().unwrap() > 0); + assert!(sock.keep_alive_interval().unwrap() > 0); + assert!(sock.keep_alive_count().unwrap() > 0); + assert!(sock.hop_limit().unwrap() > 0); + assert!(sock.receive_buffer_size().unwrap() > 0); + assert!(sock.send_buffer_size().unwrap() > 0); +} + +fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { + let sock = TcpSocket::new(family); + + assert!(matches!( + sock.set_listen_backlog_size(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_listen_backlog_size(1), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_listen_backlog_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + + assert!(matches!(sock.set_keep_alive_enabled(true), Ok(_))); + assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_))); + + assert!(matches!( + sock.set_keep_alive_idle_time(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); // Unsupported sizes should be silently clamped. + let idle_time = sock.keep_alive_idle_time().unwrap(); // Check that the special 0/reset behavior was not triggered by the previous line. + assert!(idle_time > 0 && idle_time <= 1 * SECOND); + assert!(matches!(sock.set_keep_alive_idle_time(u64::MAX), Ok(_))); // Unsupported sizes should be silently clamped. + + assert!(matches!( + sock.set_keep_alive_interval(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); // Unsupported sizes should be silently clamped. + let idle_time = sock.keep_alive_interval().unwrap(); // Check that the special 0/reset behavior was not triggered by the previous line. + assert!(idle_time > 0 && idle_time <= 1 * SECOND); + assert!(matches!(sock.set_keep_alive_interval(u64::MAX), Ok(_))); // Unsupported sizes should be silently clamped. + + assert!(matches!( + sock.set_keep_alive_count(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); // Unsupported sizes should be silently clamped. + assert!(matches!(sock.set_keep_alive_count(u32::MAX), Ok(_))); // Unsupported sizes should be silently clamped. + + assert!(matches!( + sock.set_hop_limit(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_hop_limit(1), Ok(_))); + assert!(matches!(sock.set_hop_limit(u8::MAX), Ok(_))); + + assert!(matches!( + sock.set_receive_buffer_size(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_receive_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_receive_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!( + sock.set_send_buffer_size(0), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!(sock.set_send_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_send_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. +} + +fn test_tcp_sockopt_readback(family: IpAddressFamily) { + let sock = TcpSocket::new(family); + + sock.set_keep_alive_enabled(true).unwrap(); + assert_eq!(sock.keep_alive_enabled().unwrap(), true); + sock.set_keep_alive_enabled(false).unwrap(); + assert_eq!(sock.keep_alive_enabled().unwrap(), false); + + sock.set_keep_alive_idle_time(42 * SECOND).unwrap(); + assert_eq!(sock.keep_alive_idle_time().unwrap(), 42 * SECOND); + + sock.set_keep_alive_interval(42 * SECOND).unwrap(); + assert_eq!(sock.keep_alive_interval().unwrap(), 42 * SECOND); + + sock.set_keep_alive_count(42).unwrap(); + assert_eq!(sock.keep_alive_count().unwrap(), 42); + + sock.set_hop_limit(42).unwrap(); + assert_eq!(sock.hop_limit().unwrap(), 42); + + sock.set_receive_buffer_size(0x10000).unwrap(); + assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000); + + sock.set_send_buffer_size(0x10000).unwrap(); + assert_eq!(sock.send_buffer_size().unwrap(), 0x10000); +} + +async fn test_tcp_sockopt_inheritance(family: IpAddressFamily) { + let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let listener = TcpSocket::new(family); + + let default_keep_alive = listener.keep_alive_enabled().unwrap(); + + // Configure options on listener: + { + listener + .set_keep_alive_enabled(!default_keep_alive) + .unwrap(); + listener.set_keep_alive_idle_time(42 * SECOND).unwrap(); + listener.set_keep_alive_interval(42 * SECOND).unwrap(); + listener.set_keep_alive_count(42).unwrap(); + listener.set_hop_limit(42).unwrap(); + listener.set_receive_buffer_size(0x10000).unwrap(); + listener.set_send_buffer_size(0x10000).unwrap(); + } + + listener.bind(bind_addr).unwrap(); + let mut accept = listener.listen().unwrap(); + let bound_addr = listener.local_address().unwrap(); + let client = TcpSocket::new(family); + client.connect(bound_addr).await.unwrap(); + let mut sock = accept.next().await.unwrap(); + assert_eq!(sock.len(), 1); + let sock = sock.pop().unwrap(); + + // Verify options on accepted socket: + { + assert_eq!(sock.keep_alive_enabled().unwrap(), !default_keep_alive); + assert_eq!(sock.keep_alive_idle_time().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_interval().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_count().unwrap(), 42); + assert_eq!(sock.hop_limit().unwrap(), 42); + assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000); + assert_eq!(sock.send_buffer_size().unwrap(), 0x10000); + } + + // Update options on listener to something else: + { + listener.set_keep_alive_enabled(default_keep_alive).unwrap(); + listener.set_keep_alive_idle_time(43 * SECOND).unwrap(); + listener.set_keep_alive_interval(43 * SECOND).unwrap(); + listener.set_keep_alive_count(43).unwrap(); + listener.set_hop_limit(43).unwrap(); + listener.set_receive_buffer_size(0x20000).unwrap(); + listener.set_send_buffer_size(0x20000).unwrap(); + } + + // Verify that the already accepted socket was not affected: + { + assert_eq!(sock.keep_alive_enabled().unwrap(), !default_keep_alive); + assert_eq!(sock.keep_alive_idle_time().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_interval().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_count().unwrap(), 42); + assert_eq!(sock.hop_limit().unwrap(), 42); + assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000); + assert_eq!(sock.send_buffer_size().unwrap(), 0x10000); + } +} + +async fn test_tcp_sockopt_after_listen(family: IpAddressFamily) { + let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let listener = TcpSocket::new(family); + listener.bind(bind_addr).unwrap(); + let mut accept = listener.listen().unwrap(); + let bound_addr = listener.local_address().unwrap(); + + let default_keep_alive = listener.keep_alive_enabled().unwrap(); + + // Update options while the socket is already listening: + { + listener + .set_keep_alive_enabled(!default_keep_alive) + .unwrap(); + listener.set_keep_alive_idle_time(42 * SECOND).unwrap(); + listener.set_keep_alive_interval(42 * SECOND).unwrap(); + listener.set_keep_alive_count(42).unwrap(); + listener.set_hop_limit(42).unwrap(); + listener.set_receive_buffer_size(0x10000).unwrap(); + listener.set_send_buffer_size(0x10000).unwrap(); + } + + let client = TcpSocket::new(family); + client.connect(bound_addr).await.unwrap(); + let mut sock = accept.next().await.unwrap(); + assert_eq!(sock.len(), 1); + let sock = sock.pop().unwrap(); + + // Verify options on accepted socket: + { + assert_eq!(sock.keep_alive_enabled().unwrap(), !default_keep_alive); + assert_eq!(sock.keep_alive_idle_time().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_interval().unwrap(), 42 * SECOND); + assert_eq!(sock.keep_alive_count().unwrap(), 42); + assert_eq!(sock.hop_limit().unwrap(), 42); + assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000); + assert_eq!(sock.send_buffer_size().unwrap(), 0x10000); + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_states.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_states.rs new file mode 100644 index 0000000000..c830d15f83 --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_states.rs @@ -0,0 +1,190 @@ +use test_programs::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket, +}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + test_tcp_unbound_state_invariants(IpAddressFamily::Ipv4); + test_tcp_unbound_state_invariants(IpAddressFamily::Ipv6); + + test_tcp_bound_state_invariants(IpAddressFamily::Ipv4); + test_tcp_bound_state_invariants(IpAddressFamily::Ipv6); + + test_tcp_listening_state_invariants(IpAddressFamily::Ipv4).await; + test_tcp_listening_state_invariants(IpAddressFamily::Ipv6).await; + + test_tcp_connected_state_invariants(IpAddressFamily::Ipv4).await; + test_tcp_connected_state_invariants(IpAddressFamily::Ipv6).await; + Ok(()) + } +} + +fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { + let sock = TcpSocket::new(family); + + // TODO: Test send and receive + //assert!(matches!( + // sock.shutdown(ShutdownType::Both), + // Err(ErrorCode::InvalidState) + //)); + assert!(matches!(sock.local_address(), Err(ErrorCode::InvalidState))); + assert!(matches!( + sock.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.is_listening(), false); + assert_eq!(sock.address_family(), family); + + assert!(matches!(sock.set_listen_backlog_size(32), Ok(_))); + assert!(matches!(sock.keep_alive_enabled(), Ok(_))); + assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_))); + assert!(matches!(sock.keep_alive_idle_time(), Ok(_))); + assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); + assert!(matches!(sock.keep_alive_interval(), Ok(_))); + assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); + assert!(matches!(sock.keep_alive_count(), Ok(_))); + assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); + assert!(matches!(sock.hop_limit(), Ok(_))); + assert!(matches!(sock.set_hop_limit(255), Ok(_))); + assert!(matches!(sock.receive_buffer_size(), Ok(_))); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); + assert!(matches!(sock.send_buffer_size(), Ok(_))); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); +} + +fn test_tcp_bound_state_invariants(family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocket::new(family); + sock.bind(bind_address).unwrap(); + + assert!(matches!( + sock.bind(bind_address), + Err(ErrorCode::InvalidState) + )); + // TODO: Test send and receive + //assert!(matches!( + // sock.shutdown(ShutdownType::Both), + // Err(ErrorCode::InvalidState) + //)); + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!( + sock.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.is_listening(), false); + assert_eq!(sock.address_family(), family); + + assert!(matches!(sock.set_listen_backlog_size(32), Ok(_))); + assert!(matches!(sock.keep_alive_enabled(), Ok(_))); + assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_))); + assert!(matches!(sock.keep_alive_idle_time(), Ok(_))); + assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); + assert!(matches!(sock.keep_alive_interval(), Ok(_))); + assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); + assert!(matches!(sock.keep_alive_count(), Ok(_))); + assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); + assert!(matches!(sock.hop_limit(), Ok(_))); + assert!(matches!(sock.set_hop_limit(255), Ok(_))); + assert!(matches!(sock.receive_buffer_size(), Ok(_))); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); + assert!(matches!(sock.send_buffer_size(), Ok(_))); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); +} + +async fn test_tcp_listening_state_invariants(family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocket::new(family); + sock.bind(bind_address).unwrap(); + sock.listen().unwrap(); + + assert!(matches!( + sock.bind(bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + sock.connect(bind_address).await, // Actual address shouldn't matter + Err(ErrorCode::InvalidState) + )); + assert!(matches!(sock.listen(), Err(ErrorCode::InvalidState))); + // Skipping: tcp::accept + // TODO: Test send and receive + //assert!(matches!( + // sock.shutdown(ShutdownType::Both), + // Err(ErrorCode::InvalidState) + //)); + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!( + sock.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.is_listening(), true); + assert_eq!(sock.address_family(), family); + + assert!(matches!( + sock.set_listen_backlog_size(32), + Ok(_) | Err(ErrorCode::NotSupported) + )); + assert!(matches!(sock.keep_alive_enabled(), Ok(_))); + assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_))); + assert!(matches!(sock.keep_alive_idle_time(), Ok(_))); + assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); + assert!(matches!(sock.keep_alive_interval(), Ok(_))); + assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); + assert!(matches!(sock.keep_alive_count(), Ok(_))); + assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); + assert!(matches!(sock.hop_limit(), Ok(_))); + assert!(matches!(sock.set_hop_limit(255), Ok(_))); + assert!(matches!(sock.receive_buffer_size(), Ok(_))); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); + assert!(matches!(sock.send_buffer_size(), Ok(_))); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); +} + +async fn test_tcp_connected_state_invariants(family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock_listener = TcpSocket::new(family); + sock_listener.bind(bind_address).unwrap(); + sock_listener.listen().unwrap(); + let addr_listener = sock_listener.local_address().unwrap(); + let sock = TcpSocket::new(family); + sock.connect(addr_listener).await.unwrap(); + + assert!(matches!( + sock.bind(bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + sock.connect(addr_listener).await, + Err(ErrorCode::InvalidState) + )); + assert!(matches!(sock.listen(), Err(ErrorCode::InvalidState))); + // Skipping: tcp::shutdown + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!(sock.remote_address(), Ok(_))); + assert_eq!(sock.is_listening(), false); + assert_eq!(sock.address_family(), family); + + assert!(matches!(sock.keep_alive_enabled(), Ok(_))); + assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_))); + assert!(matches!(sock.keep_alive_idle_time(), Ok(_))); + assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); + assert!(matches!(sock.keep_alive_interval(), Ok(_))); + assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); + assert!(matches!(sock.keep_alive_count(), Ok(_))); + assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); + assert!(matches!(sock.hop_limit(), Ok(_))); + assert!(matches!(sock.set_hop_limit(255), Ok(_))); + assert!(matches!(sock.receive_buffer_size(), Ok(_))); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); + assert!(matches!(sock.send_buffer_size(), Ok(_))); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); +} + +fn main() {} diff --git a/crates/test-programs/src/bin/sockets_0_3_tcp_streams.rs b/crates/test-programs/src/bin/sockets_0_3_tcp_streams.rs new file mode 100644 index 0000000000..0645d4fb64 --- /dev/null +++ b/crates/test-programs/src/bin/sockets_0_3_tcp_streams.rs @@ -0,0 +1,149 @@ +use core::future::Future; + +use futures::{SinkExt as _, StreamExt as _}; +use test_programs::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket, +}; +use test_programs::p3::wit_stream; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + test_tcp_input_stream_should_be_closed_by_remote_shutdown(IpAddressFamily::Ipv4).await; + test_tcp_input_stream_should_be_closed_by_remote_shutdown(IpAddressFamily::Ipv6).await; + + test_tcp_input_stream_should_be_closed_by_local_shutdown(IpAddressFamily::Ipv4).await; + test_tcp_input_stream_should_be_closed_by_local_shutdown(IpAddressFamily::Ipv6).await; + + test_tcp_output_stream_should_be_closed_by_local_shutdown(IpAddressFamily::Ipv4).await; + test_tcp_output_stream_should_be_closed_by_local_shutdown(IpAddressFamily::Ipv6).await; + + test_tcp_shutdown_should_not_lose_data(IpAddressFamily::Ipv4).await; + test_tcp_shutdown_should_not_lose_data(IpAddressFamily::Ipv6).await; + Ok(()) + } +} + +/// InputStream::read should return `StreamError::Closed` after the connection has been shut down by the server. +async fn test_tcp_input_stream_should_be_closed_by_remote_shutdown(family: IpAddressFamily) { + setup(family, |server, client| async move { + let (mut server_tx, server_rx) = wit_stream::new(); + server.send(server_rx).unwrap(); + + // Shut down the connection from the server side: + server_tx.close().await.unwrap(); + drop(server_tx); + drop(server); + + let (mut client_rx, client_fut) = client.receive(); + + // The input stream should immediately signal StreamError::Closed. + // Notably, it should _not_ return an empty list (the wasi-io equivalent of EWOULDBLOCK) + // See: https://github.com/bytecodealliance/wasmtime/pull/8968 + + // TODO: Verify + + // Wait for the shutdown signal to reach the client: + assert!(client_rx.next().await.is_none()); + assert_eq!(client_fut.await, Some(Err(ErrorCode::ConnectionReset))); + }) + .await; +} + +/// InputStream::read should return `StreamError::Closed` after the connection has been shut down locally. +async fn test_tcp_input_stream_should_be_closed_by_local_shutdown(family: IpAddressFamily) { + setup(family, |server, client| async move { + let (mut server_tx, server_rx) = wit_stream::new(); + server.send(server_rx).unwrap(); + + // On Linux, `recv` continues to work even after `shutdown(sock, SHUT_RD)` + // has been called. To properly test that this behavior doesn't happen in + // WASI, we make sure there's some data to read by the client: + server_tx.send(b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".into()).await.unwrap(); + + let (client_rx, client_fut) = client.receive(); + + // TODO: Verify + + // Shut down socket locally: + drop(client_rx); + // Wait for the shutdown signal to reach the client: + assert_eq!(client_fut.await, Some(Err(ErrorCode::ConnectionReset))); + }).await; +} + +/// OutputStream should return `StreamError::Closed` after the connection has been locally shut down for sending. +async fn test_tcp_output_stream_should_be_closed_by_local_shutdown(family: IpAddressFamily) { + setup(family, |server, client| async move { + let (server_tx, server_rx) = wit_stream::new(); + server.send(server_rx).unwrap(); + drop(server_tx); + + let (_server_tx, server_rx) = wit_stream::new(); + assert_eq!(server.send(server_rx), Err(ErrorCode::ConnectionReset)); + + let (client_tx, client_rx) = wit_stream::new(); + client.send(client_rx).unwrap(); + drop(client_tx); + + let (_client_tx, client_rx) = wit_stream::new(); + assert_eq!(client.send(client_rx), Err(ErrorCode::ConnectionReset)); + }) + .await; +} + +/// Calling `shutdown` while the OutputStream is in the middle of a background write should not cause that write to be lost. +async fn test_tcp_shutdown_should_not_lose_data(family: IpAddressFamily) { + setup(family, |server, client| async move { + // Minimize the local send buffer: + client.set_send_buffer_size(1024).unwrap(); + let small_buffer_size = client.send_buffer_size().unwrap(); + + // Create a significantly bigger buffer, so that we can be pretty sure the `write` won't finish immediately: + let big_buffer_size = 100 * small_buffer_size; + assert!(big_buffer_size > small_buffer_size); + let outgoing_data = vec![0; big_buffer_size as usize]; + + // Submit the oversized buffer and immediately initiate the shutdown: + let (mut client_tx, client_rx) = wit_stream::new(); + client.send(client_rx).unwrap(); + client_tx.send(outgoing_data.clone()).await.unwrap(); + client_tx.close().await.unwrap(); + drop(client_tx); + + // The peer should receive _all_ data: + let (server_rx, server_fut) = server.receive(); + let incoming_data = server_rx.collect::>().await.concat(); + assert_eq!( + outgoing_data.len(), + incoming_data.len(), + "Received data should match the sent data" + ); + server_fut.await.unwrap().unwrap() + }) + .await; +} + +fn main() {} + +/// Set up a connected pair of sockets +async fn setup>( + family: IpAddressFamily, + body: impl FnOnce(TcpSocket, TcpSocket) -> Fut, +) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let listener = TcpSocket::new(family); + listener.bind(bind_address).unwrap(); + let mut accept = listener.listen().unwrap(); + let bound_address = listener.local_address().unwrap(); + let client_socket = TcpSocket::new(family); + client_socket.connect(bound_address).await.unwrap(); + let mut accepted_socket = accept.next().await.unwrap(); + assert_eq!(accepted_socket.len(), 1); + let accepted_socket = accepted_socket.pop().unwrap(); + + body(accepted_socket, client_socket).await; +} diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs index 49301621db..8df9c43d5f 100644 --- a/crates/test-programs/src/lib.rs +++ b/crates/test-programs/src/lib.rs @@ -1,5 +1,6 @@ pub mod http; pub mod nn; +pub mod p3; pub mod preview1; pub mod sockets; diff --git a/crates/test-programs/src/p3/mod.rs b/crates/test-programs/src/p3/mod.rs new file mode 100644 index 0000000000..752855ef12 --- /dev/null +++ b/crates/test-programs/src/p3/mod.rs @@ -0,0 +1,21 @@ +pub mod sockets; + +wit_bindgen::generate!({ + path: "../wasi/src/p3/wit", + world: "wasi:cli/command", + default_bindings_module: "test_programs::p3", + pub_export_macro: true, + async: { + imports: [ + "wasi:clocks/monotonic-clock@0.3.0#wait-for", + "wasi:clocks/monotonic-clock@0.3.0#wait-until", + "wasi:sockets/ip-name-lookup@0.3.0#resolve-addresses", + //"wasi:sockets/types@0.3.0#[method]tcp-socket.bind", + "wasi:sockets/types@0.3.0#[method]tcp-socket.connect", + ], + exports: [ + "wasi:cli/run@0.3.0#run", + ], + }, + generate_all, +}); diff --git a/crates/test-programs/src/p3/sockets.rs b/crates/test-programs/src/p3/sockets.rs new file mode 100644 index 0000000000..e337b3a050 --- /dev/null +++ b/crates/test-programs/src/p3/sockets.rs @@ -0,0 +1,147 @@ +use core::ops::Range; + +use crate::p3::wasi::random; +use crate::p3::wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, +}; + +impl IpAddress { + pub const IPV4_BROADCAST: IpAddress = IpAddress::Ipv4((255, 255, 255, 255)); + + pub const IPV4_LOOPBACK: IpAddress = IpAddress::Ipv4((127, 0, 0, 1)); + pub const IPV6_LOOPBACK: IpAddress = IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 1)); + + pub const IPV4_UNSPECIFIED: IpAddress = IpAddress::Ipv4((0, 0, 0, 0)); + pub const IPV6_UNSPECIFIED: IpAddress = IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 0)); + + pub const IPV4_MAPPED_LOOPBACK: IpAddress = + IpAddress::Ipv6((0, 0, 0, 0, 0, 0xFFFF, 0x7F00, 0x0001)); + + pub const fn new_loopback(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_LOOPBACK, + IpAddressFamily::Ipv6 => Self::IPV6_LOOPBACK, + } + } + + pub const fn new_unspecified(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_UNSPECIFIED, + IpAddressFamily::Ipv6 => Self::IPV6_UNSPECIFIED, + } + } + + pub const fn family(&self) -> IpAddressFamily { + match self { + IpAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } +} + +impl PartialEq for IpAddress { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Ipv4(left), Self::Ipv4(right)) => left == right, + (Self::Ipv6(left), Self::Ipv6(right)) => left == right, + _ => false, + } + } +} + +impl IpSocketAddress { + pub const fn new(ip: IpAddress, port: u16) -> IpSocketAddress { + match ip { + IpAddress::Ipv4(addr) => IpSocketAddress::Ipv4(Ipv4SocketAddress { + port, + address: addr, + }), + IpAddress::Ipv6(addr) => IpSocketAddress::Ipv6(Ipv6SocketAddress { + port, + address: addr, + flow_info: 0, + scope_id: 0, + }), + } + } + + pub const fn ip(&self) -> IpAddress { + match self { + IpSocketAddress::Ipv4(addr) => IpAddress::Ipv4(addr.address), + IpSocketAddress::Ipv6(addr) => IpAddress::Ipv6(addr.address), + } + } + + pub const fn port(&self) -> u16 { + match self { + IpSocketAddress::Ipv4(addr) => addr.port, + IpSocketAddress::Ipv6(addr) => addr.port, + } + } + + pub const fn family(&self) -> IpAddressFamily { + match self { + IpSocketAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpSocketAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } +} + +impl PartialEq for Ipv4SocketAddress { + fn eq(&self, other: &Self) -> bool { + self.port == other.port && self.address == other.address + } +} + +impl PartialEq for Ipv6SocketAddress { + fn eq(&self, other: &Self) -> bool { + self.port == other.port + && self.flow_info == other.flow_info + && self.address == other.address + && self.scope_id == other.scope_id + } +} + +impl PartialEq for IpSocketAddress { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Ipv4(l0), Self::Ipv4(r0)) => l0 == r0, + (Self::Ipv6(l0), Self::Ipv6(r0)) => l0 == r0, + _ => false, + } + } +} + +fn generate_random_u16(range: Range) -> u16 { + let start = range.start as u64; + let end = range.end as u64; + let port = start + (random::random::get_random_u64() % (end - start)); + port as u16 +} + +/// Execute the inner function with a randomly generated port. +/// To prevent random failures, we make a few attempts before giving up. +pub fn attempt_random_port( + local_address: IpAddress, + mut f: F, +) -> Result +where + F: FnMut(IpSocketAddress) -> Result<(), ErrorCode>, +{ + const MAX_ATTEMPTS: u32 = 10; + let mut i = 0; + loop { + i += 1; + + let port: u16 = generate_random_u16(1024..u16::MAX); + let sock_addr = IpSocketAddress::new(local_address, port); + + match f(sock_addr) { + Ok(_) => return Ok(sock_addr), + Err(e) if i >= MAX_ATTEMPTS => return Err(e), + // Try again if the port is already taken. This can sometimes show up as `AccessDenied` on Windows. + Err(ErrorCode::AddressInUse | ErrorCode::AccessDenied) => {} + Err(e) => return Err(e), + } + } +} diff --git a/crates/test-programs/src/sockets.rs b/crates/test-programs/src/sockets.rs index 0fcccaaab4..8683cb340e 100644 --- a/crates/test-programs/src/sockets.rs +++ b/crates/test-programs/src/sockets.rs @@ -313,11 +313,11 @@ impl IpSocketAddress { pub const fn new(ip: IpAddress, port: u16) -> IpSocketAddress { match ip { IpAddress::Ipv4(addr) => IpSocketAddress::Ipv4(Ipv4SocketAddress { - port: port, + port, address: addr, }), IpAddress::Ipv6(addr) => IpSocketAddress::Ipv6(Ipv6SocketAddress { - port: port, + port, address: addr, flow_info: 0, scope_id: 0, diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index e18afd81c2..88c3620587 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -54,10 +54,13 @@ windows-sys = { workspace = true } rustix = { workspace = true, features = ["event", "net"] } [features] -default = [ "preview1"] +default = [ "preview1", "p3"] preview1 = [ "dep:wiggle", ] +p3 = [ + "wasmtime/component-model-async", +] [[test]] name = "process_stdin" diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index 71eac77264..5e89545d7b 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -242,6 +242,8 @@ mod filesystem; mod host; mod ip_name_lookup; mod network; +#[cfg(feature = "p3")] +pub mod p3; pub mod pipe; mod poll; #[cfg(feature = "preview1")] diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs new file mode 100644 index 0000000000..e1aa409481 --- /dev/null +++ b/crates/wasi/src/p3/bindings.rs @@ -0,0 +1,277 @@ +//! Auto-generated bindings for WASI interfaces. +//! +//! This module contains the output of the [`bindgen!`] macro when run over +//! the `wasi:cli/imports` world. +//! +//! [`bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! +//! # Examples +//! +//! If you have a WIT world which refers to `wasi:cli` interfaces you probably want to +//! use this crate's bindings rather than generate fresh bindings. That can be +//! done using the `with` option to [`bindgen!`]: +//! +//! ```rust +//! use core::future::Future; +//! +//! use wasmtime_wasi::p3::cli::{WasiCliCtx, WasiCliView}; +//! use wasmtime_wasi::p3::clocks::{WasiClocksCtx, WasiClocksView}; +//! use wasmtime_wasi::p3::random::{WasiRandomCtx, WasiRandomView}; +//! use wasmtime_wasi::p3::sockets::{WasiSocketsCtx, WasiSocketsView}; +//! use wasmtime::{Result, StoreContextMut, Engine, Config}; +//! use wasmtime::component::{for_any, Linker}; +//! +//! wasmtime::component::bindgen!({ +//! world: "example:wasi/my-world", +//! inline: " +//! package example:wasi; +//! +//! // An example of extending the `wasi:cli/imports` world with a +//! // custom host interface. +//! world my-world { +//! include wasi:cli/imports@0.3.0; +//! +//! import custom-host; +//! } +//! +//! interface custom-host { +//! my-custom-function: func(); +//! } +//! ", +//! path: "src/p3/wit", +//! with: { +//! "wasi": wasmtime_wasi::p3::bindings, +//! }, +//! concurrent_exports: true, +//! concurrent_imports: true, +//! async: { +//! only_imports: [ +//! "example:wasi/custom-host#my-custom-function", +//! "wasi:clocks/monotonic-clock@0.3.0#wait-for", +//! "wasi:clocks/monotonic-clock@0.3.0#wait-until", +//! ], +//! }, +//! }); +//! +//! struct MyState { +//! cli: WasiCliCtx, +//! clocks: WasiClocksCtx, +//! random: WasiRandomCtx, +//! sockets: WasiSocketsCtx, +//! table: ResourceTable, +//! } +//! +//! impl example::wasi::custom_host::Host for MyState { +//! type Data = Self; +//! +//! fn my_custom_function( +//! store: StoreContextMut<'_, Self::Data>, +//! ) -> impl Future< +//! Output = impl FnOnce(StoreContextMut<'_, Self::Data>) + 'static +//! > + 'static { +//! async move { +//! // .. +//! for_any(|_| {}) +//! } +//! } +//! } +//! +//! impl WasiCliView for MyState { +//! fn cli(&self) -> &WasiCliCtx { &self.cli } +//! } +//! +//! impl WasiClocksView for Ctx { +//! fn clocks(&self) -> &WasiClocksCtx { &self.clocks } +//! } +//! +//! impl WasiRandomView for Ctx { +//! fn random(&mut self) -> &mut WasiRandomCtx { &mut self.random } +//! } +//! +//! impl WasiSocketsView for Ctx { +//! fn sockets(&self) -> &WasiSocketsCtx { &self.sockets } +//! +//! fn table(&mut self) -> &mut ResourceTable { &mut self.table } +//! } +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.async_support(true); +//! let engine = Engine::new(&config)?; +//! let mut linker: Linker = Linker::new(&engine); +//! wasmtime_wasi::p3::add_to_linker(&mut linker)?; +//! //example::wasi::custom_host::add_to_linker(&mut linker, |state| state)?; +//! +//! // .. use `Linker` to instantiate component ... +//! +//! Ok(()) +//! } +//! ``` + +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:cli/command", + //tracing: true, + trappable_imports: true, + concurrent_exports: true, + concurrent_imports: true, + async: { + only_imports: [ + "wasi:clocks/monotonic-clock@0.3.0#wait-for", + "wasi:clocks/monotonic-clock@0.3.0#wait-until", + "wasi:sockets/ip-name-lookup@0.3.0#resolve-addresses", + "wasi:sockets/types@0.3.0#[method]tcp-socket.bind", + "wasi:sockets/types@0.3.0#[method]tcp-socket.connect", + ], + }, + with: { + "wasi:sockets/types/tcp-socket": crate::p3::sockets::tcp::TcpSocket, + } + }); +} +pub use self::generated::exports; +pub use self::generated::wasi::*; +pub use self::generated::LinkOptions; + +/// Bindings to execute and run a `wasi:cli/command`. +/// +/// This structure is automatically generated by `bindgen!`. +/// +/// This can be used for a more "typed" view of executing a command component +/// through the [`Command::wasi_cli_run`] method plus +/// [`Guest::call_run`](exports::wasi::cli::run::Guest::call_run). +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker, Component}; +/// use wasmtime_wasi::{IoView, WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime_wasi::bindings::Command; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::add_to_linker_async(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtxBuilder::new(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = Command::instantiate_async(&mut store, &component, &linker).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl IoView for MyState { +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +/// +/// --- +pub use self::generated::Command; + +/// Pre-instantiated analog of [`Command`] +/// +/// This can be used to front-load work such as export lookup before +/// instantiation. +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker, Component}; +/// use wasmtime_wasi::{IoView, WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime_wasi::bindings::CommandPre; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::add_to_linker_async(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// let pre = CommandPre::new(linker.instantiate_pre(&component)?)?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtxBuilder::new(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = pre.instantiate_async(&mut store).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl IoView for MyState { +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +/// +/// --- +pub use self::generated::CommandPre; + +pub use self::generated::CommandIndices; diff --git a/crates/wasi/src/p3/cli/host.rs b/crates/wasi/src/p3/cli/host.rs new file mode 100644 index 0000000000..7dfbdd3928 --- /dev/null +++ b/crates/wasi/src/p3/cli/host.rs @@ -0,0 +1,19 @@ +use crate::p3::bindings::cli::environment; +use crate::p3::cli::{WasiCliImpl, WasiCliView}; + +impl environment::Host for WasiCliImpl<&mut T> +where + T: WasiCliView, +{ + fn get_environment(&mut self) -> wasmtime::Result> { + Ok(self.cli().environment.clone()) + } + + fn get_arguments(&mut self) -> wasmtime::Result> { + Ok(self.cli().arguments.clone()) + } + + fn initial_cwd(&mut self) -> wasmtime::Result> { + Ok(self.cli().initial_cwd.clone()) + } +} diff --git a/crates/wasi/src/p3/cli/mod.rs b/crates/wasi/src/p3/cli/mod.rs new file mode 100644 index 0000000000..eefd49d000 --- /dev/null +++ b/crates/wasi/src/p3/cli/mod.rs @@ -0,0 +1,109 @@ +mod host; + +use wasmtime::component::Linker; + +#[repr(transparent)] +pub struct WasiCliImpl(pub T); + +impl WasiCliView for &T { + fn cli(&self) -> &WasiCliCtx { + (**self).cli() + } +} + +impl WasiCliView for &mut T { + fn cli(&self) -> &WasiCliCtx { + (**self).cli() + } +} + +impl WasiCliView for WasiCliImpl { + fn cli(&self) -> &WasiCliCtx { + self.0.cli() + } +} + +pub trait WasiCliView: Send { + fn cli(&self) -> &WasiCliCtx; +} + +pub struct WasiCliCtx { + pub environment: Vec<(String, String)>, + pub arguments: Vec, + pub initial_cwd: Option, +} + +impl Default for WasiCliCtx { + fn default() -> Self { + Self { + environment: Vec::default(), + arguments: Vec::default(), + initial_cwd: None, + } + } +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi_cli::{WasiCliView, WasiCliCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_cli::p3::add_to_linker_only_cli(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// cli: WasiCliCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// cli: WasiCliCtx, +/// } +/// +/// impl wasmtime_wasi_cli::WasiCliView for MyState { +/// fn cli(&self) -> &WasiCliCtx { &self.cli } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiCliView + 'static, +{ + let closure = annotate_cli(|cx| WasiCliImpl(cx)); + crate::p3::bindings::cli::environment::add_to_linker_get_host(linker, closure)?; + Ok(()) +} + +fn annotate_cli(val: F) -> F +where + F: Fn(&mut T) -> WasiCliImpl<&mut T>, +{ + val +} diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs new file mode 100644 index 0000000000..da1046af37 --- /dev/null +++ b/crates/wasi/src/p3/clocks/host.rs @@ -0,0 +1,88 @@ +use core::future::Future; +use core::time::Duration; + +use cap_std::time::SystemTime; +use tokio::time::sleep; +use wasmtime::{component, StoreContextMut}; + +use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; +use crate::p3::clocks::{WasiClocksImpl, WasiClocksView}; + +impl TryFrom for wall_clock::Datetime { + type Error = wasmtime::Error; + + fn try_from(time: SystemTime) -> Result { + let duration = + time.duration_since(SystemTime::from_std(std::time::SystemTime::UNIX_EPOCH))?; + + Ok(Self { + seconds: duration.as_secs(), + nanoseconds: duration.subsec_nanos(), + }) + } +} + +impl wall_clock::Host for WasiClocksImpl<&mut T> +where + T: WasiClocksView, +{ + fn now(&mut self) -> wasmtime::Result { + let now = self.clocks().wall_clock.now(); + Ok(wall_clock::Datetime { + seconds: now.as_secs(), + nanoseconds: now.subsec_nanos(), + }) + } + + fn resolution(&mut self) -> wasmtime::Result { + let res = self.clocks().wall_clock.resolution(); + Ok(wall_clock::Datetime { + seconds: res.as_secs(), + nanoseconds: res.subsec_nanos(), + }) + } +} + +impl monotonic_clock::Host for WasiClocksImpl<&mut T> +where + T: WasiClocksView, +{ + type Data = T; + + fn now(&mut self) -> wasmtime::Result { + Ok(self.clocks().monotonic_clock.now()) + } + + fn resolution(&mut self) -> wasmtime::Result { + Ok(self.clocks().monotonic_clock.resolution()) + } + + fn wait_until( + store: StoreContextMut<'_, Self::Data>, + when: monotonic_clock::Instant, + ) -> impl Future< + Output = impl FnOnce(StoreContextMut<'_, Self::Data>) -> wasmtime::Result<()> + 'static, + > + 'static { + let clock_now = store.data().clocks().monotonic_clock.now(); + async move { + if when > clock_now { + sleep(Duration::from_nanos(when - clock_now)).await; + }; + component::for_any(|_| Ok(())) + } + } + + fn wait_for( + _store: StoreContextMut<'_, Self::Data>, + duration: monotonic_clock::Duration, + ) -> impl Future< + Output = impl FnOnce(StoreContextMut<'_, Self::Data>) -> wasmtime::Result<()> + 'static, + > + 'static { + async move { + if duration > 0 { + sleep(Duration::from_nanos(duration)).await; + } + component::for_any(|_| Ok(())) + } + } +} diff --git a/crates/wasi/src/p3/clocks/mod.rs b/crates/wasi/src/p3/clocks/mod.rs new file mode 100644 index 0000000000..fc40898dc6 --- /dev/null +++ b/crates/wasi/src/p3/clocks/mod.rs @@ -0,0 +1,201 @@ +mod host; +#[cfg(feature = "p3")] +pub mod p3; + +use cap_std::time::{Duration, Instant, SystemClock}; +use cap_std::{ambient_authority, AmbientAuthority}; +use cap_time_ext::{MonotonicClockExt as _, SystemClockExt as _}; +use wasmtime::component::Linker; + +#[repr(transparent)] +pub struct WasiClocksImpl(pub T); + +impl WasiClocksView for &T { + fn clocks(&self) -> &WasiClocksCtx { + (**self).clocks() + } +} + +impl WasiClocksView for &mut T { + fn clocks(&self) -> &WasiClocksCtx { + (**self).clocks() + } +} + +impl WasiClocksView for WasiClocksImpl { + fn clocks(&self) -> &WasiClocksCtx { + self.0.clocks() + } +} + +pub trait WasiClocksView: Send { + fn clocks(&self) -> &WasiClocksCtx; +} + +pub struct WasiClocksCtx { + pub wall_clock: Box, + pub monotonic_clock: Box, +} + +impl Default for WasiClocksCtx { + fn default() -> Self { + Self { + wall_clock: wall_clock(), + monotonic_clock: monotonic_clock(), + } + } +} + +pub trait HostWallClock: Send { + fn resolution(&self) -> Duration; + fn now(&self) -> Duration; +} + +pub trait HostMonotonicClock: Send { + fn resolution(&self) -> u64; + fn now(&self) -> u64; +} + +pub struct WallClock { + /// The underlying system clock. + clock: cap_std::time::SystemClock, +} + +impl Default for WallClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl WallClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + Self { + clock: cap_std::time::SystemClock::new(ambient_authority), + } + } +} + +impl HostWallClock for WallClock { + fn resolution(&self) -> Duration { + self.clock.resolution() + } + + fn now(&self) -> Duration { + // WASI defines wall clocks to return "Unix time". + self.clock + .now() + .duration_since(SystemClock::UNIX_EPOCH) + .unwrap() + } +} + +pub struct MonotonicClock { + /// The underlying system clock. + clock: cap_std::time::MonotonicClock, + + /// The `Instant` this clock was created. All returned times are + /// durations since that time. + initial: Instant, +} + +impl Default for MonotonicClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl MonotonicClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + let clock = cap_std::time::MonotonicClock::new(ambient_authority); + let initial = clock.now(); + Self { clock, initial } + } +} + +impl HostMonotonicClock for MonotonicClock { + fn resolution(&self) -> u64 { + self.clock.resolution().as_nanos().try_into().unwrap() + } + + fn now(&self) -> u64 { + // Unwrap here and in `resolution` above; a `u64` is wide enough to + // hold over 584 years of nanoseconds. + self.clock + .now() + .duration_since(self.initial) + .as_nanos() + .try_into() + .unwrap() + } +} + +pub fn monotonic_clock() -> Box { + Box::new(MonotonicClock::default()) +} + +pub fn wall_clock() -> Box { + Box::new(WallClock::default()) +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:clocks/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi_clocks::{WasiClocksView, WasiClocksCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_clocks::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// clocks: WasiClocksCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// clocks: WasiClocksCtx, +/// } +/// +/// impl wasmtime_wasi_clocks::WasiClocksView for MyState { +/// fn clocks(&self) -> &WasiClocksCtx { &self.clocks } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { + let closure = annotate_clocks(|cx| WasiClocksImpl(cx)); + crate::p3::bindings::clocks::wall_clock::add_to_linker_get_host(linker, closure)?; + crate::p3::bindings::clocks::monotonic_clock::add_to_linker_get_host(linker, closure)?; + Ok(()) +} + +fn annotate_clocks(val: F) -> F +where + F: Fn(&mut T) -> WasiClocksImpl<&mut T>, +{ + val +} diff --git a/crates/wasi/src/p3/clocks/p3/bindings.rs b/crates/wasi/src/p3/clocks/p3/bindings.rs new file mode 100644 index 0000000000..5de1aad306 --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/bindings.rs @@ -0,0 +1,104 @@ +//! Auto-generated bindings for `wasi-clocks` +//! +//! This module contains the output of the [`bindgen!`] macro when run over +//! the `wasi:clocks/imports` world. +//! +//! [`bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! +//! # Examples +//! +//! If you have a WIT world which refers to `wasi:clocks` interfaces you probably want to +//! use this crate's bindings rather than generate fresh bindings. That can be +//! done using the `with` option to [`bindgen!`]: +//! +//! ```rust +//! use core::future::Future; +//! +//! use wasmtime_wasi_clocks::{WasiClocksCtx, WasiClocksView}; +//! use wasmtime::{Result, StoreContextMut, Engine, Config}; +//! use wasmtime::component::{for_any, Linker}; +//! +//! wasmtime::component::bindgen!({ +//! world: "example:wasi/my-world", +//! inline: " +//! package example:wasi; +//! +//! // An example of extending the `wasi:clocks/imports` world with a +//! // custom host interface. +//! world my-world { +//! include wasi:clocks/imports@0.3.0; +//! +//! import custom-host; +//! } +//! +//! interface custom-host { +//! my-custom-function: func(); +//! } +//! ", +//! path: "src/p3/wit", +//! with: { +//! "wasi:clocks": wasmtime_wasi_clocks::p3::bindings, +//! }, +//! concurrent_imports: true, +//! async: { +//! only_imports: [ +//! "example:wasi/custom-host#my-custom-function", +//! "wasi:clocks/monotonic-clock@0.3.0#wait-for", +//! "wasi:clocks/monotonic-clock@0.3.0#wait-until", +//! ], +//! }, +//! }); +//! +//! struct MyState { +//! clocks: WasiClocksCtx, +//! } +//! +//! impl example::wasi::custom_host::Host for MyState { +//! type Data = Self; +//! +//! fn my_custom_function( +//! store: StoreContextMut<'_, Self::Data>, +//! ) -> impl Future< +//! Output = impl FnOnce(StoreContextMut<'_, Self::Data>) + 'static +//! > + 'static { +//! async move { +//! // .. +//! for_any(|_| {}) +//! } +//! } +//! } +//! +//! impl WasiClocksView for MyState { +//! fn clocks(&self) -> &WasiClocksCtx { &self.clocks } +//! } +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.async_support(true); +//! let engine = Engine::new(&config)?; +//! let mut linker: Linker = Linker::new(&engine); +//! wasmtime_wasi_clocks::p3::add_to_linker(&mut linker)?; +//! //example::wasi::custom_host::add_to_linker(&mut linker, |state| state)?; +//! +//! // .. use `Linker` to instantiate component ... +//! +//! Ok(()) +//! } +//! ``` + +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:clocks/imports", + tracing: true, + trappable_imports: true, + concurrent_imports: true, + async: { + only_imports: [ + "wasi:clocks/monotonic-clock@0.3.0#wait-for", + "wasi:clocks/monotonic-clock@0.3.0#wait-until", + ], + }, + }); +} +pub use self::generated::wasi::clocks::*; diff --git a/crates/wasi/src/p3/clocks/p3/mod.rs b/crates/wasi/src/p3/clocks/p3/mod.rs new file mode 100644 index 0000000000..d208ca28fe --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/mod.rs @@ -0,0 +1,14 @@ +//! Experimental, unstable and incomplete implementation of wasip3 version of WASI. +//! +//! This module is under heavy development. +//! It is not compliant with semver and is not ready +//! for production use. +//! +//! Bug and security fixes limited to wasip3 will not be given patch releases. +//! +//! Documentation of this module may be incorrect or out-of-sync +//! with the implementation. + +use wasmtime::component::Linker; + +pub mod bindings; diff --git a/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit new file mode 100644 index 0000000000..87ebdaac51 --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.3.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. +@since(version = 0.3.0) +interface monotonic-clock { + /// 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. + @since(version = 0.3.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0) + resolution: func() -> duration; + + /// Wait until the specified instant has occurred. + @since(version = 0.3.0) + wait-until: func( + when: instant, + ); + + /// Wait for the specified duration has elapsed. + @since(version = 0.3.0) + wait-for: func( + how-long: duration, + ); +} diff --git a/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit new file mode 100644 index 0000000000..ac9146834f --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.3.0; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit new file mode 100644 index 0000000000..b7a85ab356 --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.3.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. +@since(version = 0.3.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.3.0) + 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 + @since(version = 0.3.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0) + resolution: func() -> datetime; +} diff --git a/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..f97bcfef13 --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import monotonic-clock; + @since(version = 0.3.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/src/p3/clocks/p3/wit/package.wit b/crates/wasi/src/p3/clocks/p3/wit/package.wit new file mode 100644 index 0000000000..b44516eb47 --- /dev/null +++ b/crates/wasi/src/p3/clocks/p3/wit/package.wit @@ -0,0 +1 @@ +package wasmtime:wasi-clocks; diff --git a/crates/wasi/src/p3/mod.rs b/crates/wasi/src/p3/mod.rs new file mode 100644 index 0000000000..549980e88a --- /dev/null +++ b/crates/wasi/src/p3/mod.rs @@ -0,0 +1,72 @@ +pub mod bindings; +pub mod cli; +pub mod clocks; +pub mod random; +pub mod sockets; +//pub mod filesystem; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi_cli::{WasiCliView, WasiCliCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// cli: WasiCliCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// cli: WasiCliCtx, +/// } +/// +/// impl wasmtime_wasi_cli::WasiCliView for MyState { +/// fn cli(&self) -> &WasiCliCtx { &self.cli } +/// } +/// ``` +pub fn add_to_linker(linker: &mut wasmtime::component::Linker) -> wasmtime::Result<()> +where + T: clocks::WasiClocksView + + random::WasiRandomView + + sockets::WasiSocketsView + //+ filesystem::WasiFilesystemView + + cli::WasiCliView + + 'static, +{ + clocks::add_to_linker(linker)?; + random::add_to_linker(linker)?; + sockets::add_to_linker(linker)?; + //filesystem::add_to_linker(linker)?; + cli::add_to_linker(linker)?; + Ok(()) +} diff --git a/crates/wasi/src/p3/random/host.rs b/crates/wasi/src/p3/random/host.rs new file mode 100644 index 0000000000..982a4c32f2 --- /dev/null +++ b/crates/wasi/src/p3/random/host.rs @@ -0,0 +1,47 @@ +use cap_rand::distributions::Standard; +use cap_rand::Rng; + +use crate::p3::bindings::random::{insecure, insecure_seed, random}; +use crate::p3::random::{WasiRandomImpl, WasiRandomView}; + +impl random::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn get_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.random().random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_random_u64(&mut self) -> wasmtime::Result { + Ok(self.random().random.sample(Standard)) + } +} + +impl insecure::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn get_insecure_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.random().insecure_random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_insecure_random_u64(&mut self) -> wasmtime::Result { + Ok(self.random().insecure_random.sample(Standard)) + } +} + +impl insecure_seed::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn insecure_seed(&mut self) -> wasmtime::Result<(u64, u64)> { + let seed: u128 = self.random().insecure_random_seed; + Ok((seed as u64, (seed >> 64) as u64)) + } +} diff --git a/crates/wasi/src/p3/random/mod.rs b/crates/wasi/src/p3/random/mod.rs new file mode 100644 index 0000000000..4dfdde16a2 --- /dev/null +++ b/crates/wasi/src/p3/random/mod.rs @@ -0,0 +1,171 @@ +mod host; + +use cap_rand::{Rng as _, RngCore, SeedableRng as _}; +use wasmtime::component::Linker; + +#[repr(transparent)] +pub struct WasiRandomImpl(pub T); + +impl WasiRandomView for &mut T { + fn random(&mut self) -> &mut WasiRandomCtx { + (**self).random() + } +} + +impl WasiRandomView for WasiRandomImpl { + fn random(&mut self) -> &mut WasiRandomCtx { + self.0.random() + } +} + +pub trait WasiRandomView: Send { + fn random(&mut self) -> &mut WasiRandomCtx; +} + +pub struct WasiRandomCtx { + pub random: Box, + pub insecure_random: Box, + pub insecure_random_seed: u128, +} + +impl Default for WasiRandomCtx { + fn default() -> Self { + // For the insecure random API, use `SmallRng`, which is fast. It's + // also insecure, but that's the deal here. + let insecure_random = Box::new( + cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) + .unwrap(), + ); + // For the insecure random seed, use a `u128` generated from + // `thread_rng()`, so that it's not guessable from the insecure_random + // API. + let insecure_random_seed = + cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); + Self { + random: thread_rng(), + insecure_random, + insecure_random_seed, + } + } +} + +/// Implement `insecure-random` using a deterministic cycle of bytes. +pub struct Deterministic { + cycle: std::iter::Cycle>, +} + +impl Deterministic { + pub fn new(bytes: Vec) -> Self { + Deterministic { + cycle: bytes.into_iter().cycle(), + } + } +} + +impl RngCore for Deterministic { + fn next_u32(&mut self) -> u32 { + let b0 = self.cycle.next().expect("infinite sequence"); + let b1 = self.cycle.next().expect("infinite sequence"); + let b2 = self.cycle.next().expect("infinite sequence"); + let b3 = self.cycle.next().expect("infinite sequence"); + ((b0 as u32) << 24) + ((b1 as u32) << 16) + ((b2 as u32) << 8) + (b3 as u32) + } + fn next_u64(&mut self) -> u64 { + let w0 = self.next_u32(); + let w1 = self.next_u32(); + ((w0 as u64) << 32) + (w1 as u64) + } + fn fill_bytes(&mut self, buf: &mut [u8]) { + for b in buf.iter_mut() { + *b = self.cycle.next().expect("infinite sequence"); + } + } + fn try_fill_bytes(&mut self, buf: &mut [u8]) -> Result<(), cap_rand::Error> { + self.fill_bytes(buf); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn deterministic() { + let mut det = Deterministic::new(vec![1, 2, 3, 4]); + let mut buf = vec![0; 1024]; + det.try_fill_bytes(&mut buf).expect("get randomness"); + for (ix, b) in buf.iter().enumerate() { + assert_eq!(*b, (ix % 4) as u8 + 1) + } + } +} + +pub fn thread_rng() -> Box { + use cap_rand::{Rng, SeedableRng}; + let mut rng = cap_rand::thread_rng(cap_rand::ambient_authority()); + Box::new(cap_rand::rngs::StdRng::from_seed(rng.r#gen())) +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:random/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi_random::{WasiRandomView, WasiRandomCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_random::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// random: WasiRandomCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// random: WasiRandomCtx, +/// } +/// +/// impl wasmtime_wasi_random::WasiRandomView for MyState { +/// fn random(&mut self) -> &mut WasiRandomCtx { &mut self.random } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { + let closure = annotate_random(|cx| WasiRandomImpl(cx)); + crate::p3::bindings::random::random::add_to_linker_get_host(linker, closure)?; + crate::p3::bindings::random::insecure::add_to_linker_get_host(linker, closure)?; + crate::p3::bindings::random::insecure_seed::add_to_linker_get_host(linker, closure)?; + Ok(()) +} + +fn annotate_random(val: F) -> F +where + F: Fn(&mut T) -> WasiRandomImpl<&mut T>, +{ + val +} diff --git a/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs b/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs new file mode 100644 index 0000000000..5b25277fa3 --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs @@ -0,0 +1,61 @@ +use core::future::Future; +use core::net::Ipv6Addr; +use core::str::FromStr as _; + +use tokio::net::lookup_host; +use wasmtime::component::for_any; +use wasmtime::StoreContextMut; + +use crate::p3::bindings::sockets::ip_name_lookup::{ErrorCode, Host}; +use crate::p3::bindings::sockets::types; +use crate::p3::sockets::util::{from_ipv4_addr, from_ipv6_addr}; +use crate::p3::sockets::{WasiSocketsImpl, WasiSocketsView}; + +impl Host for WasiSocketsImpl<&mut T> +where + T: WasiSocketsView, +{ + type Data = T; + + fn resolve_addresses( + store: StoreContextMut<'_, Self::Data>, + name: String, + ) -> impl Future< + Output = impl FnOnce( + StoreContextMut<'_, Self::Data>, + ) -> wasmtime::Result, ErrorCode>> + + 'static, + > + 'static { + // `url::Host::parse` serves us two functions: + // 1. validate the input is a valid domain name or IP, + // 2. convert unicode domains to punycode. + let mut host = if let Ok(host) = url::Host::parse(&name) { + Ok(host) + } else if let Ok(addr) = Ipv6Addr::from_str(&name) { + // `url::Host::parse` doesn't understand bare IPv6 addresses without [brackets] + Ok(url::Host::Ipv6(addr)) + } else { + Err(ErrorCode::InvalidArgument) + }; + if host.is_ok() && !store.data().sockets().allowed_network_uses.ip_name_lookup { + host = Err(ErrorCode::PermanentResolverFailure); + } + async move { + let res = match host { + Ok(url::Host::Ipv4(addr)) => Ok(vec![types::IpAddress::Ipv4(from_ipv4_addr(addr))]), + Ok(url::Host::Ipv6(addr)) => Ok(vec![types::IpAddress::Ipv6(from_ipv6_addr(addr))]), + Ok(url::Host::Domain(domain)) => { + // This is only resolving names, not ports, so force the port to be 0. + if let Ok(addrs) = lookup_host((domain.as_str(), 0)).await { + Ok(addrs.map(|addr| addr.ip().to_canonical().into()).collect()) + } else { + // If/when we use `getaddrinfo` directly, map the error properly. + Err(ErrorCode::NameUnresolvable) + } + } + Err(err) => Err(err), + }; + for_any(move |_| Ok(res)) + } + } +} diff --git a/crates/wasi/src/p3/sockets/host/mod.rs b/crates/wasi/src/p3/sockets/host/mod.rs new file mode 100644 index 0000000000..aa4d333fbf --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/mod.rs @@ -0,0 +1,2 @@ +mod ip_name_lookup; +mod types; diff --git a/crates/wasi/src/p3/sockets/host/types/mod.rs b/crates/wasi/src/p3/sockets/host/types/mod.rs new file mode 100644 index 0000000000..5d23fed172 --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/mod.rs @@ -0,0 +1,7 @@ +mod tcp; +mod udp; + +impl crate::p3::bindings::sockets::types::Host for crate::p3::sockets::WasiSocketsImpl<&mut T> where + T: crate::p3::sockets::WasiSocketsView +{ +} diff --git a/crates/wasi/src/p3/sockets/host/types/tcp.rs b/crates/wasi/src/p3/sockets/host/types/tcp.rs new file mode 100644 index 0000000000..4de14cb409 --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/tcp.rs @@ -0,0 +1,475 @@ +use core::future::Future; +use core::mem; +use core::net::SocketAddr; + +use anyhow::{ensure, Context as _}; +use rustix::io::Errno; +use wasmtime::component::{for_any, FutureReader, Resource, StreamReader}; +use wasmtime::StoreContextMut; + +use crate::p3::bindings::sockets::types::{ + Duration, ErrorCode, HostTcpSocket, IpAddressFamily, IpSocketAddress, TcpSocket, +}; +use crate::p3::sockets::tcp::{bind, TcpState}; +use crate::p3::sockets::util::is_valid_unicast_address; +use crate::p3::sockets::{SocketAddrUse, WasiSocketsImpl, WasiSocketsView}; + +impl HostTcpSocket for WasiSocketsImpl<&mut T> +where + T: WasiSocketsView, +{ + type TcpSocketData = T; + + fn new(&mut self, address_family: IpAddressFamily) -> wasmtime::Result> { + let socket = TcpSocket::new(address_family.into()).context("failed to create socket")?; + let socket = self + .table() + .push(socket) + .context("failed to push socket resource to table")?; + Ok(socket) + } + + fn bind( + mut store: StoreContextMut<'_, Self::TcpSocketData>, + mut socket: Resource, + local_address: IpSocketAddress, + ) -> impl Future< + Output = impl FnOnce( + StoreContextMut<'_, Self::TcpSocketData>, + ) -> wasmtime::Result> + + 'static, + > + 'static { + let ctx = store.data().sockets(); + let allowed = ctx.allowed_network_uses.tcp; + let socket_addr_check = ctx.socket_addr_check.clone(); + let sock = store + .data_mut() + .table() + .get_mut(&mut socket) + .context("failed to get socket resource from table") + .map(|socket| { + let tcp_state = mem::replace(&mut socket.tcp_state, TcpState::BindStarted); + if let TcpState::Default(sock) = tcp_state { + Some((sock, socket.family)) + } else { + socket.tcp_state = tcp_state; + None + } + }); + let local_address = SocketAddr::from(local_address); + async move { + let res = match sock { + Ok(sock) + if !allowed + || !socket_addr_check(local_address, SocketAddrUse::TcpBind).await => + { + if let Some((sock, ..)) = sock { + Ok(Ok((sock, Err(ErrorCode::AccessDenied)))) + } else { + Ok(Err(ErrorCode::AccessDenied)) + } + } + Ok(Some((sock, family))) => { + let res = bind(&sock, local_address, family); + Ok(Ok((sock, res))) + } + Ok(None) => Ok(Err(ErrorCode::InvalidState)), + Err(err) => Err(err), + }; + for_any(move |mut store: StoreContextMut<'_, Self::TcpSocketData>| { + let sock = res?; + let socket = store + .data_mut() + .table() + .get_mut(&mut socket) + .context("failed to get socket resource from table")?; + let (sock, res) = match sock { + Ok(sock) => sock, + Err(err) => return Ok(Err(err)), + }; + ensure!( + matches!(socket.tcp_state, TcpState::BindStarted), + "corrupted socket state" + ); + if let Err(err) = res { + socket.tcp_state = TcpState::Default(sock); + Ok(Err(err)) + } else { + socket.tcp_state = TcpState::Bound(sock); + Ok(Ok(())) + } + }) + } + } + + fn connect( + mut store: StoreContextMut<'_, Self::TcpSocketData>, + mut socket: Resource, + remote_address: IpSocketAddress, + ) -> impl Future< + Output = impl FnOnce( + StoreContextMut<'_, Self::TcpSocketData>, + ) -> wasmtime::Result> + + 'static, + > + 'static { + let ctx = store.data().sockets(); + let allowed = ctx.allowed_network_uses.tcp; + let socket_addr_check = ctx.socket_addr_check.clone(); + let sock = store + .data_mut() + .table() + .get_mut(&mut socket) + .context("failed to get socket resource from table") + .map(|socket| { + let tcp_state = mem::replace(&mut socket.tcp_state, TcpState::Connecting); + if let TcpState::Default(sock) = tcp_state { + Some((sock, false, socket.family)) + } else if let TcpState::Bound(sock) = tcp_state { + Some((sock, true, socket.family)) + } else { + socket.tcp_state = tcp_state; + None + } + }); + let remote_address = SocketAddr::from(remote_address); + let ip = remote_address.ip().to_canonical(); + async move { + let res = match sock { + Ok(Some((sock, bound, family))) + if !is_valid_unicast_address(ip, family) + || ip.is_unspecified() + || remote_address.port() == 0 => + { + Ok(Ok(Err((sock, bound, ErrorCode::InvalidArgument)))) + } + Ok(sock) + if !allowed + || !socket_addr_check(remote_address, SocketAddrUse::TcpConnect).await => + { + if let Some((sock, bound, ..)) = sock { + Ok(Ok(Err((sock, bound, ErrorCode::AccessDenied)))) + } else { + Ok(Err(ErrorCode::AccessDenied)) + } + } + Ok(Some((sock, ..))) => Ok(Ok(Ok(sock.connect(remote_address).await))), + Ok(None) => Ok(Err(ErrorCode::InvalidState)), + Err(err) => Err(err), + }; + for_any(move |mut store: StoreContextMut<'_, Self::TcpSocketData>| { + let sock = res?; + let socket = store + .data_mut() + .table() + .get_mut(&mut socket) + .context("failed to get socket resource from table")?; + let sock = match sock { + Ok(sock) => sock, + Err(err) => return Ok(Err(err)), + }; + ensure!( + matches!(socket.tcp_state, TcpState::Connecting), + "corrupted socket state" + ); + match sock { + Ok(Ok(stream)) => { + socket.tcp_state = TcpState::Connected(stream); + Ok(Ok(())) + } + Ok(Err(err)) => { + socket.tcp_state = TcpState::Closed; + Ok(Err(err.into())) + } + Err((sock, true, err)) => { + socket.tcp_state = TcpState::Bound(sock); + Ok(Err(err)) + } + Err((sock, false, err)) => { + socket.tcp_state = TcpState::Default(sock); + Ok(Err(err)) + } + } + }) + } + } + + fn listen( + &mut self, + mut socket: Resource, + ) -> wasmtime::Result>, ErrorCode>> { + let ctx = self.sockets(); + let allowed = ctx.allowed_network_uses.tcp; + let socket = self + .table() + .get_mut(&mut socket) + .context("failed to get socket resource from table")?; + let sock = match mem::replace(&mut socket.tcp_state, TcpState::Closed) { + TcpState::Default(sock) | TcpState::Bound(sock) => sock, + tcp_state => { + socket.tcp_state = tcp_state; + return Ok(Err(ErrorCode::InvalidState)); + } + }; + match sock.listen(socket.listen_backlog_size) { + Ok(listener) => { + socket.tcp_state = TcpState::Listening(listener); + //let (tx, rx) = stream(self).context("failed to create stream")?; + // TODO: Store the worker task in enum + // TODO: Get a store/refactor + //tx.write(); + //Ok(Ok(rx)) + Ok(Ok(todo!())) + } + Err(err) => { + match Errno::from_io_error(&err) { + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-listen#:~:text=WSAEMFILE + // According to the docs, `listen` can return EMFILE on Windows. + // This is odd, because we're not trying to create a new socket + // or file descriptor of any kind. So we rewrite it to less + // surprising error code. + // + // At the time of writing, this behavior has never been experimentally + // observed by any of the wasmtime authors, so we're relying fully + // on Microsoft's documentation here. + #[cfg(windows)] + Some(Errno::MFILE) => Ok(Err(ErrorCode::OutOfMemory)), + + _ => Ok(Err(err.into())), + } + } + } + } + + fn send( + &mut self, + socket: Resource, + data: StreamReader, + ) -> wasmtime::Result> { + todo!() + } + + fn receive( + &mut self, + socket: Resource, + ) -> wasmtime::Result<(StreamReader, FutureReader>)> { + todo!() + } + + fn local_address( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket resource from table")?; + Ok(sock.local_address()) + } + + fn remote_address( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket resource from table")?; + Ok(sock.remote_address()) + } + + fn is_listening(&mut self, socket: Resource) -> wasmtime::Result { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.is_listening()) + } + + fn address_family(&mut self, socket: Resource) -> wasmtime::Result { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.address_family()) + } + + fn set_listen_backlog_size( + &mut self, + socket: Resource, + value: u64, + ) -> wasmtime::Result> { + let sock = self + .table() + .get_mut(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_listen_backlog_size(value)) + } + + fn keep_alive_enabled( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.keep_alive_enabled()) + } + + fn set_keep_alive_enabled( + &mut self, + socket: Resource, + value: bool, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_keep_alive_enabled(value)) + } + + fn keep_alive_idle_time( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.keep_alive_idle_time()) + } + + fn set_keep_alive_idle_time( + &mut self, + socket: Resource, + value: Duration, + ) -> wasmtime::Result> { + let sock = self + .table() + .get_mut(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_keep_alive_idle_time(value)) + } + + fn keep_alive_interval( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.keep_alive_interval()) + } + + fn set_keep_alive_interval( + &mut self, + socket: Resource, + value: Duration, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_keep_alive_interval(value)) + } + + fn keep_alive_count( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.keep_alive_count()) + } + + fn set_keep_alive_count( + &mut self, + socket: Resource, + value: u32, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_keep_alive_count(value)) + } + + fn hop_limit( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.hop_limit()) + } + + fn set_hop_limit( + &mut self, + socket: Resource, + value: u8, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_hop_limit(value)) + } + + fn receive_buffer_size( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.receive_buffer_size()) + } + + fn set_receive_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> wasmtime::Result> { + let sock = self + .table() + .get_mut(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_receive_buffer_size(value)) + } + + fn send_buffer_size( + &mut self, + socket: Resource, + ) -> wasmtime::Result> { + let sock = self + .table() + .get(&socket) + .context("failed to get socket from table")?; + Ok(sock.send_buffer_size()) + } + + fn set_send_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> wasmtime::Result> { + let sock = self + .table() + .get_mut(&socket) + .context("failed to get socket from table")?; + Ok(sock.set_send_buffer_size(value)) + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.table() + .delete(rep) + .context("failed to delete socket resource from table")?; + Ok(()) + } +} diff --git a/crates/wasi/src/p3/sockets/host/types/udp.rs b/crates/wasi/src/p3/sockets/host/types/udp.rs new file mode 100644 index 0000000000..9ec2f00573 --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/udp.rs @@ -0,0 +1,123 @@ +#![allow(unused)] // TODO: Remove + +use wasmtime::component::Resource; + +use crate::p3::bindings::sockets::types::{ + ErrorCode, HostUdpSocket, IpAddressFamily, IpSocketAddress, UdpSocket, +}; +use crate::p3::sockets::{WasiSocketsImpl, WasiSocketsView}; + +impl HostUdpSocket for WasiSocketsImpl +where + T: WasiSocketsView, +{ + fn new(&mut self, address_family: IpAddressFamily) -> wasmtime::Result> { + todo!() + } + + fn bind( + &mut self, + self_: Resource, + local_address: IpSocketAddress, + ) -> wasmtime::Result> { + todo!() + } + + fn connect( + &mut self, + self_: Resource, + remote_address: IpSocketAddress, + ) -> wasmtime::Result> { + todo!() + } + + fn disconnect( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn send( + &mut self, + self_: Resource, + data: Vec, + remote_address: Option, + ) -> wasmtime::Result> { + todo!() + } + + fn receive( + &mut self, + self_: Resource, + ) -> wasmtime::Result, IpSocketAddress), ErrorCode>> { + todo!() + } + + fn local_address( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn remote_address( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn address_family(&mut self, self_: Resource) -> wasmtime::Result { + todo!() + } + + fn unicast_hop_limit( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn set_unicast_hop_limit( + &mut self, + self_: Resource, + value: u8, + ) -> wasmtime::Result> { + todo!() + } + + fn receive_buffer_size( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn set_receive_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> wasmtime::Result> { + todo!() + } + + fn send_buffer_size( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + todo!() + } + + fn set_send_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> wasmtime::Result> { + todo!() + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + todo!() + } +} diff --git a/crates/wasi/src/p3/sockets/mod.rs b/crates/wasi/src/p3/sockets/mod.rs new file mode 100644 index 0000000000..8fe2e2d350 --- /dev/null +++ b/crates/wasi/src/p3/sockets/mod.rs @@ -0,0 +1,217 @@ +use core::future::Future; +use core::net::SocketAddr; +use core::ops::Deref; +use core::pin::Pin; + +use std::sync::Arc; + +use wasmtime::component::Linker; +use wasmtime::component::ResourceTable; + +mod host; +pub mod tcp; +pub mod util; + +#[repr(transparent)] +pub struct WasiSocketsImpl(pub T); + +impl WasiSocketsView for &mut T { + fn sockets(&self) -> &WasiSocketsCtx { + (**self).sockets() + } + + fn table(&mut self) -> &mut ResourceTable { + (**self).table() + } +} + +impl WasiSocketsView for WasiSocketsImpl { + fn sockets(&self) -> &WasiSocketsCtx { + self.0.sockets() + } + + fn table(&mut self) -> &mut ResourceTable { + self.0.table() + } +} + +pub trait WasiSocketsView: Send { + fn sockets(&self) -> &WasiSocketsCtx; + fn table(&mut self) -> &mut ResourceTable; +} + +#[derive(Default)] +pub struct WasiSocketsCtx { + pub socket_addr_check: SocketAddrCheck, + pub allowed_network_uses: AllowedNetworkUses, +} + +pub struct Network { + pub socket_addr_check: SocketAddrCheck, + pub allow_ip_name_lookup: bool, +} + +impl Network { + pub async fn check_socket_addr( + &self, + addr: SocketAddr, + reason: SocketAddrUse, + ) -> std::io::Result<()> { + self.socket_addr_check.check(addr, reason).await + } +} + +/// A check that will be called for each socket address that is used of whether the address is permitted. +#[derive(Clone)] +pub struct SocketAddrCheck( + pub(crate) Arc< + dyn Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync, + >, +); + +impl SocketAddrCheck { + /// A check that will be called for each socket address that is used. + /// + /// Returning `true` will permit socket connections to the `SocketAddr`, + /// while returning `false` will reject the connection. + pub fn new( + f: impl Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync + + 'static, + ) -> Self { + Self(Arc::new(f)) + } + + pub async fn check(&self, addr: SocketAddr, reason: SocketAddrUse) -> std::io::Result<()> { + if (self.0)(addr, reason).await { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "An address was not permitted by the socket address check.", + )) + } + } +} + +impl Deref for SocketAddrCheck { + type Target = dyn Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl Default for SocketAddrCheck { + fn default() -> Self { + Self(Arc::new(|_, _| Box::pin(async { false }))) + } +} + +/// The reason what a socket address is being used for. +#[derive(Clone, Copy, Debug)] +pub enum SocketAddrUse { + /// Binding TCP socket + TcpBind, + /// Connecting TCP socket + TcpConnect, + /// Binding UDP socket + UdpBind, + /// Connecting UDP socket + UdpConnect, + /// Sending datagram on non-connected UDP socket + UdpOutgoingDatagram, +} + +#[derive(Copy, Clone)] +pub enum SocketAddressFamily { + Ipv4, + Ipv6, +} + +pub struct AllowedNetworkUses { + pub ip_name_lookup: bool, + pub udp: bool, + pub tcp: bool, +} + +impl Default for AllowedNetworkUses { + fn default() -> Self { + Self { + ip_name_lookup: false, + udp: true, + tcp: true, + } + } +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:sockets/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi_sockets::{WasiSocketsView, WasiSocketsCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_sockets::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// sockets: WasiSocketsCtx::default(), +/// table: ResourceTable::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// sockets: WasiSocketsCtx, +/// table: ResourceTable, +/// } +/// +/// impl wasmtime_wasi_sockets::WasiSocketsView for MyState { +/// fn sockets(&self) -> &WasiSocketsCtx { &self.sockets } +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { + let closure = annotate_sockets(|cx| WasiSocketsImpl(cx)); + crate::p3::bindings::sockets::types::add_to_linker_get_host(linker, closure)?; + crate::p3::bindings::sockets::ip_name_lookup::add_to_linker_get_host(linker, closure)?; + Ok(()) +} + +fn annotate_sockets(val: F) -> F +where + F: Fn(&mut T) -> WasiSocketsImpl<&mut T>, +{ + val +} diff --git a/crates/wasi/src/p3/sockets/tcp.rs b/crates/wasi/src/p3/sockets/tcp.rs new file mode 100644 index 0000000000..8fc124e23d --- /dev/null +++ b/crates/wasi/src/p3/sockets/tcp.rs @@ -0,0 +1,411 @@ +use core::fmt::Debug; +use core::net::SocketAddr; + +use std::os::fd::{AsFd as _, BorrowedFd}; + +use cap_net_ext::AddressFamily; +use rustix::io::Errno; +use rustix::net::sockopt; + +use crate::p3::bindings::sockets::types::{Duration, ErrorCode, IpAddressFamily, IpSocketAddress}; +use crate::p3::sockets::util::is_valid_unicast_address; +use crate::p3::sockets::SocketAddressFamily; +use crate::runtime::with_ambient_tokio_runtime; + +use super::util::{normalize_get_buffer_size, normalize_set_buffer_size}; + +/// Value taken from rust std library. +const DEFAULT_BACKLOG: u32 = 128; + +/// The state of a TCP socket. +/// +/// This represents the various states a socket can be in during the +/// activities of binding, listening, accepting, and connecting. +pub enum TcpState { + /// The initial state for a newly-created socket. + Default(tokio::net::TcpSocket), + + /// Binding started. + BindStarted, + + /// Binding finished. The socket has an address but is not yet listening for connections. + Bound(tokio::net::TcpSocket), + + /// The socket is now listening and waiting for an incoming connection. + Listening(tokio::net::TcpListener), + + /// An outgoing connection is started. + Connecting, + + /// An outgoing connection has been established. + Connected(tokio::net::TcpStream), + + Closed, +} + +impl Debug for TcpState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Default(_) => f.debug_tuple("Default").finish(), + Self::BindStarted => f.debug_tuple("BindStarted").finish(), + Self::Bound(_) => f.debug_tuple("Bound").finish(), + Self::Listening(_) => f.debug_tuple("Listening").finish(), + Self::Connecting => f.debug_tuple("Connecting").finish(), + Self::Connected { .. } => f.debug_tuple("Connected").finish(), + Self::Closed => write!(f, "Closed"), + } + } +} + +/// A host TCP socket, plus associated bookkeeping. +pub struct TcpSocket { + /// The current state in the bind/listen/accept/connect progression. + pub tcp_state: TcpState, + + /// The desired listen queue size. + pub listen_backlog_size: u32, + + pub family: SocketAddressFamily, + + // The socket options below are not automatically inherited from the listener + // on all platforms. So we keep track of which options have been explicitly + // set and manually apply those values to newly accepted clients. + #[cfg(target_os = "macos")] + pub receive_buffer_size: Option, + #[cfg(target_os = "macos")] + pub send_buffer_size: Option, + #[cfg(target_os = "macos")] + pub hop_limit: Option, + #[cfg(target_os = "macos")] + pub keep_alive_idle_time: Option, +} + +impl TcpSocket { + /// Create a new socket in the given family. + pub fn new(family: AddressFamily) -> std::io::Result { + with_ambient_tokio_runtime(|| { + let (socket, family) = match family { + AddressFamily::Ipv4 => { + let socket = tokio::net::TcpSocket::new_v4()?; + (socket, SocketAddressFamily::Ipv4) + } + AddressFamily::Ipv6 => { + let socket = tokio::net::TcpSocket::new_v6()?; + sockopt::set_ipv6_v6only(&socket, true)?; + (socket, SocketAddressFamily::Ipv6) + } + }; + + Ok(Self::from_state(TcpState::Default(socket), family)) + }) + } + + /// Create a `TcpSocket` from an existing socket. + fn from_state(state: TcpState, family: SocketAddressFamily) -> Self { + Self { + tcp_state: state, + listen_backlog_size: DEFAULT_BACKLOG, + family, + #[cfg(target_os = "macos")] + receive_buffer_size: None, + #[cfg(target_os = "macos")] + send_buffer_size: None, + #[cfg(target_os = "macos")] + hop_limit: None, + #[cfg(target_os = "macos")] + keep_alive_idle_time: None, + } + } + + pub fn as_fd(&self) -> Result, ErrorCode> { + match &self.tcp_state { + TcpState::Default(socket) | TcpState::Bound(socket) => Ok(socket.as_fd()), + TcpState::Connected(stream) => Ok(stream.as_fd()), + TcpState::Listening(listener) => Ok(listener.as_fd()), + TcpState::BindStarted | TcpState::Connecting | TcpState::Closed => { + Err(ErrorCode::InvalidState) + } + } + } + + pub fn local_address(&self) -> Result { + match &self.tcp_state { + TcpState::Bound(socket) => { + let addr = socket.local_addr()?; + Ok(addr.into()) + } + TcpState::Connected(stream) => { + let addr = stream.local_addr()?; + Ok(addr.into()) + } + TcpState::Listening(listener) => { + let addr = listener.local_addr()?; + Ok(addr.into()) + } + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn remote_address(&self) -> Result { + match &self.tcp_state { + TcpState::Connected(stream) => { + let addr = stream.peer_addr()?; + Ok(addr.into()) + } + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn is_listening(&self) -> bool { + matches!(self.tcp_state, TcpState::Listening { .. }) + } + + pub fn address_family(&self) -> IpAddressFamily { + match self.family { + SocketAddressFamily::Ipv4 => IpAddressFamily::Ipv4, + SocketAddressFamily::Ipv6 => IpAddressFamily::Ipv6, + } + } + + pub fn set_listen_backlog_size(&mut self, value: u64) -> Result<(), ErrorCode> { + const MIN_BACKLOG: u32 = 1; + const MAX_BACKLOG: u32 = i32::MAX as u32; // OS'es will most likely limit it down even further. + + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + // Silently clamp backlog size. This is OK for us to do, because operating systems do this too. + let value = value + .try_into() + .unwrap_or(MAX_BACKLOG) + .clamp(MIN_BACKLOG, MAX_BACKLOG); + match &self.tcp_state { + TcpState::Default(..) | TcpState::Bound(..) => { + // Socket not listening yet. Stash value for first invocation to `listen`. + self.listen_backlog_size = value; + Ok(()) + } + TcpState::Listening(listener) => { + // Try to update the backlog by calling `listen` again. + // Not all platforms support this. We'll only update our own value if the OS supports changing the backlog size after the fact. + if rustix::net::listen(&listener, value.try_into().unwrap_or(i32::MAX)).is_err() { + return Err(ErrorCode::NotSupported); + } + self.listen_backlog_size = value; + Ok(()) + } + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn keep_alive_enabled(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_socket_keepalive(fd)?; + Ok(v) + } + + pub fn set_keep_alive_enabled(&self, value: bool) -> Result<(), ErrorCode> { + let fd = self.as_fd()?; + let v = sockopt::set_socket_keepalive(fd, value)?; + Ok(v) + } + + pub fn keep_alive_idle_time(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_tcp_keepidle(fd)?; + Ok(v.as_nanos().try_into().unwrap_or(u64::MAX)) + } + + pub fn set_keep_alive_idle_time(&mut self, value: Duration) -> Result<(), ErrorCode> { + // Ensure that the value passed to the actual syscall never gets rounded down to 0. + const MIN_SECS: core::time::Duration = core::time::Duration::from_secs(1); + + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX_SECS: core::time::Duration = core::time::Duration::from_secs(i16::MAX as u64); + + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = core::time::Duration::from_nanos(value).clamp(MIN_SECS, MAX_SECS); + sockopt::set_tcp_keepidle(fd, value)?; + #[cfg(target_os = "macos")] + { + self.keep_alive_idle_time = Some(value); + } + Ok(()) + } + + pub fn keep_alive_interval(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_tcp_keepintvl(fd)?; + Ok(v.as_nanos().try_into().unwrap_or(u64::MAX)) + } + + pub fn set_keep_alive_interval(&self, value: Duration) -> Result<(), ErrorCode> { + // Ensure that any fractional value passed to the actual syscall never gets rounded down to 0. + const MIN_SECS: core::time::Duration = core::time::Duration::from_secs(1); + + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX_SECS: core::time::Duration = core::time::Duration::from_secs(i16::MAX as u64); + + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + sockopt::set_tcp_keepintvl( + fd, + core::time::Duration::from_nanos(value).clamp(MIN_SECS, MAX_SECS), + )?; + Ok(()) + } + + pub fn keep_alive_count(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_tcp_keepcnt(fd)?; + Ok(v) + } + + pub fn set_keep_alive_count(&self, value: u32) -> Result<(), ErrorCode> { + const MIN_CNT: u32 = 1; + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX_CNT: u32 = i8::MAX as u32; + + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + sockopt::set_tcp_keepcnt(fd, value.clamp(MIN_CNT, MAX_CNT))?; + Ok(()) + } + + pub fn hop_limit(&self) -> Result { + let fd = self.as_fd()?; + match self.family { + SocketAddressFamily::Ipv4 => { + let v = sockopt::get_ip_ttl(fd)?; + let Ok(v) = v.try_into() else { + return Err(ErrorCode::NotSupported); + }; + Ok(v) + } + SocketAddressFamily::Ipv6 => { + let v = sockopt::get_ipv6_unicast_hops(fd)?; + Ok(v) + } + } + } + + pub fn set_hop_limit(&self, value: u8) -> Result<(), ErrorCode> { + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + // + // A well-behaved IP application should never send out new packets with TTL 0. + // We validate the value ourselves because OS'es are not consistent in this. + // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation. + return Err(ErrorCode::InvalidArgument); + } + match self.family { + SocketAddressFamily::Ipv4 => { + sockopt::set_ip_ttl(fd, value.into())?; + } + SocketAddressFamily::Ipv6 => { + sockopt::set_ipv6_unicast_hops(fd, value.into())?; + } + } + Ok(()) + } + + pub fn receive_buffer_size(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_socket_recv_buffer_size(fd)?; + Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX)) + } + + pub fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = value.try_into().unwrap_or(usize::MAX); + let value = normalize_set_buffer_size(value); + match sockopt::set_socket_recv_buffer_size(fd, value) { + Err(Errno::NOBUFS) => {} + Err(err) => return Err(err.into()), + _ => {} + }; + #[cfg(target_os = "macos")] + { + self.receive_buffer_size = Some(value); + } + Ok(()) + } + + pub fn send_buffer_size(&self) -> Result { + let fd = self.as_fd()?; + let v = sockopt::get_socket_send_buffer_size(fd)?; + Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX)) + } + + pub fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + let fd = self.as_fd()?; + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = value.try_into().unwrap_or(usize::MAX); + let value = normalize_set_buffer_size(value); + match sockopt::set_socket_send_buffer_size(fd, value) { + Err(Errno::NOBUFS) => {} + Err(err) => return Err(err.into()), + _ => {} + }; + #[cfg(target_os = "macos")] + { + self.send_buffer_size = Some(value); + } + Ok(()) + } +} + +pub fn bind( + socket: &tokio::net::TcpSocket, + local_address: SocketAddr, + socket_family: SocketAddressFamily, +) -> Result<(), ErrorCode> { + if !is_valid_unicast_address(local_address.ip(), socket_family) { + return Err(ErrorCode::InvalidArgument); + } + // Automatically bypass the TIME_WAIT state when binding to a specific port + // Unconditionally (re)set SO_REUSEADDR, even when the value is false. + // This ensures we're not accidentally affected by any socket option + // state left behind by a previous failed call to this method. + #[cfg(not(windows))] + if let Err(err) = socket.set_reuseaddr(local_address.port() > 0) { + return Err(err.into()); + } + + // Perform the OS bind call. + socket + .bind(local_address) + .map_err(|err| match Errno::from_io_error(&err) { + // From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html: + // > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket + // + // The most common reasons for this error should have already + // been handled by our own validation slightly higher up in this + // function. This error mapping is here just in case there is + // an edge case we didn't catch. + Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS + // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. + #[cfg(windows)] + Some(Errno::NOBUFS) => ErrorCode::AddressInUse, + _ => err.into(), + }) +} diff --git a/crates/wasi/src/p3/sockets/util.rs b/crates/wasi/src/p3/sockets/util.rs new file mode 100644 index 0000000000..320c456743 --- /dev/null +++ b/crates/wasi/src/p3/sockets/util.rs @@ -0,0 +1,263 @@ +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + +use std::net::ToSocketAddrs; + +use rustix::io::Errno; +use tracing::debug; + +use crate::p3::bindings::sockets::types; +use crate::p3::sockets::SocketAddressFamily; + +fn is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool { + matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _]) + && addr != Ipv6Addr::UNSPECIFIED + && addr != Ipv6Addr::LOCALHOST +} + +pub fn is_valid_unicast_address(addr: IpAddr, socket_family: SocketAddressFamily) -> bool { + match (socket_family, addr.to_canonical()) { + (SocketAddressFamily::Ipv4, IpAddr::V4(ipv4)) => { + !ipv4.is_multicast() && !ipv4.is_broadcast() + } + (SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => { + !ipv6.is_multicast() + && !is_deprecated_ipv4_compatible(ipv6) + && ipv6.to_ipv4_mapped().is_none() + } + _ => false, + } +} + +pub fn to_ipv4_addr(addr: types::Ipv4Address) -> Ipv4Addr { + let (x0, x1, x2, x3) = addr; + Ipv4Addr::new(x0, x1, x2, x3) +} + +pub fn from_ipv4_addr(addr: Ipv4Addr) -> types::Ipv4Address { + let [x0, x1, x2, x3] = addr.octets(); + (x0, x1, x2, x3) +} + +pub fn to_ipv6_addr(addr: types::Ipv6Address) -> Ipv6Addr { + let (x0, x1, x2, x3, x4, x5, x6, x7) = addr; + Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7) +} + +pub fn from_ipv6_addr(addr: Ipv6Addr) -> types::Ipv6Address { + let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments(); + (x0, x1, x2, x3, x4, x5, x6, x7) +} + +pub fn normalize_get_buffer_size(value: usize) -> usize { + if cfg!(target_os = "linux") { + // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead. + // getsockopt returns this internally doubled value. + // We'll half the value to at least get it back into the same ballpark that the application requested it in. + // + // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs + value / 2 + } else { + value + } +} + +pub fn normalize_set_buffer_size(value: usize) -> usize { + value.clamp(1, i32::MAX as usize) +} + +impl From for types::IpAddress { + fn from(addr: IpAddr) -> Self { + match addr { + IpAddr::V4(v4) => Self::Ipv4(from_ipv4_addr(v4)), + IpAddr::V6(v6) => Self::Ipv6(from_ipv6_addr(v6)), + } + } +} + +impl From for IpAddr { + fn from(addr: types::IpAddress) -> Self { + match addr { + types::IpAddress::Ipv4(v4) => Self::V4(to_ipv4_addr(v4)), + types::IpAddress::Ipv6(v6) => Self::V6(to_ipv6_addr(v6)), + } + } +} + +impl From for SocketAddr { + fn from(addr: types::IpSocketAddress) -> Self { + match addr { + types::IpSocketAddress::Ipv4(ipv4) => Self::V4(ipv4.into()), + types::IpSocketAddress::Ipv6(ipv6) => Self::V6(ipv6.into()), + } + } +} + +impl From for types::IpSocketAddress { + fn from(addr: SocketAddr) -> Self { + match addr { + SocketAddr::V4(v4) => Self::Ipv4(v4.into()), + SocketAddr::V6(v6) => Self::Ipv6(v6.into()), + } + } +} + +impl From for SocketAddrV4 { + fn from(addr: types::Ipv4SocketAddress) -> Self { + Self::new(to_ipv4_addr(addr.address), addr.port) + } +} + +impl From for types::Ipv4SocketAddress { + fn from(addr: SocketAddrV4) -> Self { + Self { + address: from_ipv4_addr(*addr.ip()), + port: addr.port(), + } + } +} + +impl From for SocketAddrV6 { + fn from(addr: types::Ipv6SocketAddress) -> Self { + Self::new( + to_ipv6_addr(addr.address), + addr.port, + addr.flow_info, + addr.scope_id, + ) + } +} + +impl From for types::Ipv6SocketAddress { + fn from(addr: SocketAddrV6) -> Self { + Self { + address: from_ipv6_addr(*addr.ip()), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + } + } +} + +impl ToSocketAddrs for types::IpSocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddr::from(*self).to_socket_addrs() + } +} + +impl ToSocketAddrs for types::Ipv4SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddrV4::from(*self).to_socket_addrs() + } +} + +impl ToSocketAddrs for types::Ipv6SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddrV6::from(*self).to_socket_addrs() + } +} + +impl From for cap_net_ext::AddressFamily { + fn from(family: types::IpAddressFamily) -> Self { + match family { + types::IpAddressFamily::Ipv4 => Self::Ipv4, + types::IpAddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for types::IpAddressFamily { + fn from(family: cap_net_ext::AddressFamily) -> Self { + match family { + cap_net_ext::AddressFamily::Ipv4 => Self::Ipv4, + cap_net_ext::AddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for types::ErrorCode { + fn from(value: std::io::Error) -> Self { + (&value).into() + } +} + +impl From<&std::io::Error> for types::ErrorCode { + fn from(value: &std::io::Error) -> Self { + // Attempt the more detailed native error code first: + if let Some(errno) = Errno::from_io_error(value) { + return errno.into(); + } + + match value.kind() { + std::io::ErrorKind::AddrInUse => Self::AddressInUse, + std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable, + std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted, + std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused, + std::io::ErrorKind::ConnectionReset => Self::ConnectionReset, + std::io::ErrorKind::InvalidInput => Self::InvalidArgument, + std::io::ErrorKind::NotConnected => Self::InvalidState, + std::io::ErrorKind::OutOfMemory => Self::OutOfMemory, + std::io::ErrorKind::PermissionDenied => Self::AccessDenied, + std::io::ErrorKind::TimedOut => Self::Timeout, + std::io::ErrorKind::Unsupported => Self::NotSupported, + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} + +impl From for types::ErrorCode { + fn from(value: Errno) -> Self { + (&value).into() + } +} + +impl From<&Errno> for types::ErrorCode { + fn from(value: &Errno) -> Self { + match *value { + #[cfg(not(windows))] + Errno::PERM => Self::AccessDenied, + Errno::ACCESS => Self::AccessDenied, + Errno::ADDRINUSE => Self::AddressInUse, + Errno::ADDRNOTAVAIL => Self::AddressNotBindable, + Errno::TIMEDOUT => Self::Timeout, + Errno::CONNREFUSED => Self::ConnectionRefused, + Errno::CONNRESET => Self::ConnectionReset, + Errno::CONNABORTED => Self::ConnectionAborted, + Errno::INVAL => Self::InvalidArgument, + Errno::HOSTUNREACH => Self::RemoteUnreachable, + Errno::HOSTDOWN => Self::RemoteUnreachable, + Errno::NETDOWN => Self::RemoteUnreachable, + Errno::NETUNREACH => Self::RemoteUnreachable, + #[cfg(target_os = "linux")] + Errno::NONET => Self::RemoteUnreachable, + Errno::ISCONN => Self::InvalidState, + Errno::NOTCONN => Self::InvalidState, + Errno::DESTADDRREQ => Self::InvalidState, + Errno::MSGSIZE => Self::DatagramTooLarge, + #[cfg(not(windows))] + Errno::NOMEM => Self::OutOfMemory, + Errno::NOBUFS => Self::OutOfMemory, + Errno::OPNOTSUPP => Self::NotSupported, + Errno::NOPROTOOPT => Self::NotSupported, + Errno::PFNOSUPPORT => Self::NotSupported, + Errno::PROTONOSUPPORT => Self::NotSupported, + Errno::PROTOTYPE => Self::NotSupported, + Errno::SOCKTNOSUPPORT => Self::NotSupported, + Errno::AFNOSUPPORT => Self::NotSupported, + + // FYI, EINPROGRESS should have already been handled by connect. + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/command.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/command.wit new file mode 100644 index 0000000000..0310e51514 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world command { + @since(version = 0.3.0) + include imports; + + @since(version = 0.3.0) + export run; +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/environment.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/environment.wit new file mode 100644 index 0000000000..d99dcc0ae3 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.3.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.3.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.3.0) + initial-cwd: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/exit.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/exit.wit new file mode 100644 index 0000000000..e799a95a26 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/imports.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/imports.wit new file mode 100644 index 0000000000..5dbc2ede8d --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/imports.wit @@ -0,0 +1,34 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + include wasi:clocks/imports@0.3.0; + @since(version = 0.3.0) + include wasi:filesystem/imports@0.3.0; + @since(version = 0.3.0) + include wasi:sockets/imports@0.3.0; + @since(version = 0.3.0) + include wasi:random/imports@0.3.0; + + @since(version = 0.3.0) + import environment; + @since(version = 0.3.0) + import exit; + @since(version = 0.3.0) + import stdin; + @since(version = 0.3.0) + import stdout; + @since(version = 0.3.0) + import stderr; + @since(version = 0.3.0) + import terminal-input; + @since(version = 0.3.0) + import terminal-output; + @since(version = 0.3.0) + import terminal-stdin; + @since(version = 0.3.0) + import terminal-stdout; + @since(version = 0.3.0) + import terminal-stderr; +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/run.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/run.wit new file mode 100644 index 0000000000..6dd8b6879e --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.3.0) +interface run { + /// Run the program. + @since(version = 0.3.0) + run: func() -> result; +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/stdio.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/stdio.wit new file mode 100644 index 0000000000..6a1208fad7 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/stdio.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0) +interface stdin { + @since(version = 0.3.0) + get-stdin: func() -> stream; +} + +@since(version = 0.3.0) +interface stdout { + @since(version = 0.3.0) + set-stdout: func(data: stream); +} + +@since(version = 0.3.0) +interface stderr { + @since(version = 0.3.0) + set-stderr: func(data: stream); +} diff --git a/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/terminal.wit b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/terminal.wit new file mode 100644 index 0000000000..c37184f4c7 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli@01e1d4b@wit-0.3.0-draft/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.3.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.3.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stdin { + @since(version = 0.3.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stdout { + @since(version = 0.3.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stderr { + @since(version = 0.3.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit new file mode 100644 index 0000000000..87ebdaac51 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.3.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. +@since(version = 0.3.0) +interface monotonic-clock { + /// 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. + @since(version = 0.3.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0) + resolution: func() -> duration; + + /// Wait until the specified instant has occurred. + @since(version = 0.3.0) + wait-until: func( + when: instant, + ); + + /// Wait for the specified duration has elapsed. + @since(version = 0.3.0) + wait-for: func( + how-long: duration, + ); +} diff --git a/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit new file mode 100644 index 0000000000..ac9146834f --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.3.0; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit new file mode 100644 index 0000000000..b7a85ab356 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.3.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. +@since(version = 0.3.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.3.0) + 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 + @since(version = 0.3.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0) + resolution: func() -> datetime; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..f97bcfef13 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks@3850f9d@wit-0.3.0-draft/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import monotonic-clock; + @since(version = 0.3.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/preopens.wit b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/preopens.wit new file mode 100644 index 0000000000..0b29aae334 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.3.0; + +@since(version = 0.3.0) +interface preopens { + @since(version = 0.3.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0) + get-directories: func() -> list>; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/types.wit b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/types.wit new file mode 100644 index 0000000000..af3cb254cc --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/types.wit @@ -0,0 +1,629 @@ +package wasi:filesystem@0.3.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 +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + use wasi:clocks/wall-clock@0.3.0.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.3.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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 integrity + /// 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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`. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0) + 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, + /// 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the file fails. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> tuple, future>>; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// 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. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0) + write-via-stream: func( + /// Data to write + data: stream, + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result<_, error-code>; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0) + append-via-stream: func(data: stream) -> result<_, error-code>; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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 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. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0) + read-directory: func() -> tuple, future>>; + + /// 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. + @since(version = 0.3.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0) + 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. + /// + /// 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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. + @since(version = 0.3.0) + 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 encouraged 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. + @since(version = 0.3.0) + 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`. + @since(version = 0.3.0) + 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; + } +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/world.wit b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..c0ab32afe2 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem@a6ea03a@wit-0.3.0-draft/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import preopens; +} diff --git a/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure-seed.wit b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure-seed.wit new file mode 100644 index 0000000000..4708d90493 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.3.0; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.3.0) + insecure-seed: func() -> tuple; +} diff --git a/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure.wit b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure.wit new file mode 100644 index 0000000000..4ea5e581fd --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.3.0; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.3.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.3.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/random.wit b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/random.wit new file mode 100644 index 0000000000..786ef25f68 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.3.0; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.3.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.3.0) + get-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/world.wit b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..838d38023c --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random@3e99124@wit-0.3.0-draft/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import random; + + @since(version = 0.3.0) + import insecure; + + @since(version = 0.3.0) + import insecure-seed; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/ip-name-lookup.wit b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/ip-name-lookup.wit new file mode 100644 index 0000000000..7cc8b03e35 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/ip-name-lookup.wit @@ -0,0 +1,62 @@ +@since(version = 0.3.0) +interface ip-name-lookup { + @since(version = 0.3.0) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// The returned future will resolve to an error code in case of failure. + /// It will resolve to success once the returned stream is exhausted. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + resolve-addresses: func(name: string) -> result, error-code>; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/types.wit b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/types.wit new file mode 100644 index 0000000000..b5f84d3602 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/types.wit @@ -0,0 +1,726 @@ +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + use wasi:clocks/monotonic-clock@0.3.0.{duration}; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + } + + @since(version = 0.3.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0) + type ipv4-address = tuple; + @since(version = 0.3.0) + type ipv6-address = tuple; + + @since(version = 0.3.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0) + resource tcp-socket { + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Start listening return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + listen: func() -> result, error-code>; + + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: stream) -> result<_, error-code>; + + /// Read data from peer. + /// + /// This function returns a `stream` which provides the data received from the + /// socket, and a `future` providing additional error information in case the + /// socket is closed abnormally. + /// + /// If the socket is closed normally, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If the socket is closed abnormally, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// `receive` is meant to be called only once per socket. If it is called more + /// than once, the subsequent calls return a new `stream` that fails as if it + /// were closed abnormally. + /// + /// If the caller is not expecting to receive any data from the peer, + /// they may cancel the receive task. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> tuple, future>>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0) + keep-alive-enabled: func() -> result; + @since(version = 0.3.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.3.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-interval: func() -> result; + @since(version = 0.3.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-count: func() -> result; + @since(version = 0.3.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + hop-limit: func() -> result; + @since(version = 0.3.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0) + resource udp-socket { + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + disconnect: func() -> result<_, error-code>; + + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: list, remote-address: option) -> result<_, error-code>; + + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> result, ip-socket-address>, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + unicast-hop-limit: func() -> result; + @since(version = 0.3.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} diff --git a/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/world.wit b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..6c9951d1c6 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets@8069eb9@wit-0.3.0-draft/world.wit @@ -0,0 +1,9 @@ +package wasi:sockets@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import ip-name-lookup; +} diff --git a/crates/wasi/src/p3/wit/package.wit b/crates/wasi/src/p3/wit/package.wit new file mode 100644 index 0000000000..c223b458e8 --- /dev/null +++ b/crates/wasi/src/p3/wit/package.wit @@ -0,0 +1 @@ +package wasmtime:wasi; diff --git a/crates/wasi/tests/all/main.rs b/crates/wasi/tests/all/main.rs index 9a7d0782ad..d926373fce 100644 --- a/crates/wasi/tests/all/main.rs +++ b/crates/wasi/tests/all/main.rs @@ -89,5 +89,7 @@ macro_rules! assert_test_exists { mod api; mod async_; +#[cfg(feature = "p3")] +mod p3; mod preview1; mod sync; diff --git a/crates/wasi/tests/all/p3/clocks.rs b/crates/wasi/tests/all/p3/clocks.rs new file mode 100644 index 0000000000..bc714d67c0 --- /dev/null +++ b/crates/wasi/tests/all/p3/clocks.rs @@ -0,0 +1,9 @@ +use super::run; +use test_programs_artifacts::*; + +foreach_clocks_0_3!(assert_test_exists); + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn clocks_0_3_sleep() -> anyhow::Result<()> { + run(CLOCKS_0_3_SLEEP_COMPONENT).await +} diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs new file mode 100644 index 0000000000..5576d91241 --- /dev/null +++ b/crates/wasi/tests/all/p3/mod.rs @@ -0,0 +1,123 @@ +use anyhow::{anyhow, Context as _}; +use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::Store; +use wasmtime_wasi::p3::bindings::Command; +use wasmtime_wasi::p3::cli::{WasiCliCtx, WasiCliView}; +use wasmtime_wasi::p3::clocks::{WasiClocksCtx, WasiClocksView}; +use wasmtime_wasi::p3::random::{WasiRandomCtx, WasiRandomView}; +use wasmtime_wasi::p3::sockets::{ + AllowedNetworkUses, SocketAddrCheck, WasiSocketsCtx, WasiSocketsView, +}; +use wasmtime_wasi::{IoView, WasiCtx, WasiCtxBuilder, WasiView}; + +macro_rules! assert_test_exists { + ($name:ident) => { + #[expect(unused_imports, reason = "just here to assert it exists")] + use self::$name as _; + }; +} + +struct Ctx { + cli: WasiCliCtx, + clocks: WasiClocksCtx, + random: WasiRandomCtx, + sockets: WasiSocketsCtx, + table: ResourceTable, + wasip2: WasiCtx, +} + +impl Default for Ctx { + fn default() -> Self { + Self { + cli: WasiCliCtx::default(), + clocks: WasiClocksCtx::default(), + sockets: WasiSocketsCtx { + socket_addr_check: SocketAddrCheck::new(|_, _| Box::pin(async { true })), + allowed_network_uses: AllowedNetworkUses { + ip_name_lookup: true, + udp: true, + tcp: true, + }, + }, + random: WasiRandomCtx::default(), + table: ResourceTable::default(), + wasip2: WasiCtxBuilder::new().inherit_stdio().build(), + } + } +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.wasip2 + } +} + +impl IoView for Ctx { + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} + +impl WasiCliView for Ctx { + fn cli(&self) -> &WasiCliCtx { + &self.cli + } +} + +impl WasiClocksView for Ctx { + fn clocks(&self) -> &WasiClocksCtx { + &self.clocks + } +} + +impl WasiRandomView for Ctx { + fn random(&mut self) -> &mut WasiRandomCtx { + &mut self.random + } +} + +impl WasiSocketsView for Ctx { + fn sockets(&self) -> &WasiSocketsCtx { + &self.sockets + } + + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} + +async fn run(path: &str) -> anyhow::Result<()> { + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + config.wasm_component_model_async(true); + }); + let component = Component::from_file(&engine, path).context("failed to compile component")?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to link `wasi:cli@0.2.x`")?; + wasmtime_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; + + let mut store = Store::new(&engine, Ctx::default()); + let command = Command::instantiate_async(&mut store, &component, &linker) + .await + .context("failed to instantiate `wasi:cli/command`")?; + let mut promises = wasmtime::component::PromisesUnordered::new(); + let p = command + .wasi_cli_run() + .call_run(&mut store) + .await + .context("failed to call `wasi:cli/run#run`")?; + promises.push(p); + promises + .next(&mut store) + .await + .context("failed to get promise")? + .context("promise missing")? + .map_err(|()| anyhow!("`wasi:cli/run#run` failed")) +} + +mod clocks; +mod random; +mod sockets; +//mod filesystem; +//mod cli; diff --git a/crates/wasi/tests/all/p3/random.rs b/crates/wasi/tests/all/p3/random.rs new file mode 100644 index 0000000000..0a3a9e1fd9 --- /dev/null +++ b/crates/wasi/tests/all/p3/random.rs @@ -0,0 +1,9 @@ +use super::run; +use test_programs_artifacts::*; + +foreach_random_0_3!(assert_test_exists); + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn random_0_3_imports() -> anyhow::Result<()> { + run(RANDOM_0_3_IMPORTS_COMPONENT).await +} diff --git a/crates/wasi/tests/all/p3/sockets.rs b/crates/wasi/tests/all/p3/sockets.rs new file mode 100644 index 0000000000..5ba389ec17 --- /dev/null +++ b/crates/wasi/tests/all/p3/sockets.rs @@ -0,0 +1,45 @@ +use super::run; +use test_programs_artifacts::*; + +foreach_sockets_0_3!(assert_test_exists); + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_ip_name_lookup() -> anyhow::Result<()> { + run(SOCKETS_0_3_IP_NAME_LOOKUP_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_bind() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_BIND_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_connect() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_CONNECT_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_sample_application() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_SAMPLE_APPLICATION_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_sockopts() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_SOCKOPTS_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_states() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_STATES_COMPONENT).await +} + +#[ignore = "needs `listen`"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn sockets_0_3_tcp_streams() -> anyhow::Result<()> { + run(SOCKETS_0_3_TCP_STREAMS_COMPONENT).await +}