diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a1f8428686a..75b018a4d9e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -523,6 +523,43 @@ jobs: env: GH_TOKEN: ${{ github.token }} + build-preview1-component-adapter: + name: Build wasi-preview1-component-adapter + needs: determine + if: needs.determine.outputs.run-full + runs-on: ubuntu-latest + permissions: + deployments: write + contents: write + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - run: rustup update stable && rustup default stable + - run: rustup target add wasm32-wasi wasm32-unknown-unknown + + - name: Install wasm-tools + run: | + curl -L https://github.com/bytecodealliance/wasm-tools/releases/download/wasm-tools-1.0.27/wasm-tools-1.0.27-x86_64-linux.tar.gz | tar xfz - + echo `pwd`/wasm-tools-1.0.27-x86_64-linux >> $GITHUB_PATH + + - run: ./ci/build-wasi-preview1-component-adapter.sh + env: + VERSION: ${{ github.sha }} + + - uses: actions/upload-artifact@v3 + with: + name: bins-wasi-preview1-component-adapter + path: target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.*.wasm + + + # common logic to cancel the entire run if this job fails + - run: gh run cancel ${{ github.run_id }} + if: failure() && github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + + bench: needs: determine if: needs.determine.outputs.run-full diff --git a/.github/workflows/publish-artifacts.yml b/.github/workflows/publish-artifacts.yml index ea3c3216158d..23d6a263bf12 100644 --- a/.github/workflows/publish-artifacts.yml +++ b/.github/workflows/publish-artifacts.yml @@ -42,7 +42,7 @@ jobs: - run: | mkdir dist mv -t dist bins-*/*.tar.* - mv -t dist bins-*/*.{zip,msi} + mv -t dist bins-*/*.{zip,msi,wasm} - name: Publish Release uses: ./.github/actions/github-release with: diff --git a/Cargo.lock b/Cargo.lock index f18b99a22888..7582f44e783e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arbitrary" @@ -107,7 +107,7 @@ checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -234,6 +234,10 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "byte-array-literals" +version = "10.0.0" + [[package]] name = "byteorder" version = "1.4.3" @@ -482,7 +486,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -521,7 +525,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -673,7 +677,7 @@ dependencies = [ "target-lexicon", "thiserror", "toml", - "wasmparser", + "wasmparser 0.103.0", "wat", ] @@ -849,7 +853,7 @@ dependencies = [ "serde", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.103.0", "wasmtime-types", "wat", ] @@ -1031,7 +1035,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -1042,7 +1046,7 @@ checksum = "f3cdeb9ec472d588e539a818b2dee436825730da08ad0017c4b1a17676bdc8b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -1538,6 +1542,9 @@ name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -2389,7 +2396,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.92", "version_check", ] @@ -2406,11 +2413,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2467,9 +2474,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.18" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -2817,7 +2824,7 @@ checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -3012,6 +3019,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -3020,7 +3038,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", "unicode-xid", ] @@ -3127,7 +3145,7 @@ checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -3190,7 +3208,7 @@ checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -3247,7 +3265,7 @@ checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -3298,6 +3316,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + [[package]] name = "unicode-normalization" version = "0.1.21" @@ -3307,6 +3331,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.9" @@ -3374,6 +3404,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "verify-component-adapter" +version = "10.0.0" +dependencies = [ + "anyhow", + "wasmparser 0.92.0", + "wat", +] + [[package]] name = "version_check" version = "0.9.4" @@ -3497,6 +3536,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "wasi-preview1-component-adapter" +version = "10.0.0" +dependencies = [ + "byte-array-literals", + "object", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-encoder 0.25.0", + "wit-bindgen", +] + [[package]] name = "wasi-tokio" version = "10.0.0" @@ -3535,7 +3585,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.92", "wasm-bindgen-shared", ] @@ -3557,7 +3607,7 @@ checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3586,6 +3636,19 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-metadata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbdef99fafff010c57fabb7bc703f0903ec16fcee49207a22dcc4f78ea44562f" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "wasm-encoder 0.26.0", + "wasmparser 0.104.0", +] + [[package]] name = "wasm-mutate" version = "0.2.23" @@ -3597,7 +3660,7 @@ dependencies = [ "rand 0.8.5", "thiserror", "wasm-encoder 0.25.0", - "wasmparser", + "wasmparser 0.103.0", ] [[package]] @@ -3611,7 +3674,7 @@ dependencies = [ "indexmap", "leb128", "wasm-encoder 0.25.0", - "wasmparser", + "wasmparser 0.103.0", ] [[package]] @@ -3652,6 +3715,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "wasmparser" +version = "0.92.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da34cec2a8c23db906cdf8b26e988d7a7f0d549eb5d51299129647af61a1b37" +dependencies = [ + "indexmap", +] + [[package]] name = "wasmparser" version = "0.103.0" @@ -3662,6 +3734,16 @@ dependencies = [ "url", ] +[[package]] +name = "wasmparser" +version = "0.104.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a396af81a7c56ad976131d6a35e4b693b78a1ea0357843bd436b4577e254a7d" +dependencies = [ + "indexmap", + "url", +] + [[package]] name = "wasmparser-nostd" version = "0.91.0" @@ -3678,7 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51befda9d7eefac615a2ef75f42d2f2bd243cdabaa141a8ea0f9ffa3fc79ccf4" dependencies = [ "anyhow", - "wasmparser", + "wasmparser 0.103.0", ] [[package]] @@ -3705,7 +3787,7 @@ dependencies = [ "target-lexicon", "tempfile", "wasi-cap-std-sync", - "wasmparser", + "wasmparser 0.103.0", "wasmtime-cache", "wasmtime-component-macro", "wasmtime-component-util", @@ -3821,7 +3903,7 @@ dependencies = [ "tokio", "walkdir", "wasm-encoder 0.26.0", - "wasmparser", + "wasmparser 0.103.0", "wasmtime", "wasmtime-cache", "wasmtime-cli-flags", @@ -3861,7 +3943,7 @@ dependencies = [ "component-macro-test-helpers", "proc-macro2", "quote", - "syn", + "syn 1.0.92", "tracing", "wasmtime", "wasmtime-component-util", @@ -3889,7 +3971,7 @@ dependencies = [ "object", "target-lexicon", "thiserror", - "wasmparser", + "wasmparser 0.103.0", "wasmtime-cranelift-shared", "wasmtime-environ", ] @@ -3925,7 +4007,7 @@ dependencies = [ "target-lexicon", "thiserror", "wasm-encoder 0.26.0", - "wasmparser", + "wasmparser 0.103.0", "wasmprinter", "wasmtime-component-util", "wasmtime-types", @@ -3940,7 +4022,7 @@ dependencies = [ "component-fuzz-util", "env_logger 0.10.0", "libfuzzer-sys", - "wasmparser", + "wasmparser 0.103.0", "wasmprinter", "wasmtime-environ", "wat", @@ -3994,7 +4076,7 @@ dependencies = [ "rand 0.8.5", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.103.0", "wasmtime", "wasmtime-fuzzing", ] @@ -4019,7 +4101,7 @@ dependencies = [ "wasm-smith", "wasm-spec-interpreter", "wasmi", - "wasmparser", + "wasmparser 0.103.0", "wasmprinter", "wasmtime", "wasmtime-wast", @@ -4101,7 +4183,7 @@ dependencies = [ "cranelift-entity", "serde", "thiserror", - "wasmparser", + "wasmparser 0.103.0", ] [[package]] @@ -4187,7 +4269,7 @@ dependencies = [ "gimli", "object", "target-lexicon", - "wasmparser", + "wasmparser 0.103.0", "wasmtime-cranelift-shared", "wasmtime-environ", "winch-codegen", @@ -4289,7 +4371,7 @@ dependencies = [ "proc-macro2", "quote", "shellexpand", - "syn", + "syn 1.0.92", "witx", ] @@ -4299,7 +4381,7 @@ version = "10.0.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", "wiggle", "wiggle-generate", ] @@ -4358,14 +4440,14 @@ dependencies = [ "regalloc2", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.103.0", ] [[package]] name = "winch-environ" version = "0.8.0" dependencies = [ - "wasmparser", + "wasmparser 0.103.0", "wasmtime-environ", "winch-codegen", ] @@ -4395,7 +4477,7 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -4411,7 +4493,7 @@ dependencies = [ "similar", "target-lexicon", "toml", - "wasmparser", + "wasmparser 0.103.0", "wasmtime-environ", "wat", "winch-codegen", @@ -4563,11 +4645,86 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad22d93d3f55847ac4b3df31607a26f35231754ef472382319de032770d8b5bf" +dependencies = [ + "bitflags 2.2.1", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc1b5a6e87f16491f2297f75312dc0fb354f8c88c8bece53ea0d3167fc98867" +dependencies = [ + "anyhow", + "wit-component", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7946a66f1132d3322c29de9d28097bd263f67e1e0909054f91253aa103cdf8be" +dependencies = [ + "heck", + "wasm-metadata", + "wit-bindgen-core", + "wit-bindgen-rust-lib", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-lib" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0baf7325748c5d363ab6ed3ddbd155c241cfe385410c61f2505ec978a61a2d2c" +dependencies = [ + "heck", + "wit-bindgen-core", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c131da5d2ba7746908e1401d474640371c31ad05281528c2a9e945a87d19be" +dependencies = [ + "anyhow", + "proc-macro2", + "syn 2.0.16", + "wit-bindgen-core", + "wit-bindgen-rust", + "wit-component", +] + +[[package]] +name = "wit-component" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e291ff83cb9c8e59963cc6922bdda77ed8f5517d6835f0c98070c4e7f1ae4996" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "indexmap", + "log", + "url", + "wasm-encoder 0.26.0", + "wasm-metadata", + "wasmparser 0.104.0", + "wit-parser", +] + [[package]] name = "wit-parser" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "365273e457e2cacf762df10df4b1e0d4d9912b5332ff4cf2a172760fd67b7ec4" +checksum = "5ca2581061573ef6d1754983d7a9b3ed5871ef859d52708ea9a0f5af32919172" dependencies = [ "anyhow", "id-arena", @@ -4616,7 +4773,7 @@ checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 5fa228cd6119..3b51a55dd603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,8 @@ members = [ "crates/cli-flags", "crates/environ/fuzz", "crates/jit-icache-coherence", + "crates/wasi-preview1-component-adapter", + "crates/wasi-preview1-component-adapter/verify", "crates/winch", "examples/fib-debug/wasm", "examples/wasi/wasm", @@ -174,6 +176,9 @@ winch-environ = { path = "winch/environ", version = "=0.8.0" } winch-filetests = { path = "winch/filetests" } winch-test-macros = { path = "winch/test-macros" } +wasi-preview1-component-adapter = { path = "crates/wasi-preview1-component-adapter" } +byte-array-literals = { path = "crates/wasi-preview1-component-adapter/byte-array-literals" } + target-lexicon = { version = "0.12.3", default-features = false, features = ["std"] } anyhow = "1.0.22" wasmparser = "0.103.0" @@ -267,3 +272,19 @@ harness = false name = "wasi" harness = false +[profile.release.package.wasi-preview1-component-adapter] +opt-level = 's' +strip = 'debuginfo' + +[profile.dev.package.wasi-preview1-component-adapter] +# Make dev look like a release build since this adapter module won't work with +# a debug build that uses data segments and such. +incremental = false +opt-level = 's' +# Omit assertions, which include failure messages which require string +# initializers. +debug-assertions = false +# Omit integer overflow checks, which include failure messages which require +# string initializers. +overflow-checks = false + diff --git a/ci/build-wasi-preview1-component-adapter.sh b/ci/build-wasi-preview1-component-adapter.sh new file mode 100755 index 000000000000..ac82f6765518 --- /dev/null +++ b/ci/build-wasi-preview1-component-adapter.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -ex + +# Debug build, default features (reactor) +cargo build -p wasi-preview1-component-adapter --target wasm32-unknown-unknown +cargo run -p verify-component-adapter -- target/wasm32-unknown-unknown/debug/wasi_preview1_component_adapter.wasm + +# Debug build, command +cargo build -p wasi-preview1-component-adapter --target wasm32-unknown-unknown --no-default-features --features command +cargo run -p verify-component-adapter -- target/wasm32-unknown-unknown/debug/wasi_preview1_component_adapter.wasm + +# Release build, command +cargo build -p wasi-preview1-component-adapter --target wasm32-unknown-unknown --release --no-default-features --features command +cargo run -p verify-component-adapter -- target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.wasm +wasm-tools metadata add --name "wasi_preview1_component_adapter.command.adapter:${VERSION}" target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.wasm -o target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.command.wasm + +# Release build, default features (reactor) +cargo build -p wasi-preview1-component-adapter --target wasm32-unknown-unknown --release +cargo run -p verify-component-adapter -- ./target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.wasm +wasm-tools metadata add --name "wasi_preview1_component_adapter.reactor.adapter:${VERSION}" target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.wasm -o target/wasm32-unknown-unknown/release/wasi_preview1_component_adapter.reactor.wasm diff --git a/crates/wasi-preview1-component-adapter/Cargo.toml b/crates/wasi-preview1-component-adapter/Cargo.toml new file mode 100644 index 000000000000..c2ca4c9c61e6 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "wasi-preview1-component-adapter" +version.workspace = true +authors.workspace = true +edition.workspace = true +publish = false + +[dependencies] +wasi = { version = "0.11.0", default-features = false } +wit-bindgen = { version = "0.6.0", default-features = false, features = ["macros"] } +byte-array-literals = { workspace = true } + +[build-dependencies] +wasm-encoder = "0.25" +object = { version = "0.30.0", default-features = false, features = ["archive"] } + +[lib] +test = false +crate-type = ["cdylib"] + +[features] +default = ["reactor"] +reactor = [] +command = [] diff --git a/crates/wasi-preview1-component-adapter/README.md b/crates/wasi-preview1-component-adapter/README.md new file mode 100644 index 000000000000..83dc9212ea15 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/README.md @@ -0,0 +1,56 @@ +# `wasi_snapshot_preview1.wasm` + +> **Note**: This repository is a work in progress. This is intended to be an +> internal tool which not everyone has to look at but many might rely on. You +> may need to reach out via issues or +> [Zulip](https://bytecodealliance.zulipchat.com/) to learn more about this +> repository. + +This repository currently contains an implementation of a WebAssembly module: +`wasi_snapshot_preview1.wasm`. This module bridges the `wasi_snapshot_preview1` +ABI to the preview2 ABI of the component model. At this time the preview2 APIs +themselves are not done being specified so a local copy of `wit/*.wit` is used +instead. + +## Building + +This adapter can be built with: + +```sh +$ cargo build --target wasm32-unknown-unknown --release +``` + +And the artifact will be located at +`target/wasm32-unknown-unknown/release/wasi_snapshot_preview1.wasm`. +Alternatively the latest copy of this can be [downloaded from the releases +page][latest-release] + +[latest-release]: https://github.com/bytecodealliance/preview2-prototyping/releases/tag/latest + +## Using + +With a `wasi_snapshot_preview1.wasm` file on-hand you can create a component +from a module that imports WASI functions using the [`wasm-tools` +CLI](https://github.com/bytecodealliance/wasm-tools) + +```sh +$ cat foo.rs +fn main() { + println!("Hello, world!"); +} +$ rustc foo.rs --target wasm32-wasi +$ wasm-tools print foo.wasm | grep '(import' + (import "wasi_snapshot_preview1" "fd_write" (func ... + (import "wasi_snapshot_preview1" "environ_get" (func ... + (import "wasi_snapshot_preview1" "environ_sizes_get" ... + (import "wasi_snapshot_preview1" "proc_exit" (func ... +$ wasm-tools component new foo.wasm --adapt wasi_snapshot_preview1.wasm -o component.wasm + +# Inspect the generated `component.wasm` +$ wasm-tools validate component.wasm --features component-model +$ wasm-tools component wit component.wasm +``` + +Here the `component.wasm` that's generated is a ready-to-run component which +imports wasi preview2 functions and is compatible with the wasi-preview1-using +module internally. diff --git a/crates/wasi-preview1-component-adapter/build.rs b/crates/wasi-preview1-component-adapter/build.rs new file mode 100644 index 000000000000..fcd7b7c6dac9 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/build.rs @@ -0,0 +1,351 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + let wasm = build_raw_intrinsics(); + let archive = build_archive(&wasm); + + std::fs::write(out_dir.join("libwasm-raw-intrinsics.a"), &archive).unwrap(); + println!("cargo:rustc-link-lib=static=wasm-raw-intrinsics"); + println!( + "cargo:rustc-link-search=native={}", + out_dir.to_str().unwrap() + ); + + // Some specific flags to `wasm-ld` to inform the shape of this adapter. + // Notably we're importing memory from the main module and additionally our + // own module has no stack at all since it's specifically allocated at + // startup. + println!("cargo:rustc-link-arg=--import-memory"); + println!("cargo:rustc-link-arg=-zstack-size=0"); +} + +/// This function will produce a wasm module which is itself an object file +/// that is the basic equivalent of: +/// +/// ```rust +/// std::arch::global_asm!( +/// " +/// .globaltype internal_state_ptr, i32 +/// internal_state_ptr: +/// " +/// ); +/// +/// #[no_mangle] +/// extern "C" fn get_state_ptr() -> *mut u8 { +/// unsafe { +/// let ret: *mut u8; +/// std::arch::asm!( +/// " +/// global.get internal_state_ptr +/// ", +/// out(local) ret, +/// options(nostack, readonly) +/// ); +/// ret +/// } +/// } +/// +/// #[no_mangle] +/// extern "C" fn set_state_ptr(val: *mut u8) { +/// unsafe { +/// std::arch::asm!( +/// " +/// local.get {} +/// global.set internal_state_ptr +/// ", +/// in(local) val, +/// options(nostack, readonly) +/// ); +/// } +/// } +/// +/// // And likewise for `allocation_state`, `get_allocation_state`, and `set_allocation_state` +/// ``` +/// +/// The main trickiness here is getting the `reloc.CODE` and `linking` sections +/// right. +fn build_raw_intrinsics() -> Vec { + use wasm_encoder::Instruction::*; + use wasm_encoder::*; + + let mut module = Module::new(); + + let mut types = TypeSection::new(); + types.function([], [ValType::I32]); + types.function([ValType::I32], []); + module.section(&types); + + // Declare the functions, using the type we just added. + let mut funcs = FunctionSection::new(); + funcs.function(0); + funcs.function(1); + funcs.function(0); + funcs.function(1); + funcs.function(0); + funcs.function(1); + module.section(&funcs); + + // Declare the globals. + let mut globals = GlobalSection::new(); + // internal_state_ptr + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + }, + &ConstExpr::i32_const(0), + ); + // allocation_state + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + }, + &ConstExpr::i32_const(0), + ); + // stderr_stream + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + }, + &ConstExpr::i32_const(123), + ); + module.section(&globals); + + // Here the `code` section is defined. This is tricky because an offset is + // needed within the code section itself for the `reloc.CODE` section + // later. At this time `wasm-encoder` doesn't give enough functionality to + // use the high-level APIs. so everything is done manually here. + // + // First the function bodies are created and then they're appended into a + // code section. + + let mut code = Vec::new(); + 6u32.encode(&mut code); // number of functions + + let global_get = 0x23; + let global_set = 0x24; + + let encode = |code: &mut _, global, instruction| { + assert!(global < 0x7F); + + let mut body = Vec::new(); + 0u32.encode(&mut body); // no locals + if instruction == global_set { + LocalGet(0).encode(&mut body); + } + let global_offset = body.len() + 1; + // global.get $global ;; but with maximal encoding of $global + body.extend_from_slice(&[instruction, 0x80u8 + global, 0x80, 0x80, 0x80, 0x00]); + End.encode(&mut body); + body.len().encode(code); // length of the function + let offset = code.len() + global_offset; + code.extend_from_slice(&body); // the function itself + offset + }; + + let internal_state_ptr_ref1 = encode(&mut code, 0, global_get); // get_state_ptr + let internal_state_ptr_ref2 = encode(&mut code, 0, global_set); // set_state_ptr + let allocation_state_ref1 = encode(&mut code, 1, global_get); // get_allocation_state + let allocation_state_ref2 = encode(&mut code, 1, global_set); // set_allocation_state + let stderr_stream_ref1 = encode(&mut code, 2, global_get); // get_stderr_stream + let stderr_stream_ref2 = encode(&mut code, 2, global_set); // set_stderr_stream + + module.section(&RawSection { + id: SectionId::Code as u8, + data: &code, + }); + + // Here the linking section is constructed. There is one symbol for each function and global. The injected + // globals here are referenced in the relocations below. + // + // More information about this format is at + // https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md + { + let mut linking = Vec::new(); + linking.push(0x02); // version + + linking.push(0x08); // `WASM_SYMBOL_TABLE` + let mut subsection = Vec::new(); + 9u32.encode(&mut subsection); // 9 symbols (6 functions + 3 globals) + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 0u32.encode(&mut subsection); // function index + "get_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 1u32.encode(&mut subsection); // function index + "set_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 2u32.encode(&mut subsection); // function index + "get_allocation_state".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 3u32.encode(&mut subsection); // function index + "set_allocation_state".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 4u32.encode(&mut subsection); // function index + "get_stderr_stream".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 5u32.encode(&mut subsection); // function index + "set_stderr_stream".encode(&mut subsection); // symbol name + + subsection.push(0x02); // SYMTAB_GLOBAL + 0x02.encode(&mut subsection); // flags (WASM_SYM_BINDING_LOCAL) + 0u32.encode(&mut subsection); // global index + "internal_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x02); // SYMTAB_GLOBAL + 0x00.encode(&mut subsection); // flags + 1u32.encode(&mut subsection); // global index + "allocation_state".encode(&mut subsection); // symbol name + + subsection.push(0x02); // SYMTAB_GLOBAL + 0x00.encode(&mut subsection); // flags + 2u32.encode(&mut subsection); // global index + "stderr_stream".encode(&mut subsection); // symbol name + + subsection.encode(&mut linking); + module.section(&CustomSection { + name: "linking", + data: &linking, + }); + } + + // A `reloc.CODE` section is appended here with relocations for the + // `global`-referencing instructions that were added. + { + let mut reloc = Vec::new(); + 3u32.encode(&mut reloc); // target section (code is the 4th section, 3 when 0-indexed) + 6u32.encode(&mut reloc); // 6 relocations + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + internal_state_ptr_ref1.encode(&mut reloc); // offset + 6u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + internal_state_ptr_ref2.encode(&mut reloc); // offset + 6u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + allocation_state_ref1.encode(&mut reloc); // offset + 7u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + allocation_state_ref2.encode(&mut reloc); // offset + 7u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + stderr_stream_ref1.encode(&mut reloc); // offset + 8u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + stderr_stream_ref2.encode(&mut reloc); // offset + 8u32.encode(&mut reloc); // symbol index + + module.section(&CustomSection { + name: "reloc.CODE", + data: &reloc, + }); + } + + module.finish() +} + +/// This function produces the output of `llvm-ar crus libfoo.a foo.o` given +/// the object file above as input. The archive is what's eventually fed to +/// LLD. +/// +/// Like above this is still tricky, mainly around the production of the symbol +/// table. +fn build_archive(wasm: &[u8]) -> Vec { + use object::{bytes_of, endian::BigEndian, U32Bytes}; + + let mut archive = Vec::new(); + archive.extend_from_slice(&object::archive::MAGIC); + + // The symbol table is in the "GNU" format which means it has a structure + // that looks like: + // + // * a big-endian 32-bit integer for the number of symbols + // * N big-endian 32-bit integers for the offset to the object file, within + // the entire archive, for which object has the symbol + // * N nul-delimited strings for each symbol + // + // Here we're building an archive with just a few symbols so it's a bit + // easier. Note though we don't know the offset of our `intrinsics.o` up + // front so it's left as 0 for now and filled in later. + + let syms = [ + "get_state_ptr", + "set_state_ptr", + "get_allocation_state", + "set_allocation_state", + "allocation_state", + ]; + + let mut symbol_table = Vec::new(); + symbol_table.extend_from_slice(bytes_of(&U32Bytes::new(BigEndian, syms.len() as u32))); + for _ in syms.iter() { + symbol_table.extend_from_slice(bytes_of(&U32Bytes::new(BigEndian, 0))); + } + for s in syms.iter() { + symbol_table.extend_from_slice(&std::ffi::CString::new(*s).unwrap().into_bytes_with_nul()); + } + + archive.extend_from_slice(bytes_of(&object::archive::Header { + name: *b"/ ", + date: *b"0 ", + uid: *b"0 ", + gid: *b"0 ", + mode: *b"0 ", + size: format!("{:<10}", symbol_table.len()) + .as_bytes() + .try_into() + .unwrap(), + terminator: object::archive::TERMINATOR, + })); + let symtab_offset = archive.len(); + archive.extend_from_slice(&symbol_table); + + // All archive members must start on even offsets + if archive.len() & 1 == 1 { + archive.push(0x00); + } + + // Now that we have the starting offset of the `intrinsics.o` file go back + // and fill in the offset within the symbol table generated earlier. + let member_offset = archive.len(); + for (index, _) in syms.iter().enumerate() { + let index = index + 1; + archive[symtab_offset + (index * 4)..][..4].copy_from_slice(bytes_of(&U32Bytes::new( + BigEndian, + member_offset.try_into().unwrap(), + ))); + } + + archive.extend_from_slice(object::bytes_of(&object::archive::Header { + name: *b"intrinsics.o ", + date: *b"0 ", + uid: *b"0 ", + gid: *b"0 ", + mode: *b"644 ", + size: format!("{:<10}", wasm.len()).as_bytes().try_into().unwrap(), + terminator: object::archive::TERMINATOR, + })); + archive.extend_from_slice(&wasm); + archive +} diff --git a/crates/wasi-preview1-component-adapter/byte-array-literals/Cargo.toml b/crates/wasi-preview1-component-adapter/byte-array-literals/Cargo.toml new file mode 100644 index 000000000000..a9d08dc9890f --- /dev/null +++ b/crates/wasi-preview1-component-adapter/byte-array-literals/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "byte-array-literals" +version.workspace = true +authors.workspace = true +edition.workspace = true +publish = false + +[lib] +proc-macro = true diff --git a/crates/wasi-preview1-component-adapter/byte-array-literals/README.md b/crates/wasi-preview1-component-adapter/byte-array-literals/README.md new file mode 100644 index 000000000000..1568549409c0 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/byte-array-literals/README.md @@ -0,0 +1,15 @@ +# byte-array-literals + +This crate exists to solve a very peculiar problem for the +`wasi-preview1-component-adapter`: we want to use string literals in our +source code, but the resulting binary (when compiled for +wasm32-unknown-unknown) cannot contain any data sections. + +The answer that @sunfishcode discovered is that these string literals, if +represented as an array of u8 literals, these will somehow not end up in the +data section, at least when compiled with opt-level='s' on today's rustc +(1.69.0). So, this crate exists to transform these literals using a proc +macro. + +It is very possible this cheat code will abruptly stop working in some future +compiler, but we'll cross that bridge when we get to it. diff --git a/crates/wasi-preview1-component-adapter/byte-array-literals/src/lib.rs b/crates/wasi-preview1-component-adapter/byte-array-literals/src/lib.rs new file mode 100644 index 000000000000..ea9f013d9f53 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/byte-array-literals/src/lib.rs @@ -0,0 +1,98 @@ +extern crate proc_macro; + +use proc_macro::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree}; + +/// Expand a `str` literal into a byte array. +#[proc_macro] +pub fn str(input: TokenStream) -> TokenStream { + let rv = convert_str(input); + + vec![TokenTree::Group(Group::new( + Delimiter::Bracket, + rv.into_iter().collect(), + ))] + .into_iter() + .collect() +} + +/// The same as `str` but appends a `'\n'`. +#[proc_macro] +pub fn str_nl(input: TokenStream) -> TokenStream { + let mut rv = convert_str(input); + + rv.push(TokenTree::Literal(Literal::u8_suffixed(b'\n'))); + + vec![TokenTree::Group(Group::new( + Delimiter::Bracket, + rv.into_iter().collect(), + ))] + .into_iter() + .collect() +} + +fn convert_str(input: TokenStream) -> Vec { + let mut it = input.into_iter(); + + let mut tokens = Vec::new(); + match it.next() { + Some(TokenTree::Literal(l)) => { + for b in to_string(l).into_bytes() { + tokens.push(TokenTree::Literal(Literal::u8_suffixed(b))); + tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone))); + } + } + _ => panic!(), + } + + assert!(it.next().is_none()); + tokens +} + +fn to_string(lit: Literal) -> String { + let formatted = lit.to_string(); + + let mut it = formatted.chars(); + assert_eq!(it.next(), Some('"')); + + let mut rv = String::new(); + loop { + match it.next() { + Some('"') => match it.next() { + Some(_) => panic!(), + None => break, + }, + Some('\\') => match it.next() { + Some('x') => { + let hi = it.next().unwrap().to_digit(16).unwrap(); + let lo = it.next().unwrap().to_digit(16).unwrap(); + let v = (hi << 16) | lo; + rv.push(v as u8 as char); + } + Some('u') => { + assert_eq!(it.next(), Some('{')); + let mut c = it.next().unwrap(); + let mut ch = 0; + while let Some(v) = c.to_digit(16) { + ch *= 16; + ch |= v; + c = it.next().unwrap(); + } + assert_eq!(c, '}'); + rv.push(::std::char::from_u32(ch).unwrap()); + } + Some('0') => rv.push('\0'), + Some('\\') => rv.push('\\'), + Some('\"') => rv.push('\"'), + Some('r') => rv.push('\r'), + Some('n') => rv.push('\n'), + Some('t') => rv.push('\t'), + Some(_) => panic!(), + None => panic!(), + }, + Some(c) => rv.push(c), + None => panic!(), + } + } + + rv +} diff --git a/crates/wasi-preview1-component-adapter/src/descriptors.rs b/crates/wasi-preview1-component-adapter/src/descriptors.rs new file mode 100644 index 000000000000..214794952ba0 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/src/descriptors.rs @@ -0,0 +1,420 @@ +use crate::bindings::streams::{self, InputStream, OutputStream}; +use crate::bindings::{filesystem, tcp}; +use crate::{set_stderr_stream, BumpArena, File, ImportAlloc, TrappingUnwrap, WasmStr}; +use core::cell::{Cell, UnsafeCell}; +use core::mem::MaybeUninit; +use wasi::{Errno, Fd}; + +pub const MAX_DESCRIPTORS: usize = 128; + +#[repr(C)] +pub enum Descriptor { + /// A closed descriptor, holding a reference to the previous closed + /// descriptor to support reusing them. + Closed(Option), + + /// Input and/or output wasi-streams, along with stream metadata. + Streams(Streams), +} + +impl Drop for Descriptor { + fn drop(&mut self) { + match self { + Descriptor::Streams(stream) => { + if let Some(input) = stream.input.get() { + streams::drop_input_stream(input); + } + if let Some(output) = stream.output.get() { + streams::drop_output_stream(output); + } + match &stream.type_ { + StreamType::File(file) => filesystem::drop_descriptor(file.fd), + StreamType::Socket(_) => unreachable!(), + StreamType::Stdio => {} + } + } + Descriptor::Closed(_) => {} + } + } +} + +/// Input and/or output wasi-streams, along with a stream type that +/// identifies what kind of stream they are and possibly supporting +/// type-specific operations like seeking. +pub struct Streams { + /// The output stream, if present. + pub input: Cell>, + + /// The input stream, if present. + pub output: Cell>, + + /// Information about the source of the stream. + pub type_: StreamType, +} + +impl Streams { + /// Return the input stream, initializing it on the fly if needed. + pub fn get_read_stream(&self) -> Result { + match &self.input.get() { + Some(wasi_stream) => Ok(*wasi_stream), + None => match &self.type_ { + // For directories, preview 1 behavior was to return ERRNO_BADF on attempts to read + // or write. + StreamType::File(File { + descriptor_type: filesystem::DescriptorType::Directory, + .. + }) => Err(wasi::ERRNO_BADF), + // For files, we may have adjusted the position for seeking, so + // create a new stream. + StreamType::File(file) => { + let input = filesystem::read_via_stream(file.fd, file.position.get()); + self.input.set(Some(input)); + Ok(input) + } + _ => Err(wasi::ERRNO_BADF), + }, + } + } + + /// Return the output stream, initializing it on the fly if needed. + pub fn get_write_stream(&self) -> Result { + match &self.output.get() { + Some(wasi_stream) => Ok(*wasi_stream), + None => match &self.type_ { + // For directories, preview 1 behavior was to return ERRNO_BADF on attempts to read + // or write. + StreamType::File(File { + descriptor_type: filesystem::DescriptorType::Directory, + .. + }) => Err(wasi::ERRNO_BADF), + // For files, we may have adjusted the position for seeking, so + // create a new stream. + StreamType::File(file) => { + let output = if file.append { + filesystem::append_via_stream(file.fd) + } else { + filesystem::write_via_stream(file.fd, file.position.get()) + }; + self.output.set(Some(output)); + Ok(output) + } + _ => Err(wasi::ERRNO_BADF), + }, + } + } +} + +#[allow(dead_code)] // until Socket is implemented +pub enum StreamType { + /// Stream is used for implementing stdio. + Stdio, + + /// Streaming data with a file. + File(File), + + /// Streaming data with a socket connection. + Socket(tcp::TcpSocket), +} + +#[repr(C)] +pub struct Descriptors { + /// Storage of mapping from preview1 file descriptors to preview2 file + /// descriptors. + table: UnsafeCell>, + table_len: Cell, + + /// Points to the head of a free-list of closed file descriptors. + closed: Option, + + /// Preopened directories. Initialized lazily. Access with `State::get_preopens` + /// to take care of initialization. + preopens: Cell>, +} + +impl Descriptors { + pub fn new(import_alloc: &ImportAlloc, arena: &BumpArena) -> Self { + let d = Descriptors { + table: UnsafeCell::new(MaybeUninit::uninit()), + table_len: Cell::new(0), + closed: None, + preopens: Cell::new(None), + }; + + let stdio = crate::bindings::preopens::get_stdio(); + unsafe { set_stderr_stream(stdio.stderr) }; + + d.push(Descriptor::Streams(Streams { + input: Cell::new(Some(stdio.stdin)), + output: Cell::new(None), + type_: StreamType::Stdio, + })) + .trapping_unwrap(); + d.push(Descriptor::Streams(Streams { + input: Cell::new(None), + output: Cell::new(Some(stdio.stdout)), + type_: StreamType::Stdio, + })) + .trapping_unwrap(); + d.push(Descriptor::Streams(Streams { + input: Cell::new(None), + output: Cell::new(Some(stdio.stderr)), + type_: StreamType::Stdio, + })) + .trapping_unwrap(); + + #[link(wasm_import_module = "preopens")] + extern "C" { + #[link_name = "get-directories"] + fn get_preopens_import(rval: *mut PreopenList); + } + let mut list = PreopenList { + base: std::ptr::null(), + len: 0, + }; + import_alloc.with_arena(arena, || unsafe { + get_preopens_import(&mut list as *mut _) + }); + let preopens: &'static [Preopen] = unsafe { + // allocation comes from long lived arena, so it is safe to + // cast this to a &'static slice: + std::slice::from_raw_parts(list.base, list.len) + }; + for preopen in preopens { + // Expectation is that the descriptor index is initialized with + // stdio (0,1,2) and no others, so that preopens are 3.. + d.push(Descriptor::Streams(Streams { + input: Cell::new(None), + output: Cell::new(None), + type_: StreamType::File(File { + fd: preopen.descriptor, + descriptor_type: crate::bindings::filesystem::get_type(preopen.descriptor) + .trapping_unwrap(), + position: Cell::new(0), + append: false, + blocking: false, + }), + })) + .trapping_unwrap(); + } + + d.preopens.set(Some(preopens)); + d + } + + fn push(&self, desc: Descriptor) -> Result { + unsafe { + let table = (*self.table.get()).as_mut_ptr(); + let len = usize::try_from(self.table_len.get()).trapping_unwrap(); + if len >= (*table).len() { + return Err(wasi::ERRNO_NOMEM); + } + core::ptr::addr_of_mut!((*table)[len]).write(desc); + self.table_len.set(u16::try_from(len + 1).trapping_unwrap()); + Ok(Fd::from(u32::try_from(len).trapping_unwrap())) + } + } + + fn table(&self) -> &[Descriptor] { + unsafe { + std::slice::from_raw_parts( + (*self.table.get()).as_ptr().cast(), + usize::try_from(self.table_len.get()).trapping_unwrap(), + ) + } + } + + fn table_mut(&mut self) -> &mut [Descriptor] { + unsafe { + std::slice::from_raw_parts_mut( + (*self.table.get()).as_mut_ptr().cast(), + usize::try_from(self.table_len.get()).trapping_unwrap(), + ) + } + } + + pub fn open(&mut self, d: Descriptor) -> Result { + match self.closed { + // No closed descriptors: expand table + None => self.push(d), + Some(freelist_head) => { + // Pop an item off the freelist + let freelist_desc = self.get_mut(freelist_head).trapping_unwrap(); + let next_closed = match freelist_desc { + Descriptor::Closed(next) => *next, + _ => unreachable!("impossible: freelist points to a closed descriptor"), + }; + // Write descriptor to the entry at the nead of the list + *freelist_desc = d; + // Point closed to the following item + self.closed = next_closed; + Ok(freelist_head) + } + } + } + + pub fn get(&self, fd: Fd) -> Result<&Descriptor, Errno> { + self.table() + .get(usize::try_from(fd).trapping_unwrap()) + .ok_or(wasi::ERRNO_BADF) + } + + pub fn get_mut(&mut self, fd: Fd) -> Result<&mut Descriptor, Errno> { + self.table_mut() + .get_mut(usize::try_from(fd).trapping_unwrap()) + .ok_or(wasi::ERRNO_BADF) + } + + pub fn get_preopen(&self, fd: Fd) -> Option<&Preopen> { + let preopens = self.preopens.get().trapping_unwrap(); + // Subtract 3 for the stdio indices to compute the preopen index. + let index = fd.checked_sub(3)? as usize; + preopens.get(index) + } + + // Internal: close a fd, returning the descriptor. + fn close_(&mut self, fd: Fd) -> Result { + // Throw an error if closing an fd which is already closed + match self.get_mut(fd)? { + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF)?, + _ => {} + } + // Mutate the descriptor to be closed, and push the closed fd onto the head of the linked list: + let last_closed = self.closed; + let prev = std::mem::replace(self.get_mut(fd)?, Descriptor::Closed(last_closed)); + self.closed = Some(fd); + Ok(prev) + } + + // Close an fd. + pub fn close(&mut self, fd: Fd) -> Result<(), Errno> { + drop(self.close_(fd)?); + Ok(()) + } + + // Expand the table by pushing a closed descriptor to the end. Used for renumbering. + fn push_closed(&mut self) -> Result<(), Errno> { + let old_closed = self.closed; + let new_closed = self.push(Descriptor::Closed(old_closed))?; + self.closed = Some(new_closed); + Ok(()) + } + + // Implementation of fd_renumber + pub fn renumber(&mut self, from_fd: Fd, to_fd: Fd) -> Result<(), Errno> { + // First, ensure from_fd is in bounds: + drop(self.get(from_fd)?); + // Expand table until to_fd is in bounds as well: + while self.table_len.get() as u32 <= to_fd as u32 { + self.push_closed()?; + } + // Then, close from_fd and put its contents into to_fd: + let desc = self.close_(from_fd)?; + // TODO FIXME if this overwrites a preopen, do we need to clear it from the preopen table? + *self.get_mut(to_fd)? = desc; + + Ok(()) + } + + // A bunch of helper functions implemented in terms of the above pub functions: + + pub fn get_stream_with_error(&self, fd: Fd, error: Errno) -> Result<&Streams, Errno> { + match self.get(fd)? { + Descriptor::Streams(streams) => Ok(streams), + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF), + } + } + + pub fn get_file_with_error(&self, fd: Fd, error: Errno) -> Result<&File, Errno> { + match self.get(fd)? { + Descriptor::Streams(Streams { + type_: + StreamType::File(File { + descriptor_type: filesystem::DescriptorType::Directory, + .. + }), + .. + }) => Err(wasi::ERRNO_BADF), + Descriptor::Streams(Streams { + type_: StreamType::File(file), + .. + }) => Ok(file), + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF), + _ => Err(error), + } + } + + #[allow(dead_code)] // until Socket is implemented + pub fn get_socket(&self, fd: Fd) -> Result { + match self.get(fd)? { + Descriptor::Streams(Streams { + type_: StreamType::Socket(socket), + .. + }) => Ok(*socket), + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF), + _ => Err(wasi::ERRNO_INVAL), + } + } + + pub fn get_file(&self, fd: Fd) -> Result<&File, Errno> { + self.get_file_with_error(fd, wasi::ERRNO_INVAL) + } + + pub fn get_dir(&self, fd: Fd) -> Result<&File, Errno> { + match self.get(fd)? { + Descriptor::Streams(Streams { + type_: + StreamType::File( + file @ File { + descriptor_type: filesystem::DescriptorType::Directory, + .. + }, + ), + .. + }) => Ok(file), + Descriptor::Streams(Streams { + type_: + StreamType::File( + file @ File { + descriptor_type: _, .. + }, + ), + .. + }) => Err(wasi::ERRNO_NOTDIR), + _ => Err(wasi::ERRNO_BADF), + } + } + + pub fn get_seekable_file(&self, fd: Fd) -> Result<&File, Errno> { + self.get_file_with_error(fd, wasi::ERRNO_SPIPE) + } + + pub fn get_seekable_stream(&self, fd: Fd) -> Result<&Streams, Errno> { + self.get_stream_with_error(fd, wasi::ERRNO_SPIPE) + } + + pub fn get_read_stream(&self, fd: Fd) -> Result { + match self.get(fd)? { + Descriptor::Streams(streams) => streams.get_read_stream(), + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF), + } + } + + pub fn get_write_stream(&self, fd: Fd) -> Result { + match self.get(fd)? { + Descriptor::Streams(streams) => streams.get_write_stream(), + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF), + } + } +} + +#[repr(C)] +pub struct Preopen { + pub descriptor: u32, + pub path: WasmStr, +} + +#[repr(C)] +pub struct PreopenList { + pub base: *const Preopen, + pub len: usize, +} diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs new file mode 100644 index 000000000000..998ce47a27f7 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -0,0 +1,2382 @@ +#![allow(unused_variables)] // TODO: remove this when more things are implemented + +use crate::bindings::{ + exit, filesystem, monotonic_clock, network, poll, random, streams, wall_clock, +}; +use core::cell::{Cell, RefCell, RefMut, UnsafeCell}; +use core::cmp::min; +use core::ffi::c_void; +use core::hint::black_box; +use core::mem::{self, align_of, forget, size_of, ManuallyDrop, MaybeUninit}; +use core::ops::{Deref, DerefMut}; +use core::ptr::{self, null_mut}; +use core::slice; +use poll::Pollable; +use wasi::*; + +#[cfg(all(feature = "command", feature = "reactor"))] +compile_error!("only one of the `command` and `reactor` features may be selected at a time"); + +#[macro_use] +mod macros; + +mod descriptors; +use crate::descriptors::{Descriptor, Descriptors, StreamType, Streams}; + +pub mod bindings { + #[cfg(feature = "command")] + wit_bindgen::generate!({ + world: "command", + std_feature, + raw_strings, + // The generated definition of command will pull in std, so we are defining it + // manually below instead + skip: ["run", "get-directories", "get-environment"], + }); + + #[cfg(feature = "reactor")] + wit_bindgen::generate!({ + world: "reactor", + std_feature, + raw_strings, + skip: ["get-directories", "get-environment"], + }); +} + +#[no_mangle] +#[cfg(feature = "command")] +pub unsafe extern "C" fn run() -> u32 { + #[link(wasm_import_module = "__main_module__")] + extern "C" { + fn _start(); + } + _start(); + 0 +} + +// The unwrap/expect methods in std pull panic when they fail, which pulls +// in unwinding machinery that we can't use in the adapter. Instead, use this +// extension trait to get postfixed upwrap on Option and Result. +trait TrappingUnwrap { + fn trapping_unwrap(self) -> T; +} + +impl TrappingUnwrap for Option { + fn trapping_unwrap(self) -> T { + match self { + Some(t) => t, + None => unreachable!(), + } + } +} + +impl TrappingUnwrap for Result { + fn trapping_unwrap(self) -> T { + match self { + Ok(t) => t, + Err(_) => unreachable!(), + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn cabi_import_realloc( + old_ptr: *mut u8, + old_size: usize, + align: usize, + new_size: usize, +) -> *mut u8 { + if !old_ptr.is_null() || old_size != 0 { + unreachable!(); + } + let mut ptr = null_mut::(); + State::with(|state| { + ptr = state.import_alloc.alloc(align, new_size); + Ok(()) + }); + ptr +} + +/// Bump-allocated memory arena. This is a singleton - the +/// memory will be sized according to `bump_arena_size()`. +pub struct BumpArena { + data: MaybeUninit<[u8; bump_arena_size()]>, + position: Cell, +} + +impl BumpArena { + fn new() -> Self { + BumpArena { + data: MaybeUninit::uninit(), + position: Cell::new(0), + } + } + fn alloc(&self, align: usize, size: usize) -> *mut u8 { + let start = self.data.as_ptr() as usize; + let next = start + self.position.get(); + let alloc = align_to(next, align); + let offset = alloc - start; + if offset + size > bump_arena_size() { + unreachable!("out of memory"); + } + self.position.set(offset + size); + alloc as *mut u8 + } +} +fn align_to(ptr: usize, align: usize) -> usize { + (ptr + (align - 1)) & !(align - 1) +} + +// Invariant: buffer not-null and arena is-some are never true at the same +// time. We did not use an enum to make this invalid behavior unrepresentable +// because we can't use RefCell to borrow() the variants of the enum - only +// Cell provides mutability without pulling in panic machinery - so it would +// make the accessors a lot more awkward to write. +pub struct ImportAlloc { + // When not-null, allocator should use this buffer/len pair at most once + // to satisfy allocations. + buffer: Cell<*mut u8>, + len: Cell, + // When not-empty, allocator should use this arena to satisfy allocations. + arena: Cell>, +} + +impl ImportAlloc { + fn new() -> Self { + ImportAlloc { + buffer: Cell::new(std::ptr::null_mut()), + len: Cell::new(0), + arena: Cell::new(None), + } + } + + /// Expect at most one import allocation during execution of the provided closure. + /// Use the provided buffer to satisfy that import allocation. The user is responsible + /// for making sure allocated imports are not used beyond the lifetime of the buffer. + fn with_buffer(&self, buffer: *mut u8, len: usize, f: impl FnOnce() -> T) -> T { + if self.arena.get().is_some() { + unreachable!("arena mode") + } + let prev = self.buffer.replace(buffer); + if !prev.is_null() { + unreachable!("overwrote another buffer") + } + self.len.set(len); + let r = f(); + self.buffer.set(std::ptr::null_mut()); + r + } + + /// Permit many import allocations during execution of the provided closure. + /// Use the provided BumpArena to satisfry those allocations. The user is responsible + /// for making sure allocated imports are not used beyond the lifetime of the arena. + fn with_arena(&self, arena: &BumpArena, f: impl FnOnce() -> T) -> T { + if !self.buffer.get().is_null() { + unreachable!("buffer mode") + } + let prev = self.arena.replace(Some(unsafe { + // Safety: Need to erase the lifetime to store in the arena cell. + std::mem::transmute::<&'_ BumpArena, &'static BumpArena>(arena) + })); + if prev.is_some() { + unreachable!("overwrote another arena") + } + let r = f(); + self.arena.set(None); + r + } + + /// To be used by cabi_import_realloc only! + fn alloc(&self, align: usize, size: usize) -> *mut u8 { + if let Some(arena) = self.arena.get() { + arena.alloc(align, size) + } else { + let buffer = self.buffer.get(); + if buffer.is_null() { + unreachable!("buffer not provided, or already used") + } + let buffer = buffer as usize; + let alloc = align_to(buffer, align); + if alloc.checked_add(size).trapping_unwrap() + > buffer.checked_add(self.len.get()).trapping_unwrap() + { + unreachable!("out of memory") + } + self.buffer.set(std::ptr::null_mut()); + alloc as *mut u8 + } + } +} + +/// This allocator is only used for the `run` entrypoint. +/// +/// The implementation here is a bump allocator into `State::long_lived_arena` which +/// traps when it runs out of data. This means that the total size of +/// arguments/env/etc coming into a component is bounded by the current 64k +/// (ish) limit. That's just an implementation limit though which can be lifted +/// by dynamically calling the main module's allocator as necessary for more data. +#[no_mangle] +pub unsafe extern "C" fn cabi_export_realloc( + old_ptr: *mut u8, + old_size: usize, + align: usize, + new_size: usize, +) -> *mut u8 { + if !old_ptr.is_null() || old_size != 0 { + unreachable!(); + } + let mut ret = null_mut::(); + State::with_mut(|state| { + ret = state.long_lived_arena.alloc(align, new_size); + Ok(()) + }); + ret +} + +/// Read command-line argument data. +/// The size of the array should match that returned by `args_sizes_get` +#[no_mangle] +pub unsafe extern "C" fn args_get(mut argv: *mut *mut u8, mut argv_buf: *mut u8) -> Errno { + State::with(|state| { + for arg in state.get_args() { + // Copy the argument into `argv_buf` which must be sized + // appropriately by the caller. + ptr::copy_nonoverlapping(arg.ptr, argv_buf, arg.len); + *argv_buf.add(arg.len) = 0; + + // Copy the argument pointer into the `argv` buf + *argv = argv_buf; + + // Update our pointers past what's written to prepare for the + // next argument. + argv = argv.add(1); + argv_buf = argv_buf.add(arg.len + 1); + } + Ok(()) + }) +} + +/// Return command-line argument data sizes. +#[no_mangle] +pub unsafe extern "C" fn args_sizes_get(argc: *mut Size, argv_buf_size: *mut Size) -> Errno { + State::with(|state| { + let args = state.get_args(); + *argc = args.len(); + // Add one to each length for the terminating nul byte added by + // the `args_get` function. + *argv_buf_size = args.iter().map(|s| s.len + 1).sum(); + Ok(()) + }) +} + +/// Read environment variable data. +/// The sizes of the buffers should match that returned by `environ_sizes_get`. +#[no_mangle] +pub unsafe extern "C" fn environ_get(environ: *mut *mut u8, environ_buf: *mut u8) -> Errno { + State::with(|state| { + let mut offsets = environ; + let mut buffer = environ_buf; + for var in state.get_environment() { + ptr::write(offsets, buffer); + offsets = offsets.add(1); + + ptr::copy_nonoverlapping(var.key.ptr, buffer, var.key.len); + buffer = buffer.add(var.key.len); + + ptr::write(buffer, b'='); + buffer = buffer.add(1); + + ptr::copy_nonoverlapping(var.value.ptr, buffer, var.value.len); + buffer = buffer.add(var.value.len); + + ptr::write(buffer, 0); + buffer = buffer.add(1); + } + + Ok(()) + }) +} + +/// Return environment variable data sizes. +#[no_mangle] +pub unsafe extern "C" fn environ_sizes_get( + environc: *mut Size, + environ_buf_size: *mut Size, +) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + State::with(|state| { + let vars = state.get_environment(); + *environc = vars.len(); + *environ_buf_size = { + let mut sum = 0; + for var in vars { + sum += var.key.len + var.value.len + 2; + } + sum + }; + + Ok(()) + }) + } else { + *environc = 0; + *environ_buf_size = 0; + ERRNO_SUCCESS + } +} + +/// Return the resolution of a clock. +/// Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, +/// return `errno::inval`. +/// Note: This is similar to `clock_getres` in POSIX. +#[no_mangle] +pub extern "C" fn clock_res_get(id: Clockid, resolution: &mut Timestamp) -> Errno { + State::with(|state| { + match id { + CLOCKID_MONOTONIC => { + let res = monotonic_clock::resolution(); + *resolution = res; + } + CLOCKID_REALTIME => { + let res = wall_clock::resolution(); + *resolution = Timestamp::from(res.seconds) + .checked_mul(1_000_000_000) + .and_then(|ns| ns.checked_add(res.nanoseconds.into())) + .ok_or(ERRNO_OVERFLOW)?; + } + _ => unreachable!(), + } + Ok(()) + }) +} + +/// Return the time value of a clock. +/// Note: This is similar to `clock_gettime` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn clock_time_get( + id: Clockid, + _precision: Timestamp, + time: &mut Timestamp, +) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + State::with(|state| { + match id { + CLOCKID_MONOTONIC => { + *time = monotonic_clock::now(); + } + CLOCKID_REALTIME => { + let res = wall_clock::now(); + *time = Timestamp::from(res.seconds) + .checked_mul(1_000_000_000) + .and_then(|ns| ns.checked_add(res.nanoseconds.into())) + .ok_or(ERRNO_OVERFLOW)?; + } + _ => unreachable!(), + } + Ok(()) + }) + } else { + *time = Timestamp::from(0u64); + ERRNO_SUCCESS + } +} + +/// Provide file advisory information on a file descriptor. +/// Note: This is similar to `posix_fadvise` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_advise( + fd: Fd, + offset: Filesize, + len: Filesize, + advice: Advice, +) -> Errno { + let advice = match advice { + ADVICE_NORMAL => filesystem::Advice::Normal, + ADVICE_SEQUENTIAL => filesystem::Advice::Sequential, + ADVICE_RANDOM => filesystem::Advice::Random, + ADVICE_WILLNEED => filesystem::Advice::WillNeed, + ADVICE_DONTNEED => filesystem::Advice::DontNeed, + ADVICE_NOREUSE => filesystem::Advice::NoReuse, + _ => return ERRNO_INVAL, + }; + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_seekable_file(fd)?; + filesystem::advise(file.fd, offset, len, advice)?; + Ok(()) + }) +} + +/// Force the allocation of space in a file. +/// Note: This is similar to `posix_fallocate` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_allocate(fd: Fd, offset: Filesize, len: Filesize) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + // For not-files, fail with BADF + let file = ds.get_file(fd)?; + // For all files, fail with NOTSUP, because this call does not exist in preview 2. + Err(wasi::ERRNO_NOTSUP) + }) +} + +/// Close a file descriptor. +/// Note: This is similar to `close` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_close(fd: Fd) -> Errno { + State::with_mut(|state| { + // If there's a dirent cache entry for this file descriptor then drop + // it since the descriptor is being closed and future calls to + // `fd_readdir` should return an error. + if fd == state.dirent_cache.for_fd.get() { + drop(state.dirent_cache.stream.replace(None)); + } + + let desc = state.descriptors_mut().close(fd)?; + Ok(()) + }) +} + +/// Synchronize the data of a file to disk. +/// Note: This is similar to `fdatasync` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_datasync(fd: Fd) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_file(fd)?; + filesystem::sync_data(file.fd)?; + Ok(()) + }) +} + +/// Get the attributes of a file descriptor. +/// Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_get(fd: Fd, stat: *mut Fdstat) -> Errno { + State::with(|state| match state.descriptors().get(fd)? { + Descriptor::Streams(Streams { + type_: StreamType::File(file), + .. + }) => { + let flags = filesystem::get_flags(file.fd)?; + let type_ = filesystem::get_type(file.fd)?; + + let fs_filetype = type_.into(); + + let mut fs_flags = 0; + let mut fs_rights_base = !0; + if !flags.contains(filesystem::DescriptorFlags::READ) { + fs_rights_base &= !RIGHTS_FD_READ; + } + if !flags.contains(filesystem::DescriptorFlags::WRITE) { + fs_rights_base &= !RIGHTS_FD_WRITE; + } + if flags.contains(filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + fs_flags |= FDFLAGS_DSYNC; + } + if flags.contains(filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + fs_flags |= FDFLAGS_RSYNC; + } + if flags.contains(filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + fs_flags |= FDFLAGS_SYNC; + } + if file.append { + fs_flags |= FDFLAGS_APPEND; + } + if !file.blocking { + fs_flags |= FDFLAGS_NONBLOCK; + } + let fs_rights_inheriting = fs_rights_base; + + stat.write(Fdstat { + fs_filetype, + fs_flags, + fs_rights_base, + fs_rights_inheriting, + }); + Ok(()) + } + Descriptor::Streams(Streams { + input, + output, + type_: StreamType::Socket(_), + }) + | Descriptor::Streams(Streams { + input, + output, + type_: StreamType::Stdio, + }) => { + let fs_filetype = FILETYPE_CHARACTER_DEVICE; + let fs_flags = 0; + let mut fs_rights_base = 0; + if input.get().is_some() { + fs_rights_base |= RIGHTS_FD_READ; + } + if output.get().is_some() { + fs_rights_base |= RIGHTS_FD_WRITE; + } + let fs_rights_inheriting = fs_rights_base; + stat.write(Fdstat { + fs_filetype, + fs_flags, + fs_rights_base, + fs_rights_inheriting, + }); + Ok(()) + } + Descriptor::Closed(_) => Err(ERRNO_BADF), + }) +} + +/// Adjust the flags associated with a file descriptor. +/// Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_set_flags(fd: Fd, flags: Fdflags) -> Errno { + // Only support changing the NONBLOCK or APPEND flags. + if flags & !(FDFLAGS_NONBLOCK | FDFLAGS_APPEND) != 0 { + return wasi::ERRNO_INVAL; + } + + State::with_mut(|state| { + let mut ds = state.descriptors_mut(); + let file = match ds.get_mut(fd)? { + Descriptor::Streams(Streams { + type_: StreamType::File(file), + .. + }) if !file.is_dir() => file, + _ => Err(wasi::ERRNO_BADF)?, + }; + file.append = flags & FDFLAGS_APPEND == FDFLAGS_APPEND; + file.blocking = !(flags & FDFLAGS_NONBLOCK == FDFLAGS_NONBLOCK); + Ok(()) + }) +} + +/// Adjust the rights associated with a file descriptor. +/// This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_set_rights( + fd: Fd, + fs_rights_base: Rights, + fs_rights_inheriting: Rights, +) -> Errno { + unreachable!() +} + +/// Return the attributes of an open file. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_get(fd: Fd, buf: *mut Filestat) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + match ds.get(fd)? { + Descriptor::Streams(Streams { + type_: StreamType::File(file), + .. + }) => { + let stat = filesystem::stat(file.fd)?; + let filetype = stat.type_.into(); + *buf = Filestat { + dev: stat.device, + ino: stat.inode, + filetype, + nlink: stat.link_count, + size: stat.size, + atim: datetime_to_timestamp(stat.data_access_timestamp), + mtim: datetime_to_timestamp(stat.data_modification_timestamp), + ctim: datetime_to_timestamp(stat.status_change_timestamp), + }; + Ok(()) + } + // Stdio is all zero fields, except for filetype character device + Descriptor::Streams(Streams { + type_: StreamType::Stdio, + .. + }) => { + *buf = Filestat { + dev: 0, + ino: 0, + filetype: FILETYPE_CHARACTER_DEVICE, + nlink: 0, + size: 0, + atim: 0, + mtim: 0, + ctim: 0, + }; + Ok(()) + } + _ => Err(wasi::ERRNO_BADF), + } + }) +} + +/// Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. +/// Note: This is similar to `ftruncate` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_set_size(fd: Fd, size: Filesize) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_file(fd)?; + filesystem::set_size(file.fd, size)?; + Ok(()) + }) +} + +fn systimespec(set: bool, ts: Timestamp, now: bool) -> Result { + if set && now { + Err(wasi::ERRNO_INVAL) + } else if set { + Ok(filesystem::NewTimestamp::Timestamp(filesystem::Datetime { + seconds: ts / 1_000_000_000, + nanoseconds: (ts % 1_000_000_000) as _, + })) + } else if now { + Ok(filesystem::NewTimestamp::Now) + } else { + Ok(filesystem::NewTimestamp::NoChange) + } +} + +/// Adjust the timestamps of an open file or directory. +/// Note: This is similar to `futimens` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_set_times( + fd: Fd, + atim: Timestamp, + mtim: Timestamp, + fst_flags: Fstflags, +) -> Errno { + State::with(|state| { + let atim = systimespec( + fst_flags & FSTFLAGS_ATIM == FSTFLAGS_ATIM, + atim, + fst_flags & FSTFLAGS_ATIM_NOW == FSTFLAGS_ATIM_NOW, + )?; + let mtim = systimespec( + fst_flags & FSTFLAGS_MTIM == FSTFLAGS_MTIM, + mtim, + fst_flags & FSTFLAGS_MTIM_NOW == FSTFLAGS_MTIM_NOW, + )?; + let ds = state.descriptors(); + let file = ds.get_file(fd)?; + filesystem::set_times(file.fd, atim, mtim)?; + Ok(()) + }) +} + +/// Read from a file descriptor, without using and updating the file descriptor's offset. +/// Note: This is similar to `preadv` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_pread( + fd: Fd, + mut iovs_ptr: *const Iovec, + mut iovs_len: usize, + offset: Filesize, + nread: *mut Size, +) -> Errno { + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nread = 0; + return ERRNO_SUCCESS; + } + + State::with(|state| { + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + + let ds = state.descriptors(); + let file = ds.get_file(fd)?; + let (data, end) = state + .import_alloc + .with_buffer(ptr, len, || filesystem::read(file.fd, len as u64, offset))?; + assert_eq!(data.as_ptr(), ptr); + assert!(data.len() <= len); + + let len = data.len(); + forget(data); + if !end && len == 0 { + Err(ERRNO_INTR) + } else { + *nread = len; + Ok(()) + } + }) +} + +/// Return a description of the given preopened file descriptor. +#[no_mangle] +pub unsafe extern "C" fn fd_prestat_get(fd: Fd, buf: *mut Prestat) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + State::with(|state| { + if let Some(preopen) = state.descriptors().get_preopen(fd) { + buf.write(Prestat { + tag: 0, + u: PrestatU { + dir: PrestatDir { + pr_name_len: preopen.path.len, + }, + }, + }); + + Ok(()) + } else { + Err(ERRNO_BADF) + } + }) + } else { + ERRNO_BADF + } +} + +/// Return a description of the given preopened file descriptor. +#[no_mangle] +pub unsafe extern "C" fn fd_prestat_dir_name(fd: Fd, path: *mut u8, path_len: Size) -> Errno { + State::with(|state| { + if let Some(preopen) = state.descriptors().get_preopen(fd) { + if preopen.path.len < path_len as usize { + Err(ERRNO_NAMETOOLONG) + } else { + ptr::copy_nonoverlapping(preopen.path.ptr, path, preopen.path.len); + Ok(()) + } + } else { + Err(ERRNO_NOTDIR) + } + }) +} + +/// Write to a file descriptor, without using and updating the file descriptor's offset. +/// Note: This is similar to `pwritev` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_pwrite( + fd: Fd, + mut iovs_ptr: *const Ciovec, + mut iovs_len: usize, + offset: Filesize, + nwritten: *mut Size, +) -> Errno { + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nwritten = 0; + return ERRNO_SUCCESS; + } + + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_seekable_file(fd)?; + let bytes = filesystem::write(file.fd, slice::from_raw_parts(ptr, len), offset)?; + *nwritten = bytes as usize; + Ok(()) + }) +} + +/// Read from a file descriptor. +/// Note: This is similar to `readv` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_read( + fd: Fd, + mut iovs_ptr: *const Iovec, + mut iovs_len: usize, + nread: *mut Size, +) -> Errno { + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nread = 0; + return ERRNO_SUCCESS; + } + + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + + State::with(|state| { + match state.descriptors().get(fd)? { + Descriptor::Streams(streams) => { + let blocking = if let StreamType::File(file) = &streams.type_ { + file.blocking + } else { + false + }; + + let read_len = u64::try_from(len).trapping_unwrap(); + let wasi_stream = streams.get_read_stream()?; + let (data, end) = state + .import_alloc + .with_buffer(ptr, len, || { + if blocking { + streams::blocking_read(wasi_stream, read_len) + } else { + streams::read(wasi_stream, read_len) + } + }) + .map_err(|_| ERRNO_IO)?; + + assert_eq!(data.as_ptr(), ptr); + assert!(data.len() <= len); + + // If this is a file, keep the current-position pointer up to date. + if let StreamType::File(file) = &streams.type_ { + file.position + .set(file.position.get() + data.len() as filesystem::Filesize); + } + + let len = data.len(); + forget(data); + if !end && len == 0 { + Err(ERRNO_INTR) + } else { + *nread = len; + Ok(()) + } + } + Descriptor::Closed(_) => Err(ERRNO_BADF), + } + }) +} + +/// Read directory entries from a directory. +/// When successful, the contents of the output buffer consist of a sequence of +/// directory entries. Each directory entry consists of a `dirent` object, +/// followed by `dirent::d_namlen` bytes holding the name of the directory +/// entry. +/// This function fills the output buffer as much as possible, potentially +/// truncating the last directory entry. This allows the caller to grow its +/// read buffer size in case it's too small to fit a single large directory +/// entry, or skip the oversized directory entry. +#[no_mangle] +pub unsafe extern "C" fn fd_readdir( + fd: Fd, + buf: *mut u8, + buf_len: Size, + cookie: Dircookie, + bufused: *mut Size, +) -> Errno { + let mut buf = slice::from_raw_parts_mut(buf, buf_len); + return State::with(|state| { + // First determine if there's an entry in the dirent cache to use. This + // is done to optimize the use case where a large directory is being + // used with a fixed-sized buffer to avoid re-invoking the `readdir` + // function and continuing to use the same iterator. + // + // This is a bit tricky since the requested state in this function call + // must match the prior state of the dirent stream, if any, so that's + // all validated here as well. + // + // Note that for the duration of this function the `cookie` specifier is + // the `n`th iteration of the `readdir` stream return value. + let prev_stream = state.dirent_cache.stream.replace(None); + let stream = + if state.dirent_cache.for_fd.get() == fd && state.dirent_cache.cookie.get() == cookie { + prev_stream + } else { + None + }; + + // Compute the inode of `.` so that the iterator can produce an entry + // for it. + let ds = state.descriptors(); + let dir = ds.get_dir(fd)?; + let stat = filesystem::stat(dir.fd)?; + let dot_inode = stat.inode; + + let mut iter; + match stream { + // All our checks passed and a dirent cache was available with a + // prior stream. Construct an iterator which will yield its first + // entry from cache and is additionally resuming at the `cookie` + // specified. + Some(stream) => { + iter = DirectoryEntryIterator { + stream, + state, + cookie, + use_cache: true, + dot_inode, + } + } + + // Either a dirent stream wasn't previously available, a different + // cookie was requested, or a brand new directory is now being read. + // In these situations fall back to resuming reading the directory + // from scratch, and the `cookie` value indicates how many items + // need skipping. + None => { + iter = DirectoryEntryIterator { + state, + cookie: wasi::DIRCOOKIE_START, + use_cache: false, + stream: DirectoryEntryStream(filesystem::read_directory(dir.fd)?), + dot_inode, + }; + + // Skip to the entry that is requested by the `cookie` + // parameter. + for _ in wasi::DIRCOOKIE_START..cookie { + match iter.next() { + Some(Ok(_)) => {} + Some(Err(e)) => return Err(e), + None => return Ok(()), + } + } + } + }; + + while buf.len() > 0 { + let (dirent, name) = match iter.next() { + Some(Ok(pair)) => pair, + Some(Err(e)) => return Err(e), + None => break, + }; + + // Copy a `dirent` describing this entry into the destination `buf`, + // truncating it if it doesn't fit entirely. + let bytes = slice::from_raw_parts( + (&dirent as *const wasi::Dirent).cast::(), + size_of::(), + ); + let dirent_bytes_to_copy = buf.len().min(bytes.len()); + buf[..dirent_bytes_to_copy].copy_from_slice(&bytes[..dirent_bytes_to_copy]); + buf = &mut buf[dirent_bytes_to_copy..]; + + // Copy the name bytes into the output `buf`, truncating it if it + // doesn't fit. + // + // Note that this might be a 0-byte copy if the `dirent` was + // truncated or fit entirely into the destination. + let name_bytes_to_copy = buf.len().min(name.len()); + ptr::copy_nonoverlapping(name.as_ptr().cast(), buf.as_mut_ptr(), name_bytes_to_copy); + + buf = &mut buf[name_bytes_to_copy..]; + + // If the buffer is empty then that means the value may be + // truncated, so save the state of the iterator in our dirent cache + // and return. + // + // Note that `cookie - 1` is stored here since `iter.cookie` stores + // the address of the next item, and we're rewinding one item since + // the current item is truncated and will want to resume from that + // in the future. + // + // Additionally note that this caching step is skipped if the name + // to store doesn't actually fit in the dirent cache's path storage. + // In that case there's not much we can do and let the next call to + // `fd_readdir` start from scratch. + if buf.len() == 0 && name.len() <= DIRENT_CACHE { + let DirectoryEntryIterator { stream, cookie, .. } = iter; + state.dirent_cache.stream.set(Some(stream)); + state.dirent_cache.for_fd.set(fd); + state.dirent_cache.cookie.set(cookie - 1); + state.dirent_cache.cached_dirent.set(dirent); + ptr::copy( + name.as_ptr().cast::(), + (*state.dirent_cache.path_data.get()).as_mut_ptr() as *mut u8, + name.len(), + ); + break; + } + } + + *bufused = buf_len - buf.len(); + Ok(()) + }); + + struct DirectoryEntryIterator<'a> { + state: &'a State, + use_cache: bool, + cookie: Dircookie, + stream: DirectoryEntryStream, + dot_inode: wasi::Inode, + } + + impl<'a> Iterator for DirectoryEntryIterator<'a> { + // Note the usage of `UnsafeCell` here to indicate that the data can + // alias the storage within `state`. + type Item = Result<(wasi::Dirent, &'a [UnsafeCell]), Errno>; + + fn next(&mut self) -> Option { + let current_cookie = self.cookie; + + self.cookie += 1; + + // Preview1 programs expect to see `.` and `..` in the traversal, but + // Preview2 excludes them, so re-add them. + match current_cookie { + 0 => { + let dirent = wasi::Dirent { + d_next: self.cookie, + d_ino: self.dot_inode, + d_type: wasi::FILETYPE_DIRECTORY, + d_namlen: 1, + }; + return Some(Ok((dirent, &self.state.dotdot[..1]))); + } + 1 => { + let dirent = wasi::Dirent { + d_next: self.cookie, + d_ino: 0, + d_type: wasi::FILETYPE_DIRECTORY, + d_namlen: 2, + }; + return Some(Ok((dirent, &self.state.dotdot[..]))); + } + _ => {} + } + + if self.use_cache { + self.use_cache = false; + return Some(unsafe { + let dirent = self.state.dirent_cache.cached_dirent.as_ptr().read(); + let ptr = (*(*self.state.dirent_cache.path_data.get()).as_ptr()) + .as_ptr() + .cast(); + let buffer = slice::from_raw_parts(ptr, dirent.d_namlen as usize); + Ok((dirent, buffer)) + }); + } + let entry = self.state.import_alloc.with_buffer( + self.state.path_buf.get().cast(), + PATH_MAX, + || filesystem::read_directory_entry(self.stream.0), + ); + let entry = match entry { + Ok(Some(entry)) => entry, + Ok(None) => return None, + Err(e) => return Some(Err(e.into())), + }; + + let filesystem::DirectoryEntry { inode, type_, name } = entry; + let name = ManuallyDrop::new(name); + let dirent = wasi::Dirent { + d_next: self.cookie, + d_ino: inode.unwrap_or(0), + d_namlen: u32::try_from(name.len()).trapping_unwrap(), + d_type: type_.into(), + }; + // Extend the lifetime of `name` to the `self.state` lifetime for + // this iterator since the data for the name lives within state. + let name = unsafe { + assert_eq!(name.as_ptr(), self.state.path_buf.get().cast()); + slice::from_raw_parts(name.as_ptr().cast(), name.len()) + }; + Some(Ok((dirent, name))) + } + } +} + +/// Atomically replace a file descriptor by renumbering another file descriptor. +/// Due to the strong focus on thread safety, this environment does not provide +/// a mechanism to duplicate or renumber a file descriptor to an arbitrary +/// number, like `dup2()`. This would be prone to race conditions, as an actual +/// file descriptor with the same number could be allocated by a different +/// thread at the same time. +/// This function provides a way to atomically renumber file descriptors, which +/// would disappear if `dup2()` were to be removed entirely. +#[no_mangle] +pub unsafe extern "C" fn fd_renumber(fd: Fd, to: Fd) -> Errno { + State::with_mut(|state| state.descriptors_mut().renumber(fd, to)) +} + +/// Move the offset of a file descriptor. +/// Note: This is similar to `lseek` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_seek( + fd: Fd, + offset: Filedelta, + whence: Whence, + newoffset: *mut Filesize, +) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + let stream = ds.get_seekable_stream(fd)?; + + // Seeking only works on files. + if let StreamType::File(file) = &stream.type_ { + if let filesystem::DescriptorType::Directory = file.descriptor_type { + // This isn't really the "right" errno, but it is consistient with wasmtime's + // preview 1 tests. + return Err(ERRNO_BADF); + } + let from = match whence { + WHENCE_SET if offset >= 0 => offset, + WHENCE_CUR => match (file.position.get() as i64).checked_add(offset) { + Some(pos) if pos >= 0 => pos, + _ => return Err(ERRNO_INVAL), + }, + WHENCE_END => match (filesystem::stat(file.fd)?.size as i64).checked_add(offset) { + Some(pos) if pos >= 0 => pos, + _ => return Err(ERRNO_INVAL), + }, + _ => return Err(ERRNO_INVAL), + }; + stream.input.set(None); + stream.output.set(None); + file.position.set(from as filesystem::Filesize); + *newoffset = from as filesystem::Filesize; + Ok(()) + } else { + Err(ERRNO_SPIPE) + } + }) +} + +/// Synchronize the data and metadata of a file to disk. +/// Note: This is similar to `fsync` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_sync(fd: Fd) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_file(fd)?; + filesystem::sync(file.fd)?; + Ok(()) + }) +} + +/// Return the current offset of a file descriptor. +/// Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_tell(fd: Fd, offset: *mut Filesize) -> Errno { + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_seekable_file(fd)?; + *offset = file.position.get() as Filesize; + Ok(()) + }) +} + +/// Write to a file descriptor. +/// Note: This is similar to `writev` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_write( + fd: Fd, + mut iovs_ptr: *const Ciovec, + mut iovs_len: usize, + nwritten: *mut Size, +) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nwritten = 0; + return ERRNO_SUCCESS; + } + + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + let bytes = slice::from_raw_parts(ptr, len); + + State::with(|state| { + let ds = state.descriptors(); + match ds.get(fd)? { + Descriptor::Streams(streams) => { + let wasi_stream = streams.get_write_stream()?; + + let bytes = if let StreamType::File(file) = &streams.type_ { + if file.blocking { + streams::blocking_write(wasi_stream, bytes) + } else { + streams::write(wasi_stream, bytes) + } + } else { + streams::write(wasi_stream, bytes) + } + .map_err(|_| ERRNO_IO)?; + + // If this is a file, keep the current-position pointer up to date. + if let StreamType::File(file) = &streams.type_ { + // But don't update if we're in append mode. Strictly speaking, + // we should set the position to the new end of the file, but + // we don't have an API to do that atomically. + if !file.append { + file.position + .set(file.position.get() + filesystem::Filesize::from(bytes)); + } + } + + *nwritten = bytes as usize; + Ok(()) + } + Descriptor::Closed(_) => Err(ERRNO_BADF), + } + }) + } else { + *nwritten = 0; + ERRNO_IO + } +} + +/// Create a directory. +/// Note: This is similar to `mkdirat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_create_directory( + fd: Fd, + path_ptr: *const u8, + path_len: usize, +) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + filesystem::create_directory_at(file.fd, path)?; + Ok(()) + }) +} + +/// Return the attributes of a file or directory. +/// Note: This is similar to `stat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_filestat_get( + fd: Fd, + flags: Lookupflags, + path_ptr: *const u8, + path_len: usize, + buf: *mut Filestat, +) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + let at_flags = at_flags_from_lookupflags(flags); + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + let stat = filesystem::stat_at(file.fd, at_flags, path)?; + let filetype = stat.type_.into(); + *buf = Filestat { + dev: stat.device, + ino: stat.inode, + filetype, + nlink: stat.link_count, + size: stat.size, + atim: datetime_to_timestamp(stat.data_access_timestamp), + mtim: datetime_to_timestamp(stat.data_modification_timestamp), + ctim: datetime_to_timestamp(stat.status_change_timestamp), + }; + Ok(()) + }) +} + +/// Adjust the timestamps of a file or directory. +/// Note: This is similar to `utimensat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_filestat_set_times( + fd: Fd, + flags: Lookupflags, + path_ptr: *const u8, + path_len: usize, + atim: Timestamp, + mtim: Timestamp, + fst_flags: Fstflags, +) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + let at_flags = at_flags_from_lookupflags(flags); + + State::with(|state| { + let atim = systimespec( + fst_flags & FSTFLAGS_ATIM == FSTFLAGS_ATIM, + atim, + fst_flags & FSTFLAGS_ATIM_NOW == FSTFLAGS_ATIM_NOW, + )?; + let mtim = systimespec( + fst_flags & FSTFLAGS_MTIM == FSTFLAGS_MTIM, + mtim, + fst_flags & FSTFLAGS_MTIM_NOW == FSTFLAGS_MTIM_NOW, + )?; + + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + filesystem::set_times_at(file.fd, at_flags, path, atim, mtim)?; + Ok(()) + }) +} + +/// Create a hard link. +/// Note: This is similar to `linkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_link( + old_fd: Fd, + old_flags: Lookupflags, + old_path_ptr: *const u8, + old_path_len: usize, + new_fd: Fd, + new_path_ptr: *const u8, + new_path_len: usize, +) -> Errno { + let old_path = slice::from_raw_parts(old_path_ptr, old_path_len); + let new_path = slice::from_raw_parts(new_path_ptr, new_path_len); + let at_flags = at_flags_from_lookupflags(old_flags); + + State::with(|state| { + let old = state.descriptors().get_dir(old_fd)?.fd; + let new = state.descriptors().get_dir(new_fd)?.fd; + filesystem::link_at(old, at_flags, old_path, new, new_path)?; + Ok(()) + }) +} + +/// Open a file or directory. +/// The returned file descriptor is not guaranteed to be the lowest-numbered +/// file descriptor not currently open; it is randomized to prevent +/// applications from depending on making assumptions about indexes, since this +/// is error-prone in multi-threaded contexts. The returned file descriptor is +/// guaranteed to be less than 2**31. +/// Note: This is similar to `openat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_open( + fd: Fd, + dirflags: Lookupflags, + path_ptr: *const u8, + path_len: usize, + oflags: Oflags, + fs_rights_base: Rights, + fs_rights_inheriting: Rights, + fdflags: Fdflags, + opened_fd: *mut Fd, +) -> Errno { + drop(fs_rights_inheriting); + + let path = slice::from_raw_parts(path_ptr, path_len); + let at_flags = at_flags_from_lookupflags(dirflags); + let o_flags = o_flags_from_oflags(oflags); + let flags = descriptor_flags_from_flags(fs_rights_base, fdflags); + let mode = filesystem::Modes::READABLE | filesystem::Modes::WRITEABLE; + let append = fdflags & wasi::FDFLAGS_APPEND == wasi::FDFLAGS_APPEND; + + State::with_mut(|state| { + let mut ds = state.descriptors_mut(); + let file = ds.get_dir(fd)?; + let result = filesystem::open_at(file.fd, at_flags, path, o_flags, flags, mode)?; + let descriptor_type = filesystem::get_type(result)?; + let desc = Descriptor::Streams(Streams { + input: Cell::new(None), + output: Cell::new(None), + type_: StreamType::File(File { + fd: result, + descriptor_type, + position: Cell::new(0), + append, + blocking: (fdflags & wasi::FDFLAGS_NONBLOCK) == 0, + }), + }); + + let fd = ds.open(desc)?; + *opened_fd = fd; + Ok(()) + }) +} + +/// Read the contents of a symbolic link. +/// Note: This is similar to `readlinkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_readlink( + fd: Fd, + path_ptr: *const u8, + path_len: usize, + buf: *mut u8, + buf_len: Size, + bufused: *mut Size, +) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + + State::with(|state| { + // If the user gave us a buffer shorter than `PATH_MAX`, it may not be + // long enough to accept the actual path. `cabi_realloc` can't fail, + // so instead we handle this case specially. + let use_state_buf = buf_len < PATH_MAX; + + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + let path = if use_state_buf { + state + .import_alloc + .with_buffer(state.path_buf.get().cast(), PATH_MAX, || { + filesystem::readlink_at(file.fd, path) + })? + } else { + state + .import_alloc + .with_buffer(buf, buf_len, || filesystem::readlink_at(file.fd, path))? + }; + + if use_state_buf { + // Preview1 follows POSIX in truncating the returned path if it + // doesn't fit. + let len = min(path.len(), buf_len); + ptr::copy_nonoverlapping(path.as_ptr().cast(), buf, len); + *bufused = len; + } else { + *bufused = path.len(); + } + + // The returned string's memory was allocated in `buf`, so don't separately + // free it. + forget(path); + + Ok(()) + }) +} + +/// Remove a directory. +/// Return `errno::notempty` if the directory is not empty. +/// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_remove_directory( + fd: Fd, + path_ptr: *const u8, + path_len: usize, +) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + filesystem::remove_directory_at(file.fd, path)?; + Ok(()) + }) +} + +/// Rename a file or directory. +/// Note: This is similar to `renameat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_rename( + old_fd: Fd, + old_path_ptr: *const u8, + old_path_len: usize, + new_fd: Fd, + new_path_ptr: *const u8, + new_path_len: usize, +) -> Errno { + let old_path = slice::from_raw_parts(old_path_ptr, old_path_len); + let new_path = slice::from_raw_parts(new_path_ptr, new_path_len); + + State::with(|state| { + let ds = state.descriptors(); + let old = ds.get_dir(old_fd)?.fd; + let new = ds.get_dir(new_fd)?.fd; + filesystem::rename_at(old, old_path, new, new_path)?; + Ok(()) + }) +} + +/// Create a symbolic link. +/// Note: This is similar to `symlinkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_symlink( + old_path_ptr: *const u8, + old_path_len: usize, + fd: Fd, + new_path_ptr: *const u8, + new_path_len: usize, +) -> Errno { + let old_path = slice::from_raw_parts(old_path_ptr, old_path_len); + let new_path = slice::from_raw_parts(new_path_ptr, new_path_len); + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + filesystem::symlink_at(file.fd, old_path, new_path)?; + Ok(()) + }) +} + +/// Unlink a file. +/// Return `errno::isdir` if the path refers to a directory. +/// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_unlink_file(fd: Fd, path_ptr: *const u8, path_len: usize) -> Errno { + let path = slice::from_raw_parts(path_ptr, path_len); + + State::with(|state| { + let ds = state.descriptors(); + let file = ds.get_dir(fd)?; + filesystem::unlink_file_at(file.fd, path)?; + Ok(()) + }) +} + +struct Pollables { + pointer: *mut Pollable, + index: usize, + length: usize, +} + +impl Pollables { + unsafe fn push(&mut self, pollable: Pollable) { + assert!(self.index < self.length); + *self.pointer.add(self.index) = pollable; + self.index += 1; + } +} + +impl Drop for Pollables { + fn drop(&mut self) { + for i in 0..self.index { + poll::drop_pollable(unsafe { *self.pointer.add(i) }) + } + } +} + +impl From for Errno { + fn from(error: network::Error) -> Errno { + match error { + network::Error::Unknown => unreachable!(), // TODO + network::Error::Again => ERRNO_AGAIN, + /* TODO + // Use a black box to prevent the optimizer from generating a + // lookup table, which would require a static initializer. + ConnectionAborted => black_box(ERRNO_CONNABORTED), + ConnectionRefused => ERRNO_CONNREFUSED, + ConnectionReset => ERRNO_CONNRESET, + HostUnreachable => ERRNO_HOSTUNREACH, + NetworkDown => ERRNO_NETDOWN, + NetworkUnreachable => ERRNO_NETUNREACH, + Timedout => ERRNO_TIMEDOUT, + _ => unreachable!(), + */ + } + } +} + +/// Concurrently poll for the occurrence of a set of events. +#[no_mangle] +pub unsafe extern "C" fn poll_oneoff( + r#in: *const Subscription, + out: *mut Event, + nsubscriptions: Size, + nevents: *mut Size, +) -> Errno { + *nevents = 0; + + let subscriptions = slice::from_raw_parts(r#in, nsubscriptions); + + // We're going to split the `nevents` buffer into two non-overlapping + // buffers: one to store the pollable handles, and the other to store + // the bool results. + // + // First, we assert that this is possible: + assert!(align_of::() >= align_of::()); + assert!(align_of::() >= align_of::()); + assert!( + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + >= nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + .checked_add( + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + ) + .trapping_unwrap() + ); + // Store the pollable handles at the beginning, and the bool results at the + // end, so that we don't clobber the bool results when writting the events. + let pollables = out as *mut c_void as *mut Pollable; + let results = out.add(nsubscriptions).cast::().sub(nsubscriptions); + + // Indefinite sleeping is not supported in preview1. + if nsubscriptions == 0 { + return ERRNO_INVAL; + } + + State::with(|state| { + const EVENTTYPE_CLOCK: u8 = wasi::EVENTTYPE_CLOCK.raw(); + const EVENTTYPE_FD_READ: u8 = wasi::EVENTTYPE_FD_READ.raw(); + const EVENTTYPE_FD_WRITE: u8 = wasi::EVENTTYPE_FD_WRITE.raw(); + + let mut pollables = Pollables { + pointer: pollables, + index: 0, + length: nsubscriptions, + }; + + for subscription in subscriptions { + pollables.push(match subscription.u.tag { + EVENTTYPE_CLOCK => { + let clock = &subscription.u.u.clock; + let absolute = (clock.flags & SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) + == SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME; + match clock.id { + CLOCKID_REALTIME => { + let timeout = if absolute { + // Convert `clock.timeout` to `Datetime`. + let mut datetime = wall_clock::Datetime { + seconds: clock.timeout / 1_000_000_000, + nanoseconds: (clock.timeout % 1_000_000_000) as _, + }; + + // Subtract `now`. + let now = wall_clock::now(); + datetime.seconds -= now.seconds; + if datetime.nanoseconds < now.nanoseconds { + datetime.seconds -= 1; + datetime.nanoseconds += 1_000_000_000; + } + datetime.nanoseconds -= now.nanoseconds; + + // Convert to nanoseconds. + let nanos = datetime + .seconds + .checked_mul(1_000_000_000) + .ok_or(ERRNO_OVERFLOW)?; + nanos + .checked_add(datetime.nanoseconds.into()) + .ok_or(ERRNO_OVERFLOW)? + } else { + clock.timeout + }; + + monotonic_clock::subscribe(timeout, false) + } + + CLOCKID_MONOTONIC => monotonic_clock::subscribe(clock.timeout, absolute), + + _ => return Err(ERRNO_INVAL), + } + } + + EVENTTYPE_FD_READ => { + let stream = state + .descriptors() + .get_read_stream(subscription.u.u.fd_read.file_descriptor)?; + streams::subscribe_to_input_stream(stream) + } + + EVENTTYPE_FD_WRITE => { + let stream = state + .descriptors() + .get_write_stream(subscription.u.u.fd_write.file_descriptor)?; + streams::subscribe_to_output_stream(stream) + } + + _ => return Err(ERRNO_INVAL), + }); + } + + let vec = state.import_alloc.with_buffer( + results, + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap(), + || poll::poll_oneoff(slice::from_raw_parts(pollables.pointer, pollables.length)), + ); + + assert_eq!(vec.len(), nsubscriptions); + assert_eq!(vec.as_ptr(), results); + forget(vec); + + drop(pollables); + + let ready = subscriptions + .iter() + .enumerate() + .filter_map(|(i, s)| (*results.add(i) != 0).then_some(s)); + + let mut count = 0; + + for subscription in ready { + let error; + let type_; + let nbytes; + let flags; + + match subscription.u.tag { + EVENTTYPE_CLOCK => { + error = ERRNO_SUCCESS; + type_ = wasi::EVENTTYPE_CLOCK; + nbytes = 0; + flags = 0; + } + + EVENTTYPE_FD_READ => { + type_ = wasi::EVENTTYPE_FD_READ; + let ds = state.descriptors(); + let desc = ds + .get(subscription.u.u.fd_read.file_descriptor) + .trapping_unwrap(); + match desc { + Descriptor::Streams(streams) => match &streams.type_ { + StreamType::File(file) => match filesystem::stat(file.fd) { + Ok(stat) => { + error = ERRNO_SUCCESS; + nbytes = stat.size.saturating_sub(file.position.get()); + flags = if nbytes == 0 { + EVENTRWFLAGS_FD_READWRITE_HANGUP + } else { + 0 + }; + } + Err(e) => { + error = e.into(); + nbytes = 1; + flags = 0; + } + }, + StreamType::Socket(connection) => { + unreachable!() // TODO + /* + match tcp::bytes_readable(*connection) { + Ok(result) => { + error = ERRNO_SUCCESS; + nbytes = result.0; + flags = if result.1 { + EVENTRWFLAGS_FD_READWRITE_HANGUP + } else { + 0 + }; + } + Err(e) => { + error = e.into(); + nbytes = 0; + flags = 0; + } + } + */ + } + StreamType::Stdio => { + error = ERRNO_SUCCESS; + nbytes = 1; + flags = 0; + } + }, + _ => unreachable!(), + } + } + EVENTTYPE_FD_WRITE => { + type_ = wasi::EVENTTYPE_FD_WRITE; + let ds = state.descriptors(); + let desc = ds + .get(subscription.u.u.fd_write.file_descriptor) + .trapping_unwrap(); + match desc { + Descriptor::Streams(streams) => match streams.type_ { + StreamType::File(_) | StreamType::Stdio => { + error = ERRNO_SUCCESS; + nbytes = 1; + flags = 0; + } + StreamType::Socket(connection) => { + unreachable!() // TODO + /* + match tcp::bytes_writable(connection) { + Ok(result) => { + error = ERRNO_SUCCESS; + nbytes = result.0; + flags = if result.1 { + EVENTRWFLAGS_FD_READWRITE_HANGUP + } else { + 0 + }; + } + Err(e) => { + error = e.into(); + nbytes = 0; + flags = 0; + } + } + */ + } + }, + _ => unreachable!(), + } + } + + _ => unreachable!(), + } + + *out.add(count) = Event { + userdata: subscription.userdata, + error, + type_, + fd_readwrite: EventFdReadwrite { nbytes, flags }, + }; + + count += 1; + } + + *nevents = count; + + Ok(()) + }) +} + +/// Terminate the process normally. An exit code of 0 indicates successful +/// termination of the program. The meanings of other values is dependent on +/// the environment. +#[no_mangle] +pub unsafe extern "C" fn proc_exit(rval: Exitcode) -> ! { + let status = if rval == 0 { Ok(()) } else { Err(()) }; + exit::exit(status); // does not return + unreachable!("host exit implementation didn't exit!") // actually unreachable +} + +/// Send a signal to the process of the calling thread. +/// Note: This is similar to `raise` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn proc_raise(sig: Signal) -> Errno { + unreachable!() +} + +/// Temporarily yield execution of the calling thread. +/// Note: This is similar to `sched_yield` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sched_yield() -> Errno { + // TODO: This is not yet covered in Preview2. + + ERRNO_SUCCESS +} + +/// Write high-quality random data into a buffer. +/// This function blocks when the implementation is unable to immediately +/// provide sufficient high-quality random data. +/// This function may execute slowly, so when large mounts of random data are +/// required, it's advisable to use this function to seed a pseudo-random +/// number generator, rather than to provide the random data directly. +#[no_mangle] +pub unsafe extern "C" fn random_get(buf: *mut u8, buf_len: Size) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + State::with(|state| { + assert_eq!(buf_len as u32 as Size, buf_len); + let result = state + .import_alloc + .with_buffer(buf, buf_len, || random::get_random_bytes(buf_len as u64)); + assert_eq!(result.as_ptr(), buf); + + // The returned buffer's memory was allocated in `buf`, so don't separately + // free it. + forget(result); + + Ok(()) + }) + } else { + ERRNO_SUCCESS + } +} + +/// Accept a new incoming connection. +/// Note: This is similar to `accept` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sock_accept(fd: Fd, flags: Fdflags, connection: *mut Fd) -> Errno { + unreachable!() +} + +/// Receive a message from a socket. +/// Note: This is similar to `recv` in POSIX, though it also supports reading +/// the data into multiple buffers in the manner of `readv`. +#[no_mangle] +pub unsafe extern "C" fn sock_recv( + fd: Fd, + ri_data_ptr: *const Iovec, + ri_data_len: usize, + ri_flags: Riflags, + ro_datalen: *mut Size, + ro_flags: *mut Roflags, +) -> Errno { + unreachable!() +} + +/// Send a message on a socket. +/// Note: This is similar to `send` in POSIX, though it also supports writing +/// the data from multiple buffers in the manner of `writev`. +#[no_mangle] +pub unsafe extern "C" fn sock_send( + fd: Fd, + si_data_ptr: *const Ciovec, + si_data_len: usize, + si_flags: Siflags, + so_datalen: *mut Size, +) -> Errno { + unreachable!() +} + +/// Shut down socket send and receive channels. +/// Note: This is similar to `shutdown` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sock_shutdown(fd: Fd, how: Sdflags) -> Errno { + unreachable!() +} + +fn datetime_to_timestamp(datetime: filesystem::Datetime) -> Timestamp { + u64::from(datetime.nanoseconds).saturating_add(datetime.seconds.saturating_mul(1_000_000_000)) +} + +fn at_flags_from_lookupflags(flags: Lookupflags) -> filesystem::PathFlags { + if flags & LOOKUPFLAGS_SYMLINK_FOLLOW == LOOKUPFLAGS_SYMLINK_FOLLOW { + filesystem::PathFlags::SYMLINK_FOLLOW + } else { + filesystem::PathFlags::empty() + } +} + +fn o_flags_from_oflags(flags: Oflags) -> filesystem::OpenFlags { + let mut o_flags = filesystem::OpenFlags::empty(); + if flags & OFLAGS_CREAT == OFLAGS_CREAT { + o_flags |= filesystem::OpenFlags::CREATE; + } + if flags & OFLAGS_DIRECTORY == OFLAGS_DIRECTORY { + o_flags |= filesystem::OpenFlags::DIRECTORY; + } + if flags & OFLAGS_EXCL == OFLAGS_EXCL { + o_flags |= filesystem::OpenFlags::EXCLUSIVE; + } + if flags & OFLAGS_TRUNC == OFLAGS_TRUNC { + o_flags |= filesystem::OpenFlags::TRUNCATE; + } + o_flags +} + +fn descriptor_flags_from_flags(rights: Rights, fdflags: Fdflags) -> filesystem::DescriptorFlags { + let mut flags = filesystem::DescriptorFlags::empty(); + if rights & wasi::RIGHTS_FD_READ == wasi::RIGHTS_FD_READ { + flags |= filesystem::DescriptorFlags::READ; + } + if rights & wasi::RIGHTS_FD_WRITE == wasi::RIGHTS_FD_WRITE { + flags |= filesystem::DescriptorFlags::WRITE; + } + if fdflags & wasi::FDFLAGS_SYNC == wasi::FDFLAGS_SYNC { + flags |= filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC; + } + if fdflags & wasi::FDFLAGS_DSYNC == wasi::FDFLAGS_DSYNC { + flags |= filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC; + } + if fdflags & wasi::FDFLAGS_RSYNC == wasi::FDFLAGS_RSYNC { + flags |= filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC; + } + flags +} + +impl From for Errno { + #[inline(never)] // Disable inlining as this is bulky and relatively cold. + fn from(err: filesystem::ErrorCode) -> Errno { + match err { + // Use a black box to prevent the optimizer from generating a + // lookup table, which would require a static initializer. + filesystem::ErrorCode::Access => black_box(ERRNO_ACCES), + filesystem::ErrorCode::WouldBlock => ERRNO_AGAIN, + filesystem::ErrorCode::Already => ERRNO_ALREADY, + filesystem::ErrorCode::BadDescriptor => ERRNO_BADF, + filesystem::ErrorCode::Busy => ERRNO_BUSY, + filesystem::ErrorCode::Deadlock => ERRNO_DEADLK, + filesystem::ErrorCode::Quota => ERRNO_DQUOT, + filesystem::ErrorCode::Exist => ERRNO_EXIST, + filesystem::ErrorCode::FileTooLarge => ERRNO_FBIG, + filesystem::ErrorCode::IllegalByteSequence => ERRNO_ILSEQ, + filesystem::ErrorCode::InProgress => ERRNO_INPROGRESS, + filesystem::ErrorCode::Interrupted => ERRNO_INTR, + filesystem::ErrorCode::Invalid => ERRNO_INVAL, + filesystem::ErrorCode::Io => ERRNO_IO, + filesystem::ErrorCode::IsDirectory => ERRNO_ISDIR, + filesystem::ErrorCode::Loop => ERRNO_LOOP, + filesystem::ErrorCode::TooManyLinks => ERRNO_MLINK, + filesystem::ErrorCode::MessageSize => ERRNO_MSGSIZE, + filesystem::ErrorCode::NameTooLong => ERRNO_NAMETOOLONG, + filesystem::ErrorCode::NoDevice => ERRNO_NODEV, + filesystem::ErrorCode::NoEntry => ERRNO_NOENT, + filesystem::ErrorCode::NoLock => ERRNO_NOLCK, + filesystem::ErrorCode::InsufficientMemory => ERRNO_NOMEM, + filesystem::ErrorCode::InsufficientSpace => ERRNO_NOSPC, + filesystem::ErrorCode::Unsupported => ERRNO_NOTSUP, + filesystem::ErrorCode::NotDirectory => ERRNO_NOTDIR, + filesystem::ErrorCode::NotEmpty => ERRNO_NOTEMPTY, + filesystem::ErrorCode::NotRecoverable => ERRNO_NOTRECOVERABLE, + filesystem::ErrorCode::NoTty => ERRNO_NOTTY, + filesystem::ErrorCode::NoSuchDevice => ERRNO_NXIO, + filesystem::ErrorCode::Overflow => ERRNO_OVERFLOW, + filesystem::ErrorCode::NotPermitted => ERRNO_PERM, + filesystem::ErrorCode::Pipe => ERRNO_PIPE, + filesystem::ErrorCode::ReadOnly => ERRNO_ROFS, + filesystem::ErrorCode::InvalidSeek => ERRNO_SPIPE, + filesystem::ErrorCode::TextFileBusy => ERRNO_TXTBSY, + filesystem::ErrorCode::CrossDevice => ERRNO_XDEV, + } + } +} + +impl From for wasi::Filetype { + fn from(ty: filesystem::DescriptorType) -> wasi::Filetype { + match ty { + filesystem::DescriptorType::RegularFile => FILETYPE_REGULAR_FILE, + filesystem::DescriptorType::Directory => FILETYPE_DIRECTORY, + filesystem::DescriptorType::BlockDevice => FILETYPE_BLOCK_DEVICE, + filesystem::DescriptorType::CharacterDevice => FILETYPE_CHARACTER_DEVICE, + // preview1 never had a FIFO code. + filesystem::DescriptorType::Fifo => FILETYPE_UNKNOWN, + // TODO: Add a way to disginguish between FILETYPE_SOCKET_STREAM and + // FILETYPE_SOCKET_DGRAM. + filesystem::DescriptorType::Socket => unreachable!(), + filesystem::DescriptorType::SymbolicLink => FILETYPE_SYMBOLIC_LINK, + filesystem::DescriptorType::Unknown => FILETYPE_UNKNOWN, + } + } +} + +#[repr(C)] +pub struct File { + /// The handle to the preview2 descriptor that this file is referencing. + fd: filesystem::Descriptor, + + /// The descriptor type, as supplied by filesystem::get_type at opening + descriptor_type: filesystem::DescriptorType, + + /// The current-position pointer. + position: Cell, + + /// In append mode, all writes append to the file. + append: bool, + + /// In blocking mode, read and write calls dispatch to blocking_read and + /// blocking_write on the underlying streams. When false, read and write + /// dispatch to stream's plain read and write. + blocking: bool, +} + +impl File { + fn is_dir(&self) -> bool { + match self.descriptor_type { + filesystem::DescriptorType::Directory => true, + _ => false, + } + } +} + +const PAGE_SIZE: usize = 65536; + +/// The maximum path length. WASI doesn't explicitly guarantee this, but all +/// popular OS's have a `PATH_MAX` of at most 4096, so that's enough for this +/// polyfill. +const PATH_MAX: usize = 4096; + +/// Maximum number of bytes to cache for a `wasi::Dirent` plus its path name. +const DIRENT_CACHE: usize = 256; + +/// A canary value to detect memory corruption within `State`. +const MAGIC: u32 = u32::from_le_bytes(*b"ugh!"); + +#[repr(C)] // used for now to keep magic1 and magic2 at the start and end +struct State { + /// A canary constant value located at the beginning of this structure to + /// try to catch memory corruption coming from the bottom. + magic1: u32, + + /// Used to coordinate allocations of `cabi_import_realloc` + import_alloc: ImportAlloc, + + /// Storage of mapping from preview1 file descriptors to preview2 file + /// descriptors. + /// + /// Do not use this member directly - use State::descriptors() to ensure + /// lazy initialization happens. + descriptors: RefCell>, + + /// Auxiliary storage to handle the `path_readlink` function. + path_buf: UnsafeCell>, + + /// Long-lived bump allocated memory arena. + /// + /// This is used for the cabi_export_realloc to allocate data passed to the + /// `run` entrypoint. Allocations in this arena are safe to use for + /// the lifetime of the State struct. It may also be used for import allocations + /// which need to be long-lived, by using `import_alloc.with_arena`. + long_lived_arena: BumpArena, + + /// Arguments. Initialized lazily. Access with `State::get_args` to take care of + /// initialization. + args: Cell>, + + /// Environment variables. Initialized lazily. Access with `State::get_environment` + /// to take care of initialization. + env_vars: Cell>, + + /// Cache for the `fd_readdir` call for a final `wasi::Dirent` plus path + /// name that didn't fit into the caller's buffer. + dirent_cache: DirentCache, + + /// The string `..` for use by the directory iterator. + dotdot: [UnsafeCell; 2], + + /// Another canary constant located at the end of the structure to catch + /// memory corruption coming from the bottom. + magic2: u32, +} + +struct DirentCache { + stream: Cell>, + for_fd: Cell, + cookie: Cell, + cached_dirent: Cell, + path_data: UnsafeCell>, +} + +struct DirectoryEntryStream(filesystem::DirectoryEntryStream); + +impl Drop for DirectoryEntryStream { + fn drop(&mut self) { + filesystem::drop_directory_entry_stream(self.0); + } +} + +#[repr(C)] +pub struct WasmStr { + ptr: *const u8, + len: usize, +} + +#[repr(C)] +pub struct WasmStrList { + base: *const WasmStr, + len: usize, +} + +#[repr(C)] +pub struct StrTuple { + key: WasmStr, + value: WasmStr, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct StrTupleList { + base: *const StrTuple, + len: usize, +} + +const fn bump_arena_size() -> usize { + // The total size of the struct should be a page, so start there + let mut start = PAGE_SIZE; + + // Remove the big chunks of the struct, the `path_buf` and `descriptors` + // fields. + start -= PATH_MAX; + start -= size_of::(); + start -= size_of::(); + + // Remove miscellaneous metadata also stored in state. + start -= 16 * size_of::(); + + // Everything else is the `command_data` allocation. + start +} + +// Statically assert that the `State` structure is the size of a wasm page. This +// mostly guarantees that it's not larger than one page which is relied upon +// below. +#[cfg(target_arch = "wasm32")] +const _: () = { + let _size_assert: [(); PAGE_SIZE] = [(); size_of::>()]; +}; + +#[allow(unused)] +#[repr(i32)] +enum AllocationState { + StackUnallocated, + StackAllocating, + StackAllocated, + StateAllocating, + StateAllocated, +} + +#[allow(improper_ctypes)] +extern "C" { + fn get_state_ptr() -> *const RefCell; + fn set_state_ptr(state: *const RefCell); + fn get_allocation_state() -> AllocationState; + fn set_allocation_state(state: AllocationState); + fn get_stderr_stream() -> Fd; + fn set_stderr_stream(fd: Fd); +} + +impl State { + fn with(f: impl FnOnce(&State) -> Result<(), Errno>) -> Errno { + let ptr = State::ptr(); + let ptr = ptr.try_borrow().unwrap_or_else(|_| unreachable!()); + assert_eq!(ptr.magic1, MAGIC); + assert_eq!(ptr.magic2, MAGIC); + let ret = f(&*ptr); + match ret { + Ok(()) => ERRNO_SUCCESS, + Err(err) => err, + } + } + + fn with_mut(f: impl FnOnce(&mut State) -> Result<(), Errno>) -> Errno { + let ptr = State::ptr(); + let mut ptr = ptr.try_borrow_mut().unwrap_or_else(|_| unreachable!()); + assert_eq!(ptr.magic1, MAGIC); + assert_eq!(ptr.magic2, MAGIC); + let ret = f(&mut *ptr); + match ret { + Ok(()) => ERRNO_SUCCESS, + Err(err) => err, + } + } + + fn ptr() -> &'static RefCell { + unsafe { + let mut ptr = get_state_ptr(); + if ptr.is_null() { + ptr = State::new(); + set_state_ptr(ptr); + } + &*ptr + } + } + + #[cold] + fn new() -> &'static RefCell { + #[link(wasm_import_module = "__main_module__")] + extern "C" { + fn cabi_realloc( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, + ) -> *mut u8; + } + + assert!(matches!( + unsafe { get_allocation_state() }, + AllocationState::StackAllocated + )); + + unsafe { set_allocation_state(AllocationState::StateAllocating) }; + + let ret = unsafe { + cabi_realloc( + ptr::null_mut(), + 0, + mem::align_of::>(), + mem::size_of::>(), + ) as *mut RefCell + }; + + unsafe { set_allocation_state(AllocationState::StateAllocated) }; + + unsafe { + ret.write(RefCell::new(State { + magic1: MAGIC, + magic2: MAGIC, + import_alloc: ImportAlloc::new(), + descriptors: RefCell::new(None), + path_buf: UnsafeCell::new(MaybeUninit::uninit()), + long_lived_arena: BumpArena::new(), + args: Cell::new(None), + env_vars: Cell::new(None), + dirent_cache: DirentCache { + stream: Cell::new(None), + for_fd: Cell::new(0), + cookie: Cell::new(wasi::DIRCOOKIE_START), + cached_dirent: Cell::new(wasi::Dirent { + d_next: 0, + d_ino: 0, + d_type: FILETYPE_UNKNOWN, + d_namlen: 0, + }), + path_data: UnsafeCell::new(MaybeUninit::uninit()), + }, + dotdot: [UnsafeCell::new(b'.'), UnsafeCell::new(b'.')], + })); + &*ret + } + } + + /// Accessor for the descriptors member that ensures it is properly initialized + fn descriptors<'a>(&'a self) -> impl Deref + 'a { + let mut d = self + .descriptors + .try_borrow_mut() + .unwrap_or_else(|_| unreachable!()); + if d.is_none() { + *d = Some(Descriptors::new(&self.import_alloc, &self.long_lived_arena)); + } + RefMut::map(d, |d| d.as_mut().unwrap_or_else(|| unreachable!())) + } + + /// Mut accessor for the descriptors member that ensures it is properly initialized + fn descriptors_mut<'a>(&'a mut self) -> impl DerefMut + Deref + 'a { + let mut d = self + .descriptors + .try_borrow_mut() + .unwrap_or_else(|_| unreachable!()); + if d.is_none() { + *d = Some(Descriptors::new(&self.import_alloc, &self.long_lived_arena)); + } + RefMut::map(d, |d| d.as_mut().unwrap_or_else(|| unreachable!())) + } + + fn get_environment(&self) -> &[StrTuple] { + if self.env_vars.get().is_none() { + #[link(wasm_import_module = "environment")] + extern "C" { + #[link_name = "get-environment"] + fn get_environment_import(rval: *mut StrTupleList); + } + let mut list = StrTupleList { + base: std::ptr::null(), + len: 0, + }; + self.import_alloc + .with_arena(&self.long_lived_arena, || unsafe { + get_environment_import(&mut list as *mut _) + }); + self.env_vars.set(Some(unsafe { + /* allocation comes from long lived arena, so it is safe to + * cast this to a &'static slice: */ + std::slice::from_raw_parts(list.base, list.len) + })); + } + self.env_vars.get().trapping_unwrap() + } + + fn get_args(&self) -> &[WasmStr] { + if self.args.get().is_none() { + #[link(wasm_import_module = "environment")] + extern "C" { + #[link_name = "get-arguments"] + fn get_args_import(rval: *mut WasmStrList); + } + let mut list = WasmStrList { + base: std::ptr::null(), + len: 0, + }; + self.import_alloc + .with_arena(&self.long_lived_arena, || unsafe { + get_args_import(&mut list as *mut _) + }); + self.args.set(Some(unsafe { + /* allocation comes from long lived arena, so it is safe to + * cast this to a &'static slice: */ + std::slice::from_raw_parts(list.base, list.len) + })); + } + self.args.get().trapping_unwrap() + } +} diff --git a/crates/wasi-preview1-component-adapter/src/macros.rs b/crates/wasi-preview1-component-adapter/src/macros.rs new file mode 100644 index 000000000000..13faf6bc62fc --- /dev/null +++ b/crates/wasi-preview1-component-adapter/src/macros.rs @@ -0,0 +1,91 @@ +//! Minimal versions of standard-library panicking and printing macros. +//! +//! We're avoiding static initializers, so we can't have things like string +//! literals. Replace the standard assert macros with simpler implementations. +#[allow(dead_code)] +#[doc(hidden)] +pub fn print(message: &[u8]) { + let _ = unsafe { crate::bindings::streams::write(crate::get_stderr_stream(), message) }; +} + +/// A minimal `eprint` for debugging. +#[allow(unused_macros)] +macro_rules! eprint { + ($arg:tt) => {{ + // We have to expand string literals into byte arrays to prevent them + // from getting statically initialized. + let message = byte_array_literals::str!($arg); + $crate::macros::print(&message); + }}; +} + +/// A minimal `eprintln` for debugging. +#[allow(unused_macros)] +macro_rules! eprintln { + ($arg:tt) => {{ + // We have to expand string literals into byte arrays to prevent them + // from getting statically initialized. + let message = byte_array_literals::str_nl!($arg); + $crate::macros::print(&message); + }}; +} + +pub(crate) fn eprint_u32(x: u32) { + if x == 0 { + eprint!("0"); + } else { + eprint_u32_impl(x) + } + + fn eprint_u32_impl(x: u32) { + if x != 0 { + eprint_u32_impl(x / 10); + + let digit = [b'0' + ((x % 10) as u8)]; + crate::macros::print(&digit); + } + } +} + +/// A minimal `unreachable`. +macro_rules! unreachable { + () => {{ + eprint!("unreachable executed at adapter line "); + crate::macros::eprint_u32(line!()); + eprint!("\n"); + #[cfg(target_arch = "wasm32")] + core::arch::wasm32::unreachable(); + // This is here to keep rust-analyzer happy when building for native: + #[cfg(not(target_arch = "wasm32"))] + std::process::abort(); + }}; + + ($arg:tt) => {{ + eprint!("unreachable executed at adapter line "); + crate::macros::eprint_u32(line!()); + eprint!(": "); + eprintln!($arg); + eprint!("\n"); + #[cfg(target_arch = "wasm32")] + core::arch::wasm32::unreachable(); + // This is here to keep rust-analyzer happy when building for native: + #[cfg(not(target_arch = "wasm32"))] + std::process::abort(); + }}; +} + +/// A minimal `assert`. +macro_rules! assert { + ($cond:expr $(,)?) => { + if !$cond { + unreachable!("assertion failed") + } + }; +} + +/// A minimal `assert_eq`. +macro_rules! assert_eq { + ($left:expr, $right:expr $(,)?) => { + assert!($left == $right); + }; +} diff --git a/crates/wasi-preview1-component-adapter/verify/Cargo.toml b/crates/wasi-preview1-component-adapter/verify/Cargo.toml new file mode 100644 index 000000000000..e27b733011ba --- /dev/null +++ b/crates/wasi-preview1-component-adapter/verify/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "verify-component-adapter" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false + +[dependencies] +wasmparser = "0.92.0" +wat = "1.0.62" +anyhow = "1" diff --git a/crates/wasi-preview1-component-adapter/verify/README.md b/crates/wasi-preview1-component-adapter/verify/README.md new file mode 100644 index 000000000000..dc4768b56a0d --- /dev/null +++ b/crates/wasi-preview1-component-adapter/verify/README.md @@ -0,0 +1,11 @@ +# verify-component-adapter + +The `wasi-preview1-component-adapter` crate must compile to a wasm binary that +meets a challenging set of constraints, in order to be used as an adapter by +the `wasm-tools component new` tool. + +There are a limited set of wasm sections allowed in the binary, and a limited +set of wasm modules we allow imports from. + +This crate is a bin target which parses a wasm file and reports an error if it +does not fit in those constraints. diff --git a/crates/wasi-preview1-component-adapter/verify/src/main.rs b/crates/wasi-preview1-component-adapter/verify/src/main.rs new file mode 100644 index 000000000000..ad4861f56c6f --- /dev/null +++ b/crates/wasi-preview1-component-adapter/verify/src/main.rs @@ -0,0 +1,104 @@ +use anyhow::{bail, Result}; +use std::env; +use wasmparser::*; + +const ALLOWED_IMPORT_MODULES: &[&str] = &[ + "wall-clock", + "monotonic-clock", + "timezone", + "filesystem", + "instance-network", + "ip-name-lookup", + "network", + "tcp-create-socket", + "tcp", + "udp-create-socket", + "udp", + "random", + "poll", + "streams", + "environment", + "preopens", + "exit", + "canonical_abi", + "__main_module__", +]; + +fn main() -> Result<()> { + let file = env::args() + .nth(1) + .expect("must pass wasm file as an argument"); + let wasm = wat::parse_file(&file)?; + + let mut validator = Validator::new(); + for payload in Parser::new(0).parse_all(&wasm) { + let payload = payload?; + validator.payload(&payload)?; + match payload { + Payload::Version { encoding, .. } => { + if encoding != Encoding::Module { + bail!("adapter must be a core wasm module, not a component"); + } + } + Payload::End(_) => {} + Payload::TypeSection(_) => {} + Payload::ImportSection(s) => { + for i in s { + let i = i?; + match i.ty { + TypeRef::Func(_) => { + if !ALLOWED_IMPORT_MODULES.contains(&i.module) { + bail!("import from unknown module `{}`", i.module); + } + } + TypeRef::Table(_) => bail!("should not import table"), + TypeRef::Global(_) => bail!("should not import globals"), + TypeRef::Memory(_) => {} + TypeRef::Tag(_) => bail!("unsupported `tag` type"), + } + } + } + Payload::TableSection(_) => {} + Payload::MemorySection(_) => { + bail!("preview1.wasm should import memory"); + } + Payload::GlobalSection(_) => {} + + Payload::ExportSection(_) => {} + + Payload::FunctionSection(_) => {} + + Payload::CodeSectionStart { .. } => {} + Payload::CodeSectionEntry(_) => {} + Payload::CustomSection(_) => {} + + // sections that shouldn't appear in the specially-crafted core wasm + // adapter self we're processing + Payload::DataCountSection { .. } + | Payload::ElementSection(_) + | Payload::DataSection(_) + | Payload::StartSection { .. } + | Payload::TagSection(_) + | Payload::UnknownSection { .. } => { + bail!("unsupported section {payload:?} found in preview1.wasm") + } + + // component-model related things that shouldn't show up + Payload::ModuleSection { .. } + | Payload::ComponentSection { .. } + | Payload::InstanceSection(_) + | Payload::ComponentInstanceSection(_) + | Payload::ComponentAliasSection(_) + | Payload::ComponentCanonicalSection(_) + | Payload::ComponentStartSection(_) + | Payload::ComponentImportSection(_) + | Payload::CoreTypeSection(_) + | Payload::ComponentExportSection(_) + | Payload::ComponentTypeSection(_) => { + bail!("component section found in preview1.wasm") + } + } + } + + Ok(()) +} diff --git a/crates/wasi-preview1-component-adapter/wit/command.wit b/crates/wasi-preview1-component-adapter/wit/command.wit new file mode 100644 index 000000000000..3e929515f0ba --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/command.wit @@ -0,0 +1,21 @@ +default world command { + import wall-clock: clocks.wall-clock + import monotonic-clock: clocks.monotonic-clock + import timezone: clocks.timezone + import filesystem: filesystem.filesystem + import instance-network: sockets.instance-network + import ip-name-lookup: sockets.ip-name-lookup + import network: sockets.network + import tcp-create-socket: sockets.tcp-create-socket + import tcp: sockets.tcp + import udp-create-socket: sockets.udp-create-socket + import udp: sockets.udp + import random: random.random + import poll: poll.poll + import streams: io.streams + import environment: wasi-cli-base.environment + import preopens: wasi-cli-base.preopens + import exit: wasi-cli-base.exit + + export run: func() -> result +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/clocks/monotonic-clock.wit b/crates/wasi-preview1-component-adapter/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 000000000000..42e2981f579a --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,32 @@ +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +/// +/// It is intended for measuring elapsed time. +default interface monotonic-clock { + use poll.poll.{pollable} + + /// A timestamp in nanoseconds. + type instant = u64 + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func() -> instant + + /// Query the resolution of the clock. + resolution: func() -> instant + + /// Create a `pollable` which will resolve once the specified time has been + /// reached. + subscribe: func( + when: instant, + absolute: bool + ) -> pollable +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/clocks/timezone.wit b/crates/wasi-preview1-component-adapter/wit/deps/clocks/timezone.wit new file mode 100644 index 000000000000..63f99cc401b1 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/clocks/timezone.wit @@ -0,0 +1,61 @@ +default interface timezone { + use pkg.wall-clock.{datetime} + + /// A timezone. + /// + /// In timezones that recognize daylight saving time, also known as daylight + /// time and summer time, the information returned from the functions varies + /// over time to reflect these adjustments. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type timezone = u32 + + /// 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. + display: func(this: timezone, when: datetime) -> timezone-display + + /// The same as `display`, but only return the UTC offset. + utc-offset: func(this: timezone, when: datetime) -> s32 + + /// Dispose of the specified input-stream, after which it may no longer + /// be used. + drop-timezone: func(this: timezone) + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + 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-preview1-component-adapter/wit/deps/clocks/wall-clock.wit b/crates/wasi-preview1-component-adapter/wit/deps/clocks/wall-clock.wit new file mode 100644 index 000000000000..89c5a75da68b --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,41 @@ +/// 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. +default interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func() -> datetime + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func() -> datetime +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/filesystem/filesystem.wit b/crates/wasi-preview1-component-adapter/wit/deps/filesystem/filesystem.wit new file mode 100644 index 000000000000..234ef39a8c23 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/filesystem/filesystem.wit @@ -0,0 +1,751 @@ +/// 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`. +default interface filesystem { + use io.streams.{input-stream, output-stream} + use clocks.wall-clock.{datetime} + + /// File size or length of a region within a file. + type filesize = u64 + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// Device ID of device containing the file. + device: device, + /// File serial number. + inode: inode, + /// 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. + data-access-timestamp: datetime, + /// Last data modification timestamp. + data-modification-timestamp: datetime, + /// Last file status change timestamp. + status-change-timestamp: datetime, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Permissions mode used by `open-at`, `change-file-permissions-at`, and + /// similar. + flags modes { + /// True if the resource is considered readable by the containing + /// filesystem. + readable, + /// True if the resource is considered writeable by the containing + /// filesystem. + writeable, + /// True if the resource is considered executable by the containing + /// filesystem. This does not apply to directories. + executable, + } + + /// Number of hard links to an inode. + type link-count = u64 + + /// Identifier for a device containing a file system. Can be used in + /// combination with `inode` to uniquely identify a file or directory in + /// the filesystem. + type device = u64 + + /// Filesystem object serial number that is unique within its file system. + type inode = u64 + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The serial number of the object referred to by this directory entry. + /// May be none if the inode value is not known. + /// + /// When this is none, libc implementations might do an extra `stat-at` + /// call to retrieve the inode number to fill their `d_ino` fields, so + /// implementations which can set this to a non-none value should do so. + inode: option, + + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 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. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type descriptor = u32 + + /// 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. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func( + this: descriptor, + /// The offset within the file at which to start reading. + offset: filesize, + ) -> input-stream + + /// Return a stream for writing to a file. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func( + this: descriptor, + /// The offset within the file at which to start writing. + offset: filesize, + ) -> output-stream + + /// Return a stream for appending to a file. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func( + this: descriptor, + ) -> output-stream + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func( + this: descriptor, + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code> + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func(this: descriptor) -> result<_, error-code> + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func(this: descriptor) -> result + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func(this: descriptor) -> result + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(this: descriptor, size: filesize) -> result<_, error-code> + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func( + this: descriptor, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code> + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func( + this: descriptor, + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code> + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func( + this: descriptor, + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func( + this: descriptor + ) -> result + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func(this: descriptor) -> result<_, error-code> + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func( + this: descriptor, + /// 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. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func(this: descriptor) -> result + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code> + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func( + this: descriptor, + /// 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: descriptor, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code> + + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func( + this: descriptor, + /// 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, + /// Permissions to use when creating a new file. + modes: modes + ) -> result + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func( + this: descriptor, + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func( + this: descriptor, + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code> + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func( + this: descriptor, + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: descriptor, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code> + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func( + this: descriptor, + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code> + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func( + this: descriptor, + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code> + + /// Change the permissions of a filesystem object that is not a directory. + /// + /// Note that the ultimate meanings of these permissions is + /// filesystem-specific. + /// + /// Note: This is similar to `fchmodat` in POSIX. + change-file-permissions-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path to operate on. + path: string, + /// The new permissions for the filesystem object. + modes: modes, + ) -> result<_, error-code> + + /// Change the permissions of a directory. + /// + /// Note that the ultimate meanings of these permissions is + /// filesystem-specific. + /// + /// Unlike in POSIX, the `executable` flag is not reinterpreted as a "search" + /// flag. `read` on a directory implies readability and searchability, and + /// `execute` is not valid for directories. + /// + /// Note: This is similar to `fchmodat` in POSIX. + change-directory-permissions-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path to operate on. + path: string, + /// The new permissions for the directory. + modes: modes, + ) -> result<_, error-code> + + /// Request a shared advisory lock for an open file. + /// + /// This requests a *shared* lock; more than one shared lock can be held for + /// a file at the same time. + /// + /// If the open file has an exclusive lock, this function downgrades the lock + /// to a shared lock. If it has a shared lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified how shared locks interact with locks acquired by + /// non-WASI programs. + /// + /// This function blocks until the lock can be acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_SH)` in Unix. + lock-shared: func(this: descriptor) -> result<_, error-code> + + /// Request an exclusive advisory lock for an open file. + /// + /// This requests an *exclusive* lock; no other locks may be held for the + /// file while an exclusive lock is held. + /// + /// If the open file has a shared lock and there are no exclusive locks held + /// for the file, this function upgrades the lock to an exclusive lock. If the + /// open file already has an exclusive lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified whether this function succeeds if the file descriptor + /// is not opened for writing. It is unspecified how exclusive locks interact + /// with locks acquired by non-WASI programs. + /// + /// This function blocks until the lock can be acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_EX)` in Unix. + lock-exclusive: func(this: descriptor) -> result<_, error-code> + + /// Request a shared advisory lock for an open file. + /// + /// This requests a *shared* lock; more than one shared lock can be held for + /// a file at the same time. + /// + /// If the open file has an exclusive lock, this function downgrades the lock + /// to a shared lock. If it has a shared lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified how shared locks interact with locks acquired by + /// non-WASI programs. + /// + /// This function returns `error-code::would-block` if the lock cannot be + /// acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_SH | LOCK_NB)` in Unix. + try-lock-shared: func(this: descriptor) -> result<_, error-code> + + /// Request an exclusive advisory lock for an open file. + /// + /// This requests an *exclusive* lock; no other locks may be held for the + /// file while an exclusive lock is held. + /// + /// If the open file has a shared lock and there are no exclusive locks held + /// for the file, this function upgrades the lock to an exclusive lock. If the + /// open file already has an exclusive lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified whether this function succeeds if the file descriptor + /// is not opened for writing. It is unspecified how exclusive locks interact + /// with locks acquired by non-WASI programs. + /// + /// This function returns `error-code::would-block` if the lock cannot be + /// acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_EX | LOCK_NB)` in Unix. + try-lock-exclusive: func(this: descriptor) -> result<_, error-code> + + /// Release a shared or exclusive lock on an open file. + /// + /// Note: This is similar to `flock(fd, LOCK_UN)` in Unix. + unlock: func(this: descriptor) -> result<_, error-code> + + /// Dispose of the specified `descriptor`, after which it may no longer + /// be used. + drop-descriptor: func(this: descriptor) + + /// A stream of directory entries. + /// + /// This [represents a stream of `dir-entry`](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Streams). + type directory-entry-stream = u32 + + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func( + this: directory-entry-stream + ) -> result, error-code> + + /// Dispose of the specified `directory-entry-stream`, after which it may no longer + /// be used. + drop-directory-entry-stream: func(this: directory-entry-stream) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/http/incoming-handler.wit b/crates/wasi-preview1-component-adapter/wit/deps/http/incoming-handler.wit new file mode 100644 index 000000000000..1ecff0aa5f5b --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/http/incoming-handler.wit @@ -0,0 +1,24 @@ +// The `wasi:http/incoming-handler` interface is meant to be exported by +// components and called by the host in response to a new incoming HTTP +// response. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +default interface incoming-handler { + use pkg.types.{incoming-request, response-outparam} + + // The `handle` function takes an outparam instead of returning its response + // so that the component may stream its response while streaming any other + // request or response bodies. The callee MUST write a response to the + // `response-out` and then finish the response before returning. The `handle` + // function is allowed to continue execution after finishing the response's + // output stream. While this post-response execution is taken off the + // critical path, since there is no return value, there is no way to report + // its success or failure. + handle: func( + request: incoming-request, + response-out: response-outparam + ) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/http/outgoing-handler.wit b/crates/wasi-preview1-component-adapter/wit/deps/http/outgoing-handler.wit new file mode 100644 index 000000000000..abe812ffaba7 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/http/outgoing-handler.wit @@ -0,0 +1,18 @@ +// The `wasi:http/outgoing-handler` interface is meant to be imported by +// components and implemented by the host. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +default interface outgoing-handler { + use pkg.types.{outgoing-request, request-options, future-incoming-response} + + // The parameter and result types of the `handle` function allow the caller + // to concurrently stream the bodies of the outgoing request and the incoming + // response. + handle: func( + request: outgoing-request, + options: option + ) -> future-incoming-response +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/http/types.wit b/crates/wasi-preview1-component-adapter/wit/deps/http/types.wit new file mode 100644 index 000000000000..bdcf79737273 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/http/types.wit @@ -0,0 +1,157 @@ +// The `wasi:http/types` interface is meant to be imported by components to +// define the HTTP resource types and operations used by the component's +// imported and exported interfaces. +default interface types { + use io.streams.{input-stream, output-stream} + use poll.poll.{pollable} + + // This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + // This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + // TODO: perhaps better align with HTTP semantics? + // This type enumerates the different kinds of errors that may occur when + // initially returning a response. + variant error { + invalid-url(string), + timeout-error(string), + protocol-error(string), + unexpected-error(string) + } + + // This following block defines the `fields` resource which corresponds to + // HTTP standard Fields. Soon, when resource types are added, the `type + // fields = u32` type alias can be replaced by a proper `resource fields` + // definition containing all the functions using the method syntactic sugar. + type fields = u32 + drop-fields: func(fields: fields) + new-fields: func(entries: list>) -> fields + fields-get: func(fields: fields, name: string) -> list + fields-set: func(fields: fields, name: string, value: list) + fields-delete: func(fields: fields, name: string) + fields-append: func(fields: fields, name: string, value: string) + fields-entries: func(fields: fields) -> list> + fields-clone: func(fields: fields) -> fields + + type headers = fields + type trailers = fields + + // The following block defines stream types which corresponds to the HTTP + // standard Contents and Trailers. With Preview3, all of these fields can be + // replaced by a stream>. In the interim, we need to + // build on separate resource types defined by `wasi:io/streams`. The + // `finish-` functions emulate the stream's result value and MUST be called + // exactly once after the final read/write from/to the stream before dropping + // the stream. + type incoming-stream = input-stream + type outgoing-stream = output-stream + finish-incoming-stream: func(s: incoming-stream) -> option + finish-outgoing-stream: func(s: outgoing-stream, trailers: option) + + // The following block defines the `incoming-request` and `outgoing-request` + // resource types that correspond to HTTP standard Requests. Soon, when + // resource types are added, the `u32` type aliases can be replaced by + // proper `resource` type definitions containing all the functions as + // methods. Later, Preview2 will allow both types to be merged together into + // a single `request` type (that uses the single `stream` type mentioned + // above). The `consume` and `write` methods may only be called once (and + // return failure thereafter). + type incoming-request = u32 + type outgoing-request = u32 + drop-incoming-request: func(request: incoming-request) + drop-outgoing-request: func(request: outgoing-request) + incoming-request-method: func(request: incoming-request) -> method + incoming-request-path: func(request: incoming-request) -> string + incoming-request-query: func(request: incoming-request) -> string + incoming-request-scheme: func(request: incoming-request) -> option + incoming-request-authority: func(request: incoming-request) -> string + incoming-request-headers: func(request: incoming-request) -> headers + incoming-request-consume: func(request: incoming-request) -> result + new-outgoing-request: func( + method: method, + path: string, + query: string, + scheme: option, + authority: string, + headers: headers + ) -> outgoing-request + outgoing-request-write: func(request: outgoing-request) -> result + + // Additional optional parameters that can be set when making a request. + record request-options { + // The following timeouts are specific to the HTTP protocol and work + // independently of the overall timeouts passed to `io.poll.poll-oneoff`. + + // The timeout for the initial connect. + connect-timeout-ms: option, + + // The timeout for receiving the first byte of the response body. + first-byte-timeout-ms: option, + + // The timeout for receiving the next chunk of bytes in the response body + // stream. + between-bytes-timeout-ms: option + } + + // The following block defines a special resource type used by the + // `wasi:http/incoming-handler` interface. When resource types are added, this + // block can be replaced by a proper `resource response-outparam { ... }` + // definition. Later, with Preview3, the need for an outparam goes away entirely + // (the `wasi:http/handler` interface used for both incoming and outgoing can + // simply return a `stream`). + type response-outparam = u32 + drop-response-outparam: func(response: response-outparam) + set-response-outparam: func(response: result) -> result + + // This type corresponds to the HTTP standard Status Code. + type status-code = u16 + + // The following block defines the `incoming-response` and `outgoing-response` + // resource types that correspond to HTTP standard Responses. Soon, when + // resource types are added, the `u32` type aliases can be replaced by proper + // `resource` type definitions containing all the functions as methods. Later, + // Preview2 will allow both types to be merged together into a single `response` + // type (that uses the single `stream` type mentioned above). The `consume` and + // `write` methods may only be called once (and return failure thereafter). + type incoming-response = u32 + type outgoing-response = u32 + drop-incoming-response: func(response: incoming-response) + drop-outgoing-response: func(response: outgoing-response) + incoming-response-status: func(response: incoming-response) -> status-code + incoming-response-headers: func(response: incoming-response) -> headers + incoming-response-consume: func(response: incoming-response) -> result + new-outgoing-response: func( + status-code: status-code, + headers: headers + ) -> outgoing-response + outgoing-response-write: func(response: outgoing-response) -> result + + // The following block defines a special resource type used by the + // `wasi:http/outgoing-handler` interface to emulate + // `future>` in advance of Preview3. Given a + // `future-incoming-response`, the client can call the non-blocking `get` + // method to get the result if it is available. If the result is not available, + // the client can call `listen` to get a `pollable` that can be passed to + // `io.poll.poll-oneoff`. + type future-incoming-response = u32 + drop-future-incoming-response: func(f: future-incoming-response) + future-incoming-response-get: func(f: future-incoming-response) -> option> + listen-to-future-incoming-response: func(f: future-incoming-response) -> pollable +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/io/streams.wit b/crates/wasi-preview1-component-adapter/wit/deps/io/streams.wit new file mode 100644 index 000000000000..c1567fd4c1c8 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/io/streams.wit @@ -0,0 +1,213 @@ +/// 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. +default interface streams { + use poll.poll.{pollable} + + /// An error type returned from a stream operation. Currently this + /// doesn't provide any additional information. + record stream-error {} + + /// An input bytestream. In the future, this will be replaced by handle + /// types. + /// + /// This conceptually represents a `stream`. It's temporary + /// scaffolding until component-model's async features are ready. + /// + /// `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-to-input-stream` function to obtain a `pollable` which + /// can be polled for using `wasi_poll`. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type input-stream = u32 + + /// Read bytes from a stream. + /// + /// 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 + /// stream was reached. The returned list will contain up to `len` bytes; it + /// may return fewer than requested, but not more. + /// + /// Once a stream has reached the end, subsequent calls to read or + /// `skip` will always report end-of-stream rather than producing more + /// data. + /// + /// If `len` is 0, it represents a request to read 0 bytes, which should + /// always succeed, assuming the stream hasn't reached its end yet, and + /// return an empty list. + /// + /// The len here is a `u64`, but some callees may not be able to allocate + /// a buffer as large as that would imply. + /// FIXME: describe what happens if allocation fails. + read: func( + this: input-stream, + /// The maximum number of bytes to read + len: u64 + ) -> result, bool>, stream-error> + + /// Read bytes from a stream, with blocking. + /// + /// This is similar to `read`, except that it blocks until at least one + /// byte can be read. + blocking-read: func( + this: input-stream, + /// The maximum number of bytes to read + len: u64 + ) -> result, bool>, stream-error> + + /// Skip bytes from a stream. + /// + /// This is similar to the `read` function, but avoids copying the + /// bytes into the instance. + /// + /// Once a stream has reached the end, subsequent calls to read or + /// `skip` will always report end-of-stream rather than producing more + /// data. + /// + /// This function returns the number of bytes skipped, along with a bool + /// indicating whether the end of the stream was reached. The returned + /// value will be at most `len`; it may be less. + skip: func( + this: input-stream, + /// The maximum number of bytes to skip. + len: u64, + ) -> result, stream-error> + + /// Skip bytes from a stream, with blocking. + /// + /// This is similar to `skip`, except that it blocks until at least one + /// byte can be consumed. + blocking-skip: func( + this: input-stream, + /// The maximum number of bytes to skip. + len: u64, + ) -> result, stream-error> + + /// 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. + subscribe-to-input-stream: func(this: input-stream) -> pollable + + /// Dispose of the specified `input-stream`, after which it may no longer + /// be used. + drop-input-stream: func(this: input-stream) + + /// An output bytestream. In the future, this will be replaced by handle + /// types. + /// + /// This conceptually represents a `stream`. It's temporary + /// scaffolding until component-model's async features are ready. + /// + /// `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-to-output-stream` function to obtain a + /// `pollable` which can be polled for using `wasi_poll`. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type output-stream = u32 + + /// Write bytes to a stream. + /// + /// This function returns a `u64` indicating the number of bytes from + /// `buf` that were written; it may be less than the full list. + write: func( + this: output-stream, + /// Data to write + buf: list + ) -> result + + /// Write bytes to a stream, with blocking. + /// + /// This is similar to `write`, except that it blocks until at least one + /// byte can be written. + blocking-write: func( + this: output-stream, + /// Data to write + buf: list + ) -> result + + /// Write multiple zero bytes to a stream. + /// + /// This function returns a `u64` indicating the number of zero bytes + /// that were written; it may be less than `len`. + write-zeroes: func( + this: output-stream, + /// The number of zero bytes to write + len: u64 + ) -> result + + /// Write multiple zero bytes to a stream, with blocking. + /// + /// This is similar to `write-zeroes`, except that it blocks until at least + /// one byte can be written. + blocking-write-zeroes: func( + this: output-stream, + /// The number of zero bytes to write + len: u64 + ) -> result + + /// Read from one stream and write to another. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + /// + /// Unlike other I/O functions, this function blocks until all the data + /// read from the input stream has been written to the output stream. + splice: func( + this: output-stream, + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result, stream-error> + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until at least + /// one byte can be read. + blocking-splice: func( + this: output-stream, + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result, stream-error> + + /// Forward the entire contents of an input stream to an output stream. + /// + /// This function repeatedly reads from the input stream and writes + /// the data to the output stream, until the end of the input stream + /// is reached, or an error is encountered. + /// + /// Unlike other I/O functions, this function blocks until the end + /// of the input stream is seen and all the data has been written to + /// the output stream. + /// + /// This function returns the number of bytes transferred. + forward: func( + this: output-stream, + /// The stream to read from + src: input-stream + ) -> result + + /// Create a `pollable` which will resolve once either the specified stream + /// is ready to accept bytes or the other end of the stream has been closed. + subscribe-to-output-stream: func(this: output-stream) -> pollable + + /// Dispose of the specified `output-stream`, after which it may no longer + /// be used. + drop-output-stream: func(this: output-stream) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/logging/handler.wit b/crates/wasi-preview1-component-adapter/wit/deps/logging/handler.wit new file mode 100644 index 000000000000..c9632b9cc4f8 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/logging/handler.wit @@ -0,0 +1,32 @@ +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +default interface handler { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + + /// Describes messages indicating hazardous situations. + warn, + + /// Describes messages indicating serious errors. + error, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/poll/poll.wit b/crates/wasi-preview1-component-adapter/wit/deps/poll/poll.wit new file mode 100644 index 000000000000..28f08e17d755 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/poll/poll.wit @@ -0,0 +1,39 @@ +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +default interface poll { + /// A "pollable" handle. + /// + /// This is conceptually represents a `stream<_, _>`, or in other words, + /// a stream that one can wait on, repeatedly, but which does not itself + /// produce any data. It's temporary scaffolding until component-model's + /// async features are ready. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// `pollable` lifetimes are not automatically managed. Users must ensure + /// that they do not outlive the resource they reference. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type pollable = u32 + + /// Dispose of the specified `pollable`, after which it may no longer + /// be used. + drop-pollable: func(this: pollable) + + /// Poll for completion on a set of pollables. + /// + /// The "oneoff" in the name refers to the fact that this function must do a + /// linear scan through the entire list of subscriptions, which may be + /// inefficient if the number is large and the same subscriptions are used + /// many times. In the future, this is expected to be obsoleted by the + /// component model async proposal, which will include a scalable waiting + /// facility. + /// + /// Note that the return type would ideally be `list`, but that would + /// be more difficult to polyfill given the current state of `wit-bindgen`. + /// See + /// for details. For now, we use zero to mean "not ready" and non-zero to + /// mean "ready". + poll-oneoff: func(in: list) -> list +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/preview/command-extended.wit b/crates/wasi-preview1-component-adapter/wit/deps/preview/command-extended.wit new file mode 100644 index 000000000000..11a0444eda0d --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/preview/command-extended.wit @@ -0,0 +1,29 @@ +default world command-extended { + import wall-clock: clocks.wall-clock + import monotonic-clock: clocks.monotonic-clock + import timezone: clocks.timezone + import filesystem: filesystem.filesystem + import instance-network: sockets.instance-network + import ip-name-lookup: sockets.ip-name-lookup + import network: sockets.network + import tcp-create-socket: sockets.tcp-create-socket + import tcp: sockets.tcp + import udp-create-socket: sockets.udp-create-socket + import udp: sockets.udp + import random: random.random + import poll: poll.poll + import streams: io.streams + import environment: wasi-cli-base.environment + import preopens: wasi-cli-base.preopens + import exit: wasi-cli-base.exit + + // We should replace all others with `include self.command` + // as soon as the unioning of worlds is available: + // https://github.com/WebAssembly/component-model/issues/169 + import console: logging.handler + import default-outgoing-HTTP: http.outgoing-handler + + export run: func( + args: list, + ) -> result +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/preview/command.wit b/crates/wasi-preview1-component-adapter/wit/deps/preview/command.wit new file mode 100644 index 000000000000..3e929515f0ba --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/preview/command.wit @@ -0,0 +1,21 @@ +default world command { + import wall-clock: clocks.wall-clock + import monotonic-clock: clocks.monotonic-clock + import timezone: clocks.timezone + import filesystem: filesystem.filesystem + import instance-network: sockets.instance-network + import ip-name-lookup: sockets.ip-name-lookup + import network: sockets.network + import tcp-create-socket: sockets.tcp-create-socket + import tcp: sockets.tcp + import udp-create-socket: sockets.udp-create-socket + import udp: sockets.udp + import random: random.random + import poll: poll.poll + import streams: io.streams + import environment: wasi-cli-base.environment + import preopens: wasi-cli-base.preopens + import exit: wasi-cli-base.exit + + export run: func() -> result +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/preview/proxy.wit b/crates/wasi-preview1-component-adapter/wit/deps/preview/proxy.wit new file mode 100644 index 000000000000..2f444758946b --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/preview/proxy.wit @@ -0,0 +1,6 @@ +default world proxy { + import random: random.random + import console: logging.handler + import default-outgoing-HTTP: http.outgoing-handler + export HTTP: http.incoming-handler +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/preview/reactor.wit b/crates/wasi-preview1-component-adapter/wit/deps/preview/reactor.wit new file mode 100644 index 000000000000..2abfa466ba3b --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/preview/reactor.wit @@ -0,0 +1,21 @@ +default world reactor { + import wall-clock: clocks.wall-clock + import monotonic-clock: clocks.monotonic-clock + import timezone: clocks.timezone + import filesystem: filesystem.filesystem + import instance-network: sockets.instance-network + import ip-name-lookup: sockets.ip-name-lookup + import network: sockets.network + import tcp-create-socket: sockets.tcp-create-socket + import tcp: sockets.tcp + import udp-create-socket: sockets.udp-create-socket + import udp: sockets.udp + import random: random.random + import poll: poll.poll + import streams: io.streams + import console: logging.handler + import default-outgoing-HTTP: http.outgoing-handler + import environment: wasi-cli-base.environment + import preopens: wasi-cli-base.preopens + import exit: wasi-cli-base.exit +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/random/random.wit b/crates/wasi-preview1-component-adapter/wit/deps/random/random.wit new file mode 100644 index 000000000000..2080ddfde9dd --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/random/random.wit @@ -0,0 +1,42 @@ +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +default interface random { + /// Return `len` cryptographically-secure pseudo-random bytes. + /// + /// This function must produce data from an adequately seeded + /// cryptographically-secure pseudo-random number generator (CSPRNG), so it + /// must not block, from the perspective of the calling program, and the + /// returned data is always unpredictable. + /// + /// This function must always return fresh pseudo-random data. Deterministic + /// environments must omit this function, rather than implementing it with + /// deterministic data. + get-random-bytes: func(len: u64) -> list + + /// Return a cryptographically-secure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-random-bytes`, represented as a `u64`. + get-random-u64: func() -> u64 + + /// 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. + insecure-random: func() -> tuple +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/instance-network.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/instance-network.wit new file mode 100644 index 000000000000..b1f5c982d976 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/instance-network.wit @@ -0,0 +1,9 @@ + +/// This interface provides a value-export of the default network handle.. +default interface instance-network { + use pkg.network.{network} + + /// Get a handle to the default network. + instance-network: func() -> network + +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 000000000000..b594598e0090 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,71 @@ + +default interface ip-name-lookup { + use poll.poll.{pollable} + use pkg.network.{network, error, ip-address, ip-address-family} + + + /// Resolve an internet host name to a list of IP addresses. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// Parameters: + /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted + /// to ASCII using IDNA encoding. + /// - `address-family`: If provided, limit the results to addresses of this specific address family. + /// - `include-unavailable`: When set to true, this function will also return addresses of which the runtime + /// thinks (or knows) can't be connected to at the moment. For example, this will return IPv6 addresses on + /// systems without an active IPv6 interface. Notes: + /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. + /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. + /// + /// This function never blocks. It either immediately returns successfully with a `resolve-address-stream` + /// that can be used to (asynchronously) fetch the results. + /// Or it immediately fails whenever `name` is: + /// - empty + /// - an IP address + /// - a syntactically invalid domain name in another way + /// + /// References: + /// - + /// - + /// + resolve-addresses: func(network: network, name: string, address-family: option, include-unavailable: bool) -> result + + + + type resolve-address-stream = u32 + + /// 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`. + /// After which, you should release the stream with `drop-resolve-address-stream`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + resolve-next-address: func(this: resolve-address-stream) -> result, error> + + + + /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-resolve-address-stream: func(this: resolve-address-stream) + + /// Get/set the blocking mode of the stream. + /// + /// By default a stream is in "blocking" mode, meaning that any function blocks and waits for its completion. + /// When switched to "non-blocking" mode, operations that would block return an `again` error. After which + /// the API consumer is expected to call `subscribe` and wait for completion using the wasi-poll module. + /// + /// Note: these functions are here for WASI Preview2 only. + /// They're planned to be removed when `future` is natively supported in Preview3. + non-blocking: func(this: resolve-address-stream) -> result + set-non-blocking: func(this: resolve-address-stream, value: bool) -> result<_, error> + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: resolve-address-stream) -> pollable +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/network.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/network.wit new file mode 100644 index 000000000000..1f3a20d6b8c8 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/network.wit @@ -0,0 +1,56 @@ + +default interface network { + /// 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. + /// + /// FYI, In the future this will be replaced by handle types. + type network = u32 + + /// Dispose of the specified `network`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-network: func(this: network) + + + + enum error { + unknown, + again, + // TODO ... + } + + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + type ipv4-address = tuple + type ipv6-address = tuple + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + port: u16, // sin_port + address: ipv4-address, // sin_addr + } + + record ipv6-socket-address { + port: u16, // sin6_port + flow-info: u32, // sin6_flowinfo + address: ipv6-address, // sin6_addr + scope-id: u32, // sin6_scope_id + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + +} \ No newline at end of file diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 000000000000..571a0197a90e --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,19 @@ + +default interface tcp-create-socket { + use pkg.network.{network, error, ip-address-family} + use pkg.tcp.{tcp-socket} + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// + /// 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`/`listen`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// References: + /// - + /// - + /// + create-tcp-socket: func(address-family: ip-address-family) -> result +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp.wit new file mode 100644 index 000000000000..b2f483368f97 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/tcp.wit @@ -0,0 +1,188 @@ + +default interface tcp { + use io.streams.{input-stream, output-stream} + use poll.poll.{pollable} + use pkg.network.{network, error, ip-socket-address, ip-address-family} + + /// A TCP socket handle. + type tcp-socket = u32 + + + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + + /// 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. + /// + /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will + /// implicitly bind the socket. + /// + /// Fails when: + /// - the socket is already bound. + /// + /// References + /// - + /// - + bind: func(this: tcp-socket, network: network, local-address: ip-socket-address) -> result<_, error> + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the Connection state + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// Fails when: + /// - the socket is already bound to a different network. + /// - the provided network does not allow connections to the specified endpoint. + /// - the socket is already in the Connection or Listener state. + /// - either the remote IP address or port is 0. + /// + /// References + /// - + /// - + connect: func(this: tcp-socket, network: network, remote-address: ip-socket-address) -> result, error> + + /// Start listening for new connections. + /// + /// Transitions the socket into the Listener state. + /// + /// Fails when: + /// - the socket is already bound to a different network. + /// - the provided network does not allow listening on the specified address. + /// - the socket is already in the Connection or Listener state. + /// + /// References + /// - + /// - + listen: func(this: tcp-socket, network: network) -> result<_, error> + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the Connection state. + /// + /// 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. + /// + /// Fails when this socket is not in the Listening state. + /// + /// References: + /// - + /// - + accept: func(this: tcp-socket) -> result, error> + + /// Get the bound local address. + /// + /// Returns an error if the socket is not bound. + /// + /// References + /// - + /// - + local-address: func(this: tcp-socket) -> result + + /// Get the bound remote address. + /// + /// Fails when the socket is not in the Connection state. + /// + /// References + /// - + /// - + remote-address: func(this: tcp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: tcp-socket) -> result + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// Implementations are not required to support dual-stack mode. Calling `set-ipv6-only(false)` might fail. + /// + /// Fails when called on an IPv4 socket. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + ipv6-only: func(this: tcp-socket) -> result + set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error> + + /// Hints the desired listen queue size. Implementations are free to ignore this. + set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error> + + /// Equivalent to the SO_KEEPALIVE socket option. + keep-alive: func(this: tcp-socket) -> result + set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error> + + /// Equivalent to the TCP_NODELAY socket option. + no-delay: func(this: tcp-socket) -> result + set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + unicast-hop-limit: func(this: tcp-socket) -> result + set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Fails when this socket is in the Listening state. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + receive-buffer-size: func(this: tcp-socket) -> result + set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error> + send-buffer-size: func(this: tcp-socket) -> result + set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error> + + /// Get/set the blocking mode of the socket. + /// + /// By default a socket is in "blocking" mode, meaning that any function blocks and waits for its completion. + /// When switched to "non-blocking" mode, operations that would block return an `again` error. After which + /// the API consumer is expected to call `subscribe` and wait for completion using the wasi-poll module. + /// + /// Note: these functions are here for WASI Preview2 only. + /// They're planned to be removed when `future` is natively supported in Preview3. + non-blocking: func(this: tcp-socket) -> result + set-non-blocking: func(this: tcp-socket, value: bool) -> result<_, error> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: tcp-socket) -> pollable + + /// Gracefully shut down the connection. + /// + /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read + /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. + /// Any data still in the receive queue at time of calling `shutdown` will be discarded. + /// - send: the socket is not expecting to send any more data to the peer. All subsequent write + /// operations on the `output-stream` associated with this socket will return an error. + /// - both: same effect as receive & send combined. + /// + /// The shutdown function does not close the socket. + /// + /// Fails when the socket is not in the Connection state. + /// + /// References + /// - + /// - + shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error> + + /// Dispose of the specified `tcp-socket`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-tcp-socket: func(this: tcp-socket) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 000000000000..169957c9fe80 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,19 @@ + +default interface udp-create-socket { + use pkg.network.{network, error, ip-address-family} + use pkg.udp.{udp-socket} + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// + /// 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. + /// + /// References: + /// - + /// - + /// + create-udp-socket: func(address-family: ip-address-family) -> result +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp.wit b/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp.wit new file mode 100644 index 000000000000..af8f873b9b6c --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/sockets/udp.wit @@ -0,0 +1,162 @@ + +default interface udp { + use poll.poll.{pollable} + use pkg.network.{network, error, ip-socket-address, ip-address-family} + + + /// A UDP socket handle. + type udp-socket = u32 + + + record datagram { + data: list, // Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + remote-address: ip-socket-address, + + /// Possible future additions: + /// local-address: ip-socket-address, // IP_PKTINFO / IP_RECVDSTADDR / IPV6_PKTINFO + /// local-interface: u32, // IP_PKTINFO / IP_RECVIF + /// ttl: u8, // IP_RECVTTL + /// dscp: u6, // IP_RECVTOS + /// ecn: u2, // IP_RECVTOS + } + + + + /// 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. + /// + /// When a socket is not explicitly bound, the first invocation to a connect, send or receive operation will + /// implicitly bind the socket. + /// + /// Fails when: + /// - the socket is already bound. + /// + /// References + /// - + /// - + bind: func(this: udp-socket, network: network, local-address: ip-socket-address) -> result<_, error> + + /// Set the destination address. + /// + /// The local-address is updated based on the best network path to `remote-address`. + /// + /// When a destination address is set: + /// - all receive operations will only return datagrams sent from the provided `remote-address`. + /// - the `send` function can only be used to send to this destination. + /// + /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". + /// + /// Fails when: + /// - the socket is already bound to a different network. + /// + /// References + /// - + /// - + connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error> + + /// Receive a message. + /// + /// Returns: + /// - The sender address of the datagram + /// - The number of bytes read. + /// + /// Fails when: + /// - the socket is not bound. + /// + /// References + /// - + /// - + /// - + receive: func(this: udp-socket) -> result + + /// Send a message to a specific destination address. + /// + /// The remote address option is required. To send a message to the "connected" peer, + /// call `remote-address` to get their address. + /// + /// Fails when: + /// - the socket is not bound. Unlike POSIX, this function does not perform an implicit bind. + /// - the socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. + /// + /// References + /// - + /// - + /// - + send: func(this: udp-socket, datagram: datagram) -> result<_, error> + + /// Get the current bound address. + /// + /// Returns an error if the socket is not bound. + /// + /// References + /// - + /// - + local-address: func(this: udp-socket) -> result + + /// Get the address set with `connect`. + /// + /// References + /// - + /// - + remote-address: func(this: udp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: udp-socket) -> result + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// Implementations are not required to support dual-stack mode, so calling `set-ipv6-only(false)` might fail. + /// + /// Fails when called on an IPv4 socket. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + ipv6-only: func(this: udp-socket) -> result + set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + unicast-hop-limit: func(this: udp-socket) -> result + set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Fails when this socket is in the Listening state. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + receive-buffer-size: func(this: udp-socket) -> result + set-receive-buffer-size: func(this: udp-socket, value: u64) -> result<_, error> + send-buffer-size: func(this: udp-socket) -> result + set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error> + + /// Get/set the blocking mode of the socket. + /// + /// By default a socket is in "blocking" mode, meaning that any function blocks and waits for its completion. + /// When switched to "non-blocking" mode, operations that would block return an `again` error. After which + /// the API consumer is expected to call `subscribe` and wait for completion using the wasi-poll module. + /// + /// Note: these functions are here for WASI Preview2 only. + /// They're planned to be removed when `future` is natively supported in Preview3. + non-blocking: func(this: udp-socket) -> result + set-non-blocking: func(this: udp-socket, value: bool) -> result<_, error> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: udp-socket) -> pollable + + /// Dispose of the specified `udp-socket`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-udp-socket: func(this: udp-socket) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/environment.wit b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/environment.wit new file mode 100644 index 000000000000..876ea3a0c921 --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/environment.wit @@ -0,0 +1,14 @@ +default 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. + get-environment: func() -> list> + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/exit.wit b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/exit.wit new file mode 100644 index 000000000000..2759e9dd989c --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/exit.wit @@ -0,0 +1,4 @@ +default interface wasi-exit { + /// Exit the curerent instance and any linked instances. + exit: func(status: result) +} diff --git a/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/preopens.wit b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/preopens.wit new file mode 100644 index 000000000000..52a93442078e --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/deps/wasi-cli-base/preopens.wit @@ -0,0 +1,17 @@ +default interface preopens { + use filesystem.filesystem.{descriptor} + use io.streams.{input-stream, output-stream} + + /// Stdio preopens: these are the resources that provide stdin, stdout, and + /// stderr. + record stdio-preopens { + stdin: input-stream, + stdout: output-stream, + stderr: output-stream, + } + + /// Return the set of stdio preopens. + get-stdio: func() -> stdio-preopens + /// Return the set of of preopened directories, and their path. + get-directories: func() -> list> +} diff --git a/crates/wasi-preview1-component-adapter/wit/reactor.wit b/crates/wasi-preview1-component-adapter/wit/reactor.wit new file mode 100644 index 000000000000..2abfa466ba3b --- /dev/null +++ b/crates/wasi-preview1-component-adapter/wit/reactor.wit @@ -0,0 +1,21 @@ +default world reactor { + import wall-clock: clocks.wall-clock + import monotonic-clock: clocks.monotonic-clock + import timezone: clocks.timezone + import filesystem: filesystem.filesystem + import instance-network: sockets.instance-network + import ip-name-lookup: sockets.ip-name-lookup + import network: sockets.network + import tcp-create-socket: sockets.tcp-create-socket + import tcp: sockets.tcp + import udp-create-socket: sockets.udp-create-socket + import udp: sockets.udp + import random: random.random + import poll: poll.poll + import streams: io.streams + import console: logging.handler + import default-outgoing-HTTP: http.outgoing-handler + import environment: wasi-cli-base.environment + import preopens: wasi-cli-base.preopens + import exit: wasi-cli-base.exit +} diff --git a/deny.toml b/deny.toml index b67d310264d8..ef68b2105eb5 100644 --- a/deny.toml +++ b/deny.toml @@ -19,6 +19,7 @@ allow = [ "MIT", "MPL-2.0", "OpenSSL", + "Unicode-DFS-2016", "Zlib", ] diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index fbf81f13b4ad..657d02dc5d1a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -58,6 +58,19 @@ I am employed by a member of the Bytecode Alliance and plan to continue doing so and will actively maintain this crate over time. """ +[[wildcard-audits.wasm-metadata]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wasm-tools` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + [[wildcard-audits.wasm-mutate]] who = "Nick Fitzgerald " criteria = "safe-to-deploy" @@ -150,6 +163,84 @@ I am employed by a member of the Bytecode Alliance and plan to continue doing so and will actively maintain this crate over time. """ +[[wildcard-audits.wit-bindgen]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wit-bindgen` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + +[[wildcard-audits.wit-bindgen-core]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wit-bindgen` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + +[[wildcard-audits.wit-bindgen-rust]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wit-bindgen` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + +[[wildcard-audits.wit-bindgen-rust-lib]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wit-bindgen` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + +[[wildcard-audits.wit-bindgen-rust-macro]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wit-bindgen` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + +[[wildcard-audits.wit-component]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 +start = "2020-12-11" +end = "2024-04-14" +notes = """ +This is a Bytecode Alliance authored crate maintained in the `wasm-tools` +repository of which I'm one of the primary maintainers and publishers for. +I am employed by a member of the Bytecode Alliance and plan to continue doing +so and will actively maintain this crate over time. +""" + [[wildcard-audits.wit-parser]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -200,6 +291,11 @@ nightly feature in the standard library for backtrace integration. No undue here. """ +[[audits.anyhow]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.69 -> 1.0.71" + [[audits.arbitrary]] who = "Nick Fitzgerald " criteria = "safe-to-deploy" @@ -1039,6 +1135,11 @@ criteria = "safe-to-deploy" version = "0.3.25" notes = "This crate shells out to the pkg-config executable, but it appears to sanitize inputs reasonably." +[[audits.proc-macro2]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.51 -> 1.0.57" + [[audits.pulldown-cmark]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -1049,6 +1150,11 @@ are otherwise not doing other `unsafe` operations. Additionally the crate does not do anything other than markdown rendering as is expected. """ +[[audits.quote]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.23 -> 1.0.27" + [[audits.regalloc2]] who = "Jamey Sharp " criteria = "safe-to-deploy" @@ -1200,6 +1306,11 @@ but it's all doing what it says on the tin: being a stable polyfill for strict provenance APIs in the standard library while they're on Nightly. """ +[[audits.syn]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.92 -> 2.0.16" + [[audits.system-interface]] who = "Dan Gohman " criteria = "safe-to-deploy" @@ -1302,6 +1413,11 @@ This crate has no unsafe code and does not use `std::*`. Skimming the crate it does not attempt to out of the bounds of what it's already supposed to be doing. """ +[[audits.unicode-ident]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.0.8" + [[audits.unicode-normalization]] who = "Alex Crichton " criteria = "safe-to-deploy" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 89e34d9f0f13..ac785564bd2b 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -585,14 +585,6 @@ criteria = "safe-to-deploy" version = "1.0.4" criteria = "safe-to-deploy" -[[exemptions.proc-macro-error-attr]] -version = "1.0.4" -criteria = "safe-to-deploy" - -[[exemptions.proc-macro2]] -version = "1.0.37" -criteria = "safe-to-deploy" - [[exemptions.proptest]] version = "1.0.0" criteria = "safe-to-deploy" @@ -843,14 +835,6 @@ criteria = "safe-to-run" version = "1.15.0" criteria = "safe-to-deploy" -[[exemptions.unicode-width]] -version = "0.1.9" -criteria = "safe-to-deploy" - -[[exemptions.unicode-xid]] -version = "0.2.3" -criteria = "safe-to-deploy" - [[exemptions.uuid]] version = "1.0.0" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index e9e6be045763..fce2d03ef22b 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -22,20 +22,6 @@ user-id = 696 user-login = "fitzgen" user-name = "Nick Fitzgerald" -[[publisher.regalloc2]] -version = "0.6.1" -when = "2023-02-16" -user-id = 3726 -user-login = "cfallin" -user-name = "Chris Fallin" - -[[publisher.regalloc2]] -version = "0.7.0" -when = "2023-04-18" -user-id = 187138 -user-login = "elliottt" -user-name = "Trevor Elliott" - [[publisher.regalloc2]] version = "0.8.1" when = "2023-05-01" @@ -43,6 +29,27 @@ user-id = 3726 user-login = "cfallin" user-name = "Chris Fallin" +[[publisher.unicode-segmentation]] +version = "1.10.1" +when = "2023-01-31" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[publisher.unicode-width]] +version = "0.1.9" +when = "2021-09-16" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[publisher.unicode-xid]] +version = "0.2.3" +when = "2022-05-02" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + [[publisher.wasm-encoder]] version = "0.26.0" when = "2023-04-27" @@ -50,6 +57,13 @@ user-id = 1 user-login = "alexcrichton" user-name = "Alex Crichton" +[[publisher.wasm-metadata]] +version = "0.5.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + [[publisher.wasm-mutate]] version = "0.2.23" when = "2023-04-13" @@ -71,6 +85,13 @@ user-id = 1 user-login = "alexcrichton" user-name = "Alex Crichton" +[[publisher.wasmparser]] +version = "0.104.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + [[publisher.wasmprinter]] version = "0.2.55" when = "2023-04-13" @@ -92,9 +113,51 @@ user-id = 1 user-login = "alexcrichton" user-name = "Alex Crichton" +[[publisher.wit-bindgen]] +version = "0.6.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wit-bindgen-core]] +version = "0.6.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wit-bindgen-rust]] +version = "0.6.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wit-bindgen-rust-lib]] +version = "0.6.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wit-bindgen-rust-macro]] +version = "0.6.0" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wit-component]] +version = "0.8.2" +when = "2023-04-27" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + [[publisher.wit-parser]] -version = "0.7.0" -when = "2023-04-13" +version = "0.7.1" +when = "2023-04-27" user-id = 1 user-login = "alexcrichton" user-name = "Alex Crichton" @@ -104,12 +167,6 @@ who = "Johan Andersson " criteria = "safe-to-deploy" version = "1.0.58" -[[audits.embark-studios.audits.anyhow]] -who = "Johan Andersson " -criteria = "safe-to-deploy" -delta = "1.0.58 -> 1.0.66" -notes = "New unsafe usage, looks sane. Expert maintainer" - [[audits.embark-studios.audits.cty]] who = "Johan Andersson " criteria = "safe-to-deploy" @@ -144,6 +201,12 @@ criteria = "safe-to-run" version = "0.6.2" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.proc-macro-error-attr]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.static_assertions]] who = "ChromeOS" criteria = "safe-to-run" @@ -181,6 +244,64 @@ who = "David Cook " criteria = "safe-to-deploy" version = "0.2.83" +[[audits.mozilla.wildcard-audits.unicode-segmentation]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 +start = "2019-05-15" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-width]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 +start = "2019-12-05" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-xid]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 +start = "2019-07-25" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.57 -> 1.0.61" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +delta = "1.0.58 -> 1.0.57" +notes = "No functional differences, just CI config and docs." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.61 -> 1.0.62" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.62 -> 1.0.68" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.anyhow]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.68 -> 1.0.69" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.autocfg]] who = "Josh Stone " criteria = "safe-to-deploy" @@ -366,6 +487,54 @@ criteria = "safe-to-deploy" delta = "1.13.1 -> 1.16.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.proc-macro2]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "1.0.39" +notes = """ +`proc-macro2` acts as either a thin(-ish) wrapper around the std-provided +`proc_macro` crate, or as a fallback implementation of the crate, depending on +where it is used. + +If using this crate on older versions of rustc (1.56 and earlier), it will +temporarily replace the panic handler while initializing in order to detect if +it is running within a `proc_macro`, which could lead to surprising behaviour. +This should not be an issue for more recent compiler versions, which support +`proc_macro::is_available()`. + +The `proc-macro2` crate's fallback behaviour is not identical to the complex +behaviour of the rustc compiler (e.g. it does not perform unicode normalization +for identifiers), however it behaves well enough for its intended use-case +(tests and scripts processing rust code). + +`proc-macro2` does not use unsafe code, however exposes one `unsafe` API to +allow bypassing checks in the fallback implementation when constructing +`Literal` using `from_str_unchecked`. This was intended to only be used by the +`quote!` macro, however it has been removed +(https://github.com/dtolnay/quote/commit/f621fe64a8a501cae8e95ebd6848e637bbc79078), +and is likely completely unused. Even when used, this API shouldn't be able to +cause unsoundness. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.39 -> 1.0.43" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.43 -> 1.0.49" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.49 -> 1.0.51" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.quote]] who = "Nika Layzell " criteria = "safe-to-deploy" @@ -382,6 +551,18 @@ formatter, and runtime logic. """ aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.quote]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.18 -> 1.0.21" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.21 -> 1.0.23" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.rayon]] who = "Josh Stone " criteria = "safe-to-deploy"