diff --git a/.github/workflows/wash.yml b/.github/workflows/wash.yml index 0fefaeed..738278ba 100644 --- a/.github/workflows/wash.yml +++ b/.github/workflows/wash.yml @@ -38,6 +38,21 @@ jobs: - macos-latest - windows-latest steps: + - name: Remove unused files + if: ${{ matrix.os == 'ubuntu-latest' }} + # based on https://dev.to/mathio/squeezing-disk-space-from-github-actions-runners-an-engineers-guide-3pjg + run: | + sudo rm -rf /usr/lib/jvm + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/share/swift + sudo rm -rf /usr/local/.ghcup + sudo rm -rf /usr/local/julia* + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/local/share/chromium + sudo rm -rf /opt/microsoft /opt/google + sudo rm -rf /opt/az + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /opt/hostedtoolcache - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: diff --git a/Cargo.lock b/Cargo.lock index c1f36138..da032882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,6 +642,15 @@ dependencies = [ "futures", ] +[[package]] +name = "camino" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] + [[package]] name = "cap-fs-ext" version = "3.4.4" @@ -720,6 +729,29 @@ dependencies = [ "winx", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.16", +] + [[package]] name = "cbc" version = "0.1.2" @@ -1054,8 +1086,7 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c088d3406f0c0252efa7445adfd2d05736bfb5218838f64eaf79d567077aed14" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-assembler-x64-meta", ] @@ -1063,8 +1094,7 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64-meta" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c03f887a763abb9c1dc08f722aa82b69067fda623b6f0273050f45f8b1a6776" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-srcgen", ] @@ -1072,8 +1102,7 @@ dependencies = [ [[package]] name = "cranelift-bforest" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206887a11a43f507fee320a218dc365980bfc42ec2696792079a9f8c9369e90" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-entity", ] @@ -1081,8 +1110,7 @@ dependencies = [ [[package]] name = "cranelift-bitset" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0790c83cfdab95709c5d0105fd888221e3af9049a7d7ec376ec901ab4e4dba" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "serde", "serde_derive", @@ -1091,8 +1119,7 @@ dependencies = [ [[package]] name = "cranelift-codegen" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a98aed2d262eda69310e84bae8e053ee4f17dbdd3347b8d9156aa618ba2de0a" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1106,10 +1133,13 @@ dependencies = [ "gimli 0.32.3", "hashbrown 0.15.5", "log", + "postcard", "pulley-interpreter", "regalloc2", "rustc-hash 2.1.1", "serde", + "serde_derive", + "sha2", "smallvec", "target-lexicon", "wasmtime-internal-math", @@ -1118,8 +1148,7 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6906852826988563e9b0a9232ad951f53a47aa41ffd02f8ac852d3f41aae836a" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1131,14 +1160,12 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a50105aab667b5cc845f2be37c78475d7cc127cd8ec0a31f7b2b71d526099a7" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" [[package]] name = "cranelift-control" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6adcc7aa7c0bc1727176a6f2d99c28a9e79a541ccd5ca911a0cb352da8befa36" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "arbitrary", ] @@ -1146,8 +1173,7 @@ dependencies = [ [[package]] name = "cranelift-entity" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981b56af777f9a34ea6dcce93255125776d391410c2a68b75bed5941b714fa15" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-bitset", "serde", @@ -1157,8 +1183,7 @@ dependencies = [ [[package]] name = "cranelift-frontend" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea982589684dfb71afecb9fc09555c3a266300a1162a60d7fa39d41a5705b1c" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-codegen", "log", @@ -1169,14 +1194,12 @@ dependencies = [ [[package]] name = "cranelift-isle" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0422686b22ed6a1f33cc40e3c43eb84b67155788568d1a5cac8439d3dca1783" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" [[package]] name = "cranelift-native" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f697bbbe135c655ea1deb7af0bae4a5c4fae2c88fdfc0fa57b34ae58c91040" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-codegen", "libc", @@ -1186,8 +1209,7 @@ dependencies = [ [[package]] name = "cranelift-srcgen" version = "0.125.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718efe674f3df645462677e22a3128e890d88ba55821bb091083d257707be76c" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" [[package]] name = "crc32fast" @@ -1663,6 +1685,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2811,6 +2856,30 @@ dependencies = [ "cc", ] +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -4009,6 +4078,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4314,8 +4392,7 @@ dependencies = [ [[package]] name = "pulley-interpreter" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beafc309a2d35e16cc390644d88d14dfa45e45e15075ec6a9e37f6dfb43e926f" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cranelift-bitset", "log", @@ -4326,8 +4403,7 @@ dependencies = [ [[package]] name = "pulley-macros" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885fbb6c07454cfc8725a18a1da3cfc328ee8c53fb8d0671ea313edc8567947" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "proc-macro2", "quote", @@ -4568,6 +4644,7 @@ dependencies = [ "hashbrown 0.15.5", "log", "rustc-hash 2.1.1", + "serde", "smallvec", ] @@ -5490,6 +5567,42 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "test-log" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" +dependencies = [ + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-programs-artifacts" +version = "0.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" +dependencies = [ + "cargo_metadata", + "heck 0.5.0", + "serde", + "serde_derive", + "wasmtime", + "wasmtime-test-util", + "wat", + "wit-component 0.239.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6300,11 +6413,11 @@ dependencies = [ "url", "uuid", "wash-runtime", + "wash-wasi", "wasm-metadata 0.239.0", "wasm-pkg-client", "wasm-pkg-core", "wasmtime", - "wasmtime-wasi", "wat", "which", "wit-component 0.235.0", @@ -6352,16 +6465,50 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "wash-wasi", "wasi-graphics-context-wasmtime", "wasi-webgpu-wasmtime", "wasmtime", - "wasmtime-wasi", "wasmtime-wasi-http", "wasmtime-wasi-io", "wat", "wit-component 0.235.0", ] +[[package]] +name = "wash-wasi" +version = "2.0.0-rc.5" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.9.3", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "env_logger", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.0.8", + "system-interface", + "tempfile", + "test-log", + "test-programs-artifacts", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "wasmtime", + "wasmtime-test-util", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.60.2", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -6804,17 +6951,18 @@ dependencies = [ [[package]] name = "wasmtime" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81eafc07c867be94c47e0dc66355d9785e09107a18901f76a20701ba0663ad7" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "addr2line 0.25.1", "anyhow", "async-trait", "bitflags 2.9.3", "bumpalo", + "bytes", "cc", "cfg-if", "encoding_rs", + "futures", "fxprof-processed-profile", "gimli 0.32.3", "hashbrown 0.15.5", @@ -6858,8 +7006,7 @@ dependencies = [ [[package]] name = "wasmtime-environ" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78587abe085a44a13c90fa16fea6db014e9883e627a7044d7f0cb397ad08d1da" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cpp_demangle", @@ -6885,8 +7032,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fb9299e318b0af3efb75d88321515a20a5ccb040bcde1f0f7d46d656fa8fef" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "base64 0.22.1", @@ -6905,8 +7051,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d843bb444f2d1509ea9304ad749242d1fa5de95cde67665bfcdcafa0f360925c" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "proc-macro2", @@ -6920,14 +7065,12 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801ee1a80ab66f065a88c6a62f2d495d5540d027b366757c6a53e9c42f153aef" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" [[package]] name = "wasmtime-internal-cranelift" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb50f1c50365c32e557266ca85acdf77696c44a3f98797ba6af58cebc6d6d1e" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cfg-if", @@ -6954,8 +7097,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9308cdb17f8d51e3164185616d809e28c29a6515c03b9dd95c89436b71f6d154" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cc", @@ -6969,8 +7111,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9b63a22bf2a8b6a149a41c6768bc17a8b2e3288a249cb8216987fbd7128e81" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "cc", "object 0.37.3", @@ -6981,8 +7122,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb8e042b6e3de2f3d708279f89f50b4b9aa1b9bab177300cdffb0ffcd2816df5" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cfg-if", @@ -6993,8 +7133,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1f0674f38cd7d014eb1a49ea1d1766cca1a64459e8856ee118a10005302e16" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "libm", ] @@ -7002,14 +7141,12 @@ dependencies = [ [[package]] name = "wasmtime-internal-slab" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb24b7535306713e7a250f8b71e35f05b6a5031bf9c3ed7330c308e899cbe7d3" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" [[package]] name = "wasmtime-internal-unwinder" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d5a80e2623a49cb8e8c419542337b8fe0260b162c40dcc201080a84cbe9b7c" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cfg-if", @@ -7021,8 +7158,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e277f734b9256359b21517c3b0c26a2a9de6c53a51b670ae55cdcde548bf4e" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "proc-macro2", "quote", @@ -7032,8 +7168,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4dc9333737142f6ece4369c8bcdda03a11edbd43d8fbd3e15004c194b9b743" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cranelift-codegen", @@ -7050,8 +7185,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f758625553fe33fdce0713f63bb7784c4f5fecb7f7cd4813414519ec24b6a4c" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "bitflags 2.9.3", @@ -7060,6 +7194,17 @@ dependencies = [ "wit-parser 0.239.0", ] +[[package]] +name = "wasmtime-test-util" +version = "38.0.4" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" +dependencies = [ + "anyhow", + "serde", + "serde_derive", + "toml", +] + [[package]] name = "wasmtime-wasi" version = "38.0.4" @@ -7402,8 +7547,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" version = "38.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c0bb17ae9bf89ebc74512150e6ee0a27b1eac5ff3b54d8cec264f4b4255022d" +source = "git+https://github.com/bytecodealliance/wasmtime?tag=v38.0.4#4c22e15bade0a8590d8272f5be85426cb134601b" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -7922,6 +8066,25 @@ dependencies = [ "wit-parser 0.235.0", ] +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags 2.9.3", + "indexmap 2.11.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.239.0", + "wasm-metadata 0.239.0", + "wasmparser 0.239.0", + "wit-parser 0.239.0", +] + [[package]] name = "wit-parser" version = "0.224.1" diff --git a/Cargo.toml b/Cargo.toml index dbdf7a93..f830fe42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,33 @@ [package] name = "wash" -version = "2.0.0-rc.5" categories = ["wasm", "command-line-utilities"] description = "The Wasm Shell (wash) for developing and publishing Wasm components" keywords = ["webassembly", "wasm", "component", "wash", "cli"] readme = "README.md" -authors = ["The wasmCloud Team"] -edition = "2024" license = "Apache-2.0" repository = "https://github.com/wasmcloud/wash" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +version.workspace = true [workspace] members = [ "crates/wash-runtime", ] +package.authors = ["The wasmCloud Team"] +package.edition = "2024" +package.rust-version = "1.91.0" +package.version = "2.0.0-rc.5" + +[workspace.lints.rust] +# Inherited from Wasmtime +unused_extern_crates = 'warn' +trivial_numeric_casts = 'warn' +unstable_features = 'warn' +unused_import_braces = 'warn' +unused-lifetimes = 'warn' +unused-macro-rules = 'warn' [lib] name = "wash" @@ -139,7 +153,7 @@ wasm-pkg-core = { version = "0.10.0", default-features = false } wasm-metadata = { version = "0.239.0", default-features = false, features = ["oci"] } wasmcloud = { path = "crates/wasmcloud", default-features = false } wasmtime = { version = "38", default-features = false } -wasmtime-wasi = { version = "38", default-features = false } +wasmtime-wasi = { version = "2.0.0-rc.5", package = "wash-wasi", path = "crates/wasi", default-features = false } wasmtime-wasi-io = { version = "38", default-features = false } wasmtime-wasi-http = { version = "38", default-features = false } which = { version = "6.0.3", default-features = false } @@ -147,6 +161,23 @@ wit-component = { version = "0.235.0", default-features = false } wash-runtime = { path = "crates/wash-runtime", default-features = false } wat = { version = "1.239.0", default-features = false } +# wasmtime-wasi deps +bitflags = "2.0" +cap-fs-ext = "3.4.4" +cap-net-ext = "3.4.4" +cap-rand = { version = "3.4.4", features = ["small_rng"] } +cap-std = "3.4.4" +cap-time-ext = "3.4.4" +env_logger = "0.11.5" +fs-set-times = "0.20.1" +io-extras = "0.18.1" +io-lifetimes = { version = "2.0.3", default-features = false } +rustix = "1.0.3" +system-interface = { version = "0.27.1", features = ["cap_std_impls"] } +test-log = { version = "0.2", default-features = false, features = ["trace"] } +wiggle = { version = "=38.0.4", default-features = false } +windows-sys = "0.60.0" + [build-dependencies] anyhow = { workspace = true, default-features = true } tonic-prost-build = { workspace = true, default-features = true } @@ -179,3 +210,7 @@ ignored = [ [package.metadata.binstall] pkg-url = "{ repo }/releases/download/{name}-v{version}/wash-{ target }{ binary-ext }" pkg-fmt = "bin" + +# this section is required for testing `wash-wasi`, it should be removed before release +[patch.crates-io] +wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", tag = "v38.0.4" } diff --git a/crates/wash-runtime/src/engine/ctx.rs b/crates/wash-runtime/src/engine/ctx.rs index 9f454801..6ed5e6b3 100644 --- a/crates/wash-runtime/src/engine/ctx.rs +++ b/crates/wash-runtime/src/engine/ctx.rs @@ -23,7 +23,7 @@ pub struct Ctx { /// The unique identifier for the workload this component belongs to pub workload_id: Arc, /// The resource table used to manage resources in the Wasmtime store. - pub table: wasmtime::component::ResourceTable, + pub table: ResourceTable, /// The WASI context used to provide WASI functionality to the components using this context. pub ctx: WasiCtx, /// The HTTP context used to provide HTTP functionality to the component. @@ -72,7 +72,7 @@ impl WasiView for Ctx { } impl wasmtime_wasi_io::IoView for Ctx { - fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { + fn table(&mut self) -> &mut ResourceTable { &mut self.table } } @@ -123,6 +123,7 @@ impl CtxBuilder { } } + /// Set a custom [WasiCtx] pub fn with_wasi_ctx(mut self, ctx: WasiCtx) -> Self { self.ctx = Some(ctx); self diff --git a/crates/wash-runtime/src/engine/mod.rs b/crates/wash-runtime/src/engine/mod.rs index 437869aa..8bc68ce6 100644 --- a/crates/wash-runtime/src/engine/mod.rs +++ b/crates/wash-runtime/src/engine/mod.rs @@ -42,11 +42,12 @@ use anyhow::{Context, bail}; use wasmtime::PoolingAllocationConfig; use wasmtime::component::{Component, Linker}; +use wasmtime_wasi::sockets::loopback; use crate::engine::ctx::Ctx; use crate::engine::workload::{UnresolvedWorkload, WorkloadComponent, WorkloadService}; use crate::types::{EmptyDirVolume, HostPathVolume, VolumeType, Workload}; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; pub mod ctx; mod value; @@ -144,9 +145,18 @@ impl Engine { validated_volumes.insert(v.name.clone(), host_path); } + let loopback = Arc::default(); + // Iniitalize service let service = if let Some(svc) = service { - match self.initialize_service(id.as_ref(), &name, &namespace, svc, &validated_volumes) { + match self.initialize_service( + id.as_ref(), + &name, + &namespace, + svc, + &validated_volumes, + Arc::clone(&loopback), + ) { Ok(handle) => { tracing::debug!("successfully initialized service component"); Some(handle) @@ -169,6 +179,7 @@ impl Engine { &namespace, component, &validated_volumes, + Arc::clone(&loopback), ) { Ok(handle) => { tracing::debug!("successfully initialized workload component"); @@ -198,6 +209,7 @@ impl Engine { workload_namespace: impl AsRef, service: crate::types::Service, validated_volumes: &std::collections::HashMap, + loopback: Arc>, ) -> anyhow::Result { // Create a wasmtime component from the bytes let wasmtime_component = Component::new(&self.inner, service.bytes) @@ -239,6 +251,7 @@ impl Engine { component_volume_mounts, service.local_resources, service.max_restarts, + loopback, )) } @@ -251,6 +264,7 @@ impl Engine { workload_namespace: impl AsRef, component: crate::types::Component, validated_volumes: &std::collections::HashMap, + loopback: Arc>, ) -> anyhow::Result { // Create a wasmtime component from the bytes let wasmtime_component = Component::new(&self.inner, component.bytes) @@ -292,6 +306,7 @@ impl Engine { linker, component_volume_mounts, component.local_resources, + loopback, // TODO: implement pooling and instance limits // component.pool_size, // component.max_invocations, diff --git a/crates/wash-runtime/src/engine/workload.rs b/crates/wash-runtime/src/engine/workload.rs index 6e156aa2..59656ab7 100644 --- a/crates/wash-runtime/src/engine/workload.rs +++ b/crates/wash-runtime/src/engine/workload.rs @@ -14,7 +14,9 @@ use tracing::{debug, info, trace, warn}; use wasmtime::component::{ Component, Instance, InstancePre, Linker, ResourceAny, ResourceType, Val, types::ComponentItem, }; -use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder, p2::bindings::CommandPre}; +use wasmtime_wasi::p2::bindings::CommandPre; +use wasmtime_wasi::sockets::{SocketAddrUse, loopback}; +use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder}; use crate::{ engine::{ @@ -55,6 +57,8 @@ pub struct WorkloadMetadata { local_resources: LocalResources, /// The plugins available to this component plugins: Option>>, + /// Workload loopback + loopback: Arc>, } impl WorkloadMetadata { @@ -228,6 +232,7 @@ impl WorkloadService { volume_mounts: Vec<(PathBuf, VolumeMount)>, local_resources: LocalResources, max_restarts: u64, + loopback: Arc>, ) -> Self { Self { metadata: WorkloadMetadata { @@ -240,6 +245,7 @@ impl WorkloadService { volume_mounts, local_resources, plugins: None, + loopback, }, handle: None, max_restarts, @@ -291,6 +297,7 @@ impl WorkloadComponent { linker: Linker, volume_mounts: Vec<(PathBuf, VolumeMount)>, local_resources: LocalResources, + loopback: Arc>, ) -> Self { Self { metadata: WorkloadMetadata { @@ -303,6 +310,7 @@ impl WorkloadComponent { volume_mounts, local_resources, plugins: None, + loopback, }, name: component_name.into(), // TODO: Implement pooling and instance limits @@ -418,7 +426,8 @@ impl ResolvedWorkload { // This will always be present since we just checked above, but we need this structure // to only borrow the service metadata let mut store = if let Some(service) = self.service.as_ref() { - self.new_store_from_metadata(&service.metadata).await? + self.new_store_from_metadata(&service.metadata, true) + .await? } else { bail!("service unexpectedly missing during execution"); }; @@ -896,13 +905,15 @@ impl ResolvedWorkload { let component = components .get(component_id) .context("component ID not found in workload")?; - self.new_store_from_metadata(&component.metadata).await + self.new_store_from_metadata(&component.metadata, false) + .await } /// Creates a new wasmtime Store from the given workload metadata. pub async fn new_store_from_metadata( &self, metadata: &WorkloadMetadata, + is_service: bool, ) -> anyhow::Result> { let components = self.components.read().await; @@ -918,6 +929,24 @@ impl ResolvedWorkload { .collect::>() .as_slice(), ) + .loopback_network(Arc::clone(&metadata.loopback)) + .socket_addr_check(move |addr, reason| { + Box::pin(async move { + match reason { + SocketAddrUse::TcpBind if is_service => { + addr.ip().is_loopback() || addr.ip().is_unspecified() + } + SocketAddrUse::TcpBind => false, + SocketAddrUse::UdpBind => { + // NOTE: Outbound UDP requires an explicit bind in `wasi:sockets` + addr.ip().is_loopback() || addr.ip().is_unspecified() + } + SocketAddrUse::TcpConnect + | SocketAddrUse::UdpConnect + | SocketAddrUse::UdpOutgoingDatagram => true, + } + }) + }) .inherit_stdout() .inherit_stderr(); @@ -1685,6 +1714,7 @@ mod tests { linker, Vec::new(), local_resources, + Arc::default(), ) } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml new file mode 100644 index 00000000..d7dea161 --- /dev/null +++ b/crates/wasi/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "wash-wasi" +version.workspace = true +authors = [ + "The Wasmtime Project Developers", + "The wasmCloud Team" +] +description = "WASI implementation in Rust" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/wasmcloud/wash" +readme = "README.md" +edition.workspace = true +rust-version.workspace = true +include = ["src/**/*", "README.md", "LICENSE", "witx/*", "wit/**/*", "tests/*"] + +[lints] +workspace = true + +[dependencies] +wasmtime = { workspace = true, features = ["runtime", "std"] } +wasmtime-wasi-io = { workspace = true, features = ["std"] } +anyhow = { workspace = true } +wiggle = { workspace = true, optional = true, features = ["wasmtime"] } +tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] } +bytes = { workspace = true } +tracing = { workspace = true } +cap-std = { workspace = true } +cap-rand = { workspace = true } +cap-fs-ext = { workspace = true } +cap-net-ext = { workspace = true } +cap-time-ext = { workspace = true } +io-lifetimes = { workspace = true } +fs-set-times = { workspace = true } +bitflags = { workspace = true } +async-trait = { workspace = true } +system-interface = { workspace = true} +futures = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros", "fs"] } +test-log = { workspace = true } +tracing-subscriber = { workspace = true } +test-programs-artifacts = { git = "https://github.com/bytecodealliance/wasmtime", tag = "v38.0.4" } +tempfile = { workspace = true } +wasmtime = { workspace = true, features = ['cranelift', 'incremental-cache'] } +wasmtime-test-util = { git = "https://github.com/bytecodealliance/wasmtime", tag = "v38.0.4" } +env_logger = { workspace = true } + +[target.'cfg(unix)'.dependencies] +rustix = { workspace = true, features = ["event", "fs", "net"] } + +[target.'cfg(windows)'.dependencies] +io-extras = { workspace = true } +windows-sys = { workspace = true } +rustix = { workspace = true, features = ["event", "net"] } + +[features] +default = ["p1", "p2"] +p0 = ["p1"] +p1 = ["dep:wiggle", "p2"] +p2 = ["wasmtime/component-model", "wasmtime/async"] +p3 = [ + "wasmtime/component-model-async", + "wasmtime/component-model-async-bytes", +] + +[[test]] +name = "process_stdin" +harness = false diff --git a/crates/wasi/LICENSE b/crates/wasi/LICENSE new file mode 100644 index 00000000..f9d81955 --- /dev/null +++ b/crates/wasi/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi/README.md b/crates/wasi/README.md new file mode 100644 index 00000000..d8d8203f --- /dev/null +++ b/crates/wasi/README.md @@ -0,0 +1,2 @@ +Crate defining the `Wasi` type for Wasmtime, which represents a WASI +instance which may be added to a linker. diff --git a/crates/wasi/src/cli.rs b/crates/wasi/src/cli.rs new file mode 100644 index 00000000..b3b93ed1 --- /dev/null +++ b/crates/wasi/src/cli.rs @@ -0,0 +1,369 @@ +use crate::p2; +use std::pin::Pin; +use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncWrite, empty}; +use wasmtime::component::{HasData, ResourceTable}; +use wasmtime_wasi_io::streams::{InputStream, OutputStream}; + +mod empty; +mod file; +mod locked_async; +mod mem; +mod stdout; +mod worker_thread_stdin; + +pub use self::file::{InputFile, OutputFile}; +pub use self::locked_async::{AsyncStdinStream, AsyncStdoutStream}; + +// Convenience reexport for stdio types so tokio doesn't have to be imported +// itself. +#[doc(no_inline)] +pub use tokio::io::{Stderr, Stdin, Stdout, stderr, stdin, stdout}; + +/// A helper struct which implements [`HasData`] for the `wasi:cli` APIs. +/// +/// This can be useful when directly calling `add_to_linker` functions directly, +/// such as [`wash_wasi::p2::bindings::cli::environment::add_to_linker`] as +/// the `D` type parameter. See [`HasData`] for more information about the type +/// parameter's purpose. +/// +/// When using this type you can skip the [`WasiCliView`] trait, for +/// example. +/// +/// # Examples +/// +/// ``` +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime::{Engine, Result, Config}; +/// use wash_wasi::cli::*; +/// +/// struct MyStoreState { +/// table: ResourceTable, +/// cli: WasiCliCtx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// let mut linker = Linker::new(&engine); +/// +/// wash_wasi::p2::bindings::cli::environment::add_to_linker::( +/// &mut linker, +/// |state| WasiCliCtxView { +/// table: &mut state.table, +/// ctx: &mut state.cli, +/// }, +/// )?; +/// Ok(()) +/// } +/// ``` +pub struct WasiCli; + +impl HasData for WasiCli { + type Data<'a> = WasiCliCtxView<'a>; +} + +/// Provides a "view" of `wasi:cli`-related context used to implement host +/// traits. +pub trait WasiCliView: Send { + fn cli(&mut self) -> WasiCliCtxView<'_>; +} + +pub struct WasiCliCtxView<'a> { + pub ctx: &'a mut WasiCliCtx, + pub table: &'a mut ResourceTable, +} + +pub struct WasiCliCtx { + pub(crate) environment: Vec<(String, String)>, + pub(crate) arguments: Vec, + pub(crate) initial_cwd: Option, + pub(crate) stdin: Box, + pub(crate) stdout: Box, + pub(crate) stderr: Box, +} + +impl Default for WasiCliCtx { + fn default() -> WasiCliCtx { + WasiCliCtx { + environment: Vec::new(), + arguments: Vec::new(), + initial_cwd: None, + stdin: Box::new(empty()), + stdout: Box::new(empty()), + stderr: Box::new(empty()), + } + } +} + +pub trait IsTerminal { + /// Returns whether this stream is backed by a TTY. + fn is_terminal(&self) -> bool; +} + +/// A trait used to represent the standard input to a guest program. +/// +/// Note that there are many built-in implementations of this trait for various +/// types such as [`tokio::io::Stdin`], [`tokio::io::Empty`], and +/// [`p2::pipe::MemoryInputPipe`]. +pub trait StdinStream: IsTerminal + Send { + /// Creates a fresh stream which is reading stdin. + /// + /// Note that the returned stream must share state with all other streams + /// previously created. Guests may create multiple handles to the same stdin + /// and they should all be synchronized in their progress through the + /// program's input. + /// + /// Note that this means that if one handle becomes ready for reading they + /// all become ready for reading. Subsequently if one is read from it may + /// mean that all the others are no longer ready for reading. This is + /// basically a consequence of the way the WIT APIs are designed today. + fn async_stream(&self) -> Box; + + /// Same as [`Self::async_stream`] except that a WASIp2 [`InputStream`] is + /// returned. + /// + /// Note that this has a default implementation which uses + /// [`p2::pipe::AsyncReadStream`] as an adapter, but this can be overridden + /// if there's a more specialized implementation available. + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::AsyncReadStream::new(Pin::from( + self.async_stream(), + ))) + } +} + +/// Similar to [`StdinStream`], except for output. +/// +/// This is used both for a guest stdin and a guest stdout. +/// +/// Note that there are many built-in implementations of this trait for various +/// types such as [`tokio::io::Stdout`], [`tokio::io::Empty`], and +/// [`p2::pipe::MemoryOutputPipe`]. +pub trait StdoutStream: IsTerminal + Send { + /// Returns a fresh new stream which can write to this output stream. + /// + /// Note that all output streams should output to the same logical source. + /// This means that it's possible for each independent stream to acquire a + /// separate "permit" to write and then act on that permit. Note that + /// additionally at this time once a permit is "acquired" there's no way to + /// release it, for example you can wait for readiness and then never + /// actually write in WASI. This means that acquisition of a permit for one + /// stream cannot discount the size of a permit another stream could + /// obtain. + /// + /// Implementations must be able to handle this + fn async_stream(&self) -> Box; + + /// Same as [`Self::async_stream`] except that a WASIp2 [`OutputStream`] is + /// returned. + /// + /// Note that this has a default implementation which uses + /// [`p2::pipe::AsyncWriteStream`] as an adapter, but this can be overridden + /// if there's a more specialized implementation available. + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::AsyncWriteStream::new( + 8192, // FIXME: extract this to a constant. + Pin::from(self.async_stream()), + )) + } +} + +// Forward `&T => T` +impl IsTerminal for &T { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} +impl StdinStream for &T { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} +impl StdoutStream for &T { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} + +// Forward `&mut T => T` +impl IsTerminal for &mut T { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} +impl StdinStream for &mut T { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} +impl StdoutStream for &mut T { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} + +// Forward `Box => T` +impl IsTerminal for Box { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} +impl StdinStream for Box { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} +impl StdoutStream for Box { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} + +// Forward `Arc => T` +impl IsTerminal for Arc { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} +impl StdinStream for Arc { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} +impl StdoutStream for Arc { + fn p2_stream(&self) -> Box { + T::p2_stream(self) + } + fn async_stream(&self) -> Box { + T::async_stream(self) + } +} + +#[cfg(test)] +mod test { + use crate::cli::{AsyncStdoutStream, StdinStream, StdoutStream}; + use crate::p2::{self, OutputStream}; + use anyhow::Result; + use bytes::Bytes; + use tokio::io::AsyncReadExt; + + #[test] + fn memory_stdin_stream() { + // A StdinStream has the property that there are multiple + // InputStreams created, using the stream() method which are each + // views on the same shared state underneath. Consuming input on one + // stream results in consuming that input on all streams. + // + // The simplest way to measure this is to check if the MemoryInputPipe + // impl of StdinStream follows this property. + + let pipe = + p2::pipe::MemoryInputPipe::new("the quick brown fox jumped over the three lazy dogs"); + + let mut view1 = pipe.p2_stream(); + let mut view2 = pipe.p2_stream(); + + let read1 = view1.read(10).expect("read first 10 bytes"); + assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes"); + let read2 = view2.read(10).expect("read second 10 bytes"); + assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes"); + let read3 = view1.read(10).expect("read third 10 bytes"); + assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes"); + let read4 = view2.read(10).expect("read fourth 10 bytes"); + assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes"); + } + + #[tokio::test] + async fn async_stdin_stream() { + // A StdinStream has the property that there are multiple + // InputStreams created, using the stream() method which are each + // views on the same shared state underneath. Consuming input on one + // stream results in consuming that input on all streams. + // + // AsyncStdinStream is a slightly more complex impl of StdinStream + // than the MemoryInputPipe above. We can create an AsyncReadStream + // from a file on the disk, and an AsyncStdinStream from that common + // stream, then check that the same property holds as above. + + let dir = tempfile::tempdir().unwrap(); + let mut path = std::path::PathBuf::from(dir.path()); + path.push("file"); + std::fs::write(&path, "the quick brown fox jumped over the three lazy dogs").unwrap(); + + let file = tokio::fs::File::open(&path) + .await + .expect("open created file"); + let stdin_stream = super::AsyncStdinStream::new(file); + + use super::StdinStream; + + let mut view1 = stdin_stream.p2_stream(); + let mut view2 = stdin_stream.p2_stream(); + + view1.ready().await; + + let read1 = view1.read(10).expect("read first 10 bytes"); + assert_eq!(read1, "the quick ".as_bytes(), "first 10 bytes"); + let read2 = view2.read(10).expect("read second 10 bytes"); + assert_eq!(read2, "brown fox ".as_bytes(), "second 10 bytes"); + let read3 = view1.read(10).expect("read third 10 bytes"); + assert_eq!(read3, "jumped ove".as_bytes(), "third 10 bytes"); + let read4 = view2.read(10).expect("read fourth 10 bytes"); + assert_eq!(read4, "r the thre".as_bytes(), "fourth 10 bytes"); + } + + #[tokio::test] + async fn async_stdout_stream_unblocks() { + let (mut read, write) = tokio::io::duplex(32); + let stdout = AsyncStdoutStream::new(32, write); + + let task = tokio::task::spawn(async move { + let mut stream = stdout.p2_stream(); + blocking_write_and_flush(&mut *stream, "x".into()) + .await + .unwrap(); + }); + + let mut buf = [0; 100]; + let n = read.read(&mut buf).await.unwrap(); + assert_eq!(&buf[..n], b"x"); + + task.await.unwrap(); + } + + async fn blocking_write_and_flush(s: &mut dyn OutputStream, mut bytes: Bytes) -> Result<()> { + while !bytes.is_empty() { + let permit = s.write_ready().await?; + let len = bytes.len().min(permit); + let chunk = bytes.split_to(len); + s.write(chunk)?; + } + + s.flush()?; + s.write_ready().await?; + Ok(()) + } +} diff --git a/crates/wasi/src/cli/empty.rs b/crates/wasi/src/cli/empty.rs new file mode 100644 index 00000000..98f134f7 --- /dev/null +++ b/crates/wasi/src/cli/empty.rs @@ -0,0 +1,115 @@ +use crate::cli::{IsTerminal, StdinStream, StdoutStream}; +use crate::p2; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{self, AsyncRead, AsyncWrite}; +use wasmtime_wasi_io::streams::{InputStream, OutputStream}; + +// Implementation for tokio::io::Empty +impl IsTerminal for tokio::io::Empty { + fn is_terminal(&self) -> bool { + false + } +} +impl StdinStream for tokio::io::Empty { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::ClosedInputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} +impl StdoutStream for tokio::io::Empty { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::SinkOutputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} + +// Implementation for std::io::Empty +impl IsTerminal for std::io::Empty { + fn is_terminal(&self) -> bool { + false + } +} +impl StdinStream for std::io::Empty { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::ClosedInputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} +impl StdoutStream for std::io::Empty { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::SinkOutputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} + +// Implementation for p2::pipe::ClosedInputStream +impl IsTerminal for p2::pipe::ClosedInputStream { + fn is_terminal(&self) -> bool { + false + } +} +impl StdinStream for p2::pipe::ClosedInputStream { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::ClosedInputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} + +// Implementation for p2::pipe::SinkOutputStream +impl IsTerminal for p2::pipe::SinkOutputStream { + fn is_terminal(&self) -> bool { + false + } +} +impl StdoutStream for p2::pipe::SinkOutputStream { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::SinkOutputStream) + } + fn async_stream(&self) -> Box { + Box::new(tokio::io::empty()) + } +} + +// Implementation for p2::pipe::ClosedOutputStream +impl IsTerminal for p2::pipe::ClosedOutputStream { + fn is_terminal(&self) -> bool { + false + } +} +impl StdoutStream for p2::pipe::ClosedOutputStream { + fn p2_stream(&self) -> Box { + Box::new(p2::pipe::ClosedOutputStream) + } + fn async_stream(&self) -> Box { + struct AlwaysClosed; + + impl AsyncWrite for AlwaysClosed { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &[u8], + ) -> Poll> { + Poll::Ready(Ok(0)) + } + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + } + + Box::new(AlwaysClosed) + } +} diff --git a/crates/wasi/src/cli/file.rs b/crates/wasi/src/cli/file.rs new file mode 100644 index 00000000..9cbd7026 --- /dev/null +++ b/crates/wasi/src/cli/file.rs @@ -0,0 +1,150 @@ +use crate::cli::{IsTerminal, StdinStream, StdoutStream}; +use crate::p2::{InputStream, OutputStream, Pollable, StreamError, StreamResult}; +use bytes::Bytes; +use std::io::{Read, Write}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::io::{self, AsyncRead, AsyncWrite}; + +/// This implementation will yield output streams that block on writes, and +/// output directly to a file. If truly async output is required, [`AsyncStdoutStream`] +/// should be used instead. +#[derive(Clone)] +pub struct OutputFile { + file: Arc, +} + +impl OutputFile { + pub fn new(file: std::fs::File) -> Self { + Self { + file: Arc::new(file), + } + } +} + +impl IsTerminal for OutputFile { + fn is_terminal(&self) -> bool { + false + } +} + +impl StdoutStream for OutputFile { + fn p2_stream(&self) -> Box { + Box::new(self.clone()) + } + + fn async_stream(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait::async_trait] +impl Pollable for OutputFile { + async fn ready(&mut self) {} +} + +impl OutputStream for OutputFile { + fn write(&mut self, bytes: Bytes) -> StreamResult<()> { + (&*self.file) + .write_all(&bytes) + .map_err(|e| StreamError::LastOperationFailed(anyhow::anyhow!(e))) + } + + fn flush(&mut self) -> StreamResult<()> { + use std::io::Write; + self.file + .flush() + .map_err(|e| StreamError::LastOperationFailed(anyhow::anyhow!(e))) + } + + fn check_write(&mut self) -> StreamResult { + Ok(1024 * 1024) + } +} + +impl AsyncWrite for OutputFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match (&*self.file).write_all(buf) { + Ok(()) => Poll::Ready(Ok(buf.len())), + Err(e) => Poll::Ready(Err(e)), + } + } + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready((&*self.file).flush()) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +/// This implementation will yield input streams that block on reads, and +/// reads directly from a file. If truly async input is required, +/// [`AsyncStdinStream`] should be used instead. +#[derive(Clone)] +pub struct InputFile { + file: Arc, +} + +impl InputFile { + pub fn new(file: std::fs::File) -> Self { + Self { + file: Arc::new(file), + } + } +} + +impl StdinStream for InputFile { + fn p2_stream(&self) -> Box { + Box::new(self.clone()) + } + fn async_stream(&self) -> Box { + Box::new(self.clone()) + } +} + +impl IsTerminal for InputFile { + fn is_terminal(&self) -> bool { + false + } +} + +#[async_trait::async_trait] +impl Pollable for InputFile { + async fn ready(&mut self) {} +} + +impl InputStream for InputFile { + fn read(&mut self, size: usize) -> StreamResult { + let mut buf = bytes::BytesMut::zeroed(size); + let bytes_read = self + .file + .read(&mut buf) + .map_err(|e| StreamError::LastOperationFailed(anyhow::anyhow!(e)))?; + if bytes_read == 0 { + return Err(StreamError::Closed); + } + buf.truncate(bytes_read); + StreamResult::Ok(buf.into()) + } +} + +impl AsyncRead for InputFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut io::ReadBuf<'_>, + ) -> Poll> { + match (&*self.file).read(buf.initialize_unfilled()) { + Ok(n) => { + buf.advance(n); + Poll::Ready(Ok(())) + } + Err(e) => Poll::Ready(Err(e)), + } + } +} diff --git a/crates/wasi/src/cli/locked_async.rs b/crates/wasi/src/cli/locked_async.rs new file mode 100644 index 00000000..acd8a2d3 --- /dev/null +++ b/crates/wasi/src/cli/locked_async.rs @@ -0,0 +1,351 @@ +use crate::cli::{IsTerminal, StdinStream, StdoutStream}; +use crate::p2; +use bytes::Bytes; +use std::mem; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, ready}; +use tokio::io::{self, AsyncRead, AsyncWrite}; +use tokio::sync::{Mutex, OwnedMutexGuard}; +use wasmtime_wasi_io::streams::{InputStream, OutputStream}; + +trait SharedHandleReady: Send + Sync + 'static { + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()>; +} + +impl SharedHandleReady for p2::pipe::AsyncWriteStream { + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { + ::poll_ready(self, cx) + } +} + +impl SharedHandleReady for p2::pipe::AsyncReadStream { + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { + ::poll_ready(self, cx) + } +} + +/// An impl of [`StdinStream`] built on top of [`AsyncRead`]. +// +// Note the usage of `tokio::sync::Mutex` here as opposed to a +// `std::sync::Mutex`. This is intentionally done to implement the `Pollable` +// variant of this trait. Note that in doing so we're left with the quandry of +// how to implement methods of `InputStream` since those methods are not +// `async`. They're currently implemented with `try_lock`, which then raises the +// question of what to do on contention. Currently traps are returned. +// +// Why should it be ok to return a trap? In general concurrency/contention +// shouldn't return a trap since it should be able to happen normally. The +// current assumption, though, is that WASI stdin/stdout streams are special +// enough that the contention case should never come up in practice. Currently +// in WASI there is no actually concurrency, there's just the items in a single +// `Store` and that store owns all of its I/O in a single Tokio task. There's no +// means to actually spawn multiple Tokio tasks that use the same store. This +// means at the very least that there's zero parallelism. Due to the lack of +// multiple tasks that also means that there's no concurrency either. +// +// This `AsyncStdinStream` wrapper is only intended to be used by the WASI +// bindings themselves. It's possible for the host to take this and work with it +// on its own task, but that's niche enough it's not designed for. +// +// Overall that means that the guest is either calling `Pollable` or +// `InputStream` methods. This means that there should never be contention +// between the two at this time. This may all change in the future with WASI +// 0.3, but perhaps we'll have a better story for stdio at that time (see the +// doc block on the `OutputStream` impl below) +pub struct AsyncStdinStream(Arc>); + +impl AsyncStdinStream { + pub fn new(s: impl AsyncRead + Send + Sync + 'static) -> Self { + Self(Arc::new(Mutex::new(p2::pipe::AsyncReadStream::new(s)))) + } +} + +impl StdinStream for AsyncStdinStream { + fn p2_stream(&self) -> Box { + Box::new(Self(self.0.clone())) + } + fn async_stream(&self) -> Box { + Box::new(StdioHandle::Ready(self.0.clone())) + } +} + +impl IsTerminal for AsyncStdinStream { + fn is_terminal(&self) -> bool { + false + } +} + +#[async_trait::async_trait] +impl InputStream for AsyncStdinStream { + fn read(&mut self, size: usize) -> Result { + match self.0.try_lock() { + Ok(mut stream) => stream.read(size), + Err(_) => Err(p2::StreamError::trap("concurrent reads are not supported")), + } + } + fn skip(&mut self, size: usize) -> Result { + match self.0.try_lock() { + Ok(mut stream) => stream.skip(size), + Err(_) => Err(p2::StreamError::trap("concurrent skips are not supported")), + } + } + async fn cancel(&mut self) { + // Cancel the inner stream if we're the last reference to it: + if let Some(mutex) = Arc::get_mut(&mut self.0) { + match mutex.try_lock() { + Ok(mut stream) => stream.cancel().await, + Err(_) => {} + } + } + } +} + +#[async_trait::async_trait] +impl p2::Pollable for AsyncStdinStream { + async fn ready(&mut self) { + self.0.lock().await.ready().await + } +} + +impl AsyncRead for StdioHandle { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut io::ReadBuf<'_>, + ) -> Poll> { + match ready!(self.as_mut().poll(cx, |g| g.read(buf.remaining()))) { + Some(Ok(bytes)) => { + buf.put_slice(&bytes); + Poll::Ready(Ok(())) + } + Some(Err(e)) => Poll::Ready(Err(e)), + // If the guard can't be acquired that means that this stream is + // closed, so return that we're ready without filling in data. + None => Poll::Ready(Ok(())), + } + } +} + +/// A wrapper of [`crate::p2::pipe::AsyncWriteStream`] that implements +/// [`StdoutStream`]. Note that the [`OutputStream`] impl for this is not +/// correct when used for interleaved async IO. +// +// Note that the use of `tokio::sync::Mutex` here is intentional, in addition to +// the `try_lock()` calls below in the implementation of `OutputStream`. For +// more information see the documentation on `AsyncStdinStream`. +pub struct AsyncStdoutStream(Arc>); + +impl AsyncStdoutStream { + pub fn new(budget: usize, s: impl AsyncWrite + Send + Sync + 'static) -> Self { + Self(Arc::new(Mutex::new(p2::pipe::AsyncWriteStream::new( + budget, s, + )))) + } +} + +impl StdoutStream for AsyncStdoutStream { + fn p2_stream(&self) -> Box { + Box::new(Self(self.0.clone())) + } + fn async_stream(&self) -> Box { + Box::new(StdioHandle::Ready(self.0.clone())) + } +} + +impl IsTerminal for AsyncStdoutStream { + fn is_terminal(&self) -> bool { + false + } +} + +// This implementation is known to be bogus. All check-writes and writes are +// directed at the same underlying stream. The check-write/write protocol does +// require the size returned by a check-write to be accepted by write, even if +// other side-effects happen between those calls, and this implementation +// permits another view (created by StdoutStream::stream()) of the same +// underlying stream to accept a write which will invalidate a prior +// check-write of another view. +// Ultimately, the Std{in,out}Stream::stream() methods exist because many +// different places in a linked component (which may itself contain many +// modules) may need to access stdio without any coordination to keep those +// accesses all using pointing to the same resource. So, we allow many +// resources to be created. We have the reasonable expectation that programs +// won't attempt to interleave async IO from these disparate uses of stdio. +// If that expectation doesn't turn out to be true, and you find yourself at +// this comment to correct it: sorry about that. +#[async_trait::async_trait] +impl OutputStream for AsyncStdoutStream { + fn check_write(&mut self) -> Result { + match self.0.try_lock() { + Ok(mut stream) => stream.check_write(), + Err(_) => Err(p2::StreamError::trap("concurrent writes are not supported")), + } + } + fn write(&mut self, bytes: Bytes) -> Result<(), p2::StreamError> { + match self.0.try_lock() { + Ok(mut stream) => stream.write(bytes), + Err(_) => Err(p2::StreamError::trap("concurrent writes not supported yet")), + } + } + fn flush(&mut self) -> Result<(), p2::StreamError> { + match self.0.try_lock() { + Ok(mut stream) => stream.flush(), + Err(_) => Err(p2::StreamError::trap( + "concurrent flushes not supported yet", + )), + } + } + async fn cancel(&mut self) { + // Cancel the inner stream if we're the last reference to it: + if let Some(mutex) = Arc::get_mut(&mut self.0) { + match mutex.try_lock() { + Ok(mut stream) => stream.cancel().await, + Err(_) => {} + } + } + } +} + +#[async_trait::async_trait] +impl p2::Pollable for AsyncStdoutStream { + async fn ready(&mut self) { + self.0.lock().await.ready().await + } +} + +impl AsyncWrite for StdioHandle { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match ready!(self.poll(cx, |i| i.write(Bytes::copy_from_slice(buf)))) { + Some(Ok(())) => Poll::Ready(Ok(buf.len())), + Some(Err(e)) => Poll::Ready(Err(e)), + None => Poll::Ready(Ok(0)), + } + } + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match ready!(self.poll(cx, |i| i.flush())) { + Some(result) => Poll::Ready(result), + None => Poll::Ready(Ok(())), + } + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +/// State necessary for effectively transforming `Arc>` into `Async{Read,Write}`. +/// +/// This is a beast and inefficient. It should get the job done in theory but +/// one must truly ask oneself at some point "but at what cost". +/// +/// More seriously, it's unclear if this is the best way to transform a single +/// `AsyncRead` into a "multiple `AsyncRead`". This certainly is an attempt and +/// the hope is that everything here is private enough that we can refactor as +/// necessary in the future without causing much churn. +enum StdioHandle { + Ready(Arc>), + Locking(Box> + Send + Sync>), + Locked(OwnedMutexGuard), + Closed, +} + +impl StdioHandle +where + S: SharedHandleReady, +{ + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + op: impl FnOnce(&mut S) -> p2::StreamResult, + ) -> Poll>> { + // If we don't currently have the lock on this handle, initiate the + // lock acquisition. + if let StdioHandle::Ready(lock) = &*self { + self.set(StdioHandle::Locking(Box::new(lock.clone().lock_owned()))); + } + + // If we're in the process of locking this handle, wait for that to + // finish. + if let Some(lock) = self.as_mut().as_locking() { + let guard = ready!(lock.poll(cx)); + self.set(StdioHandle::Locked(guard)); + } + + let mut guard = match self.as_mut().take_guard() { + Some(guard) => guard, + // If the guard can't be acquired that means that this stream is + // closed, so return that we're ready without filling in data. + None => return Poll::Ready(None), + }; + + // Wait for our locked stream to be ready, resetting to the "locked" + // state if it's not quite ready yet. + match guard.poll_ready(cx) { + Poll::Ready(()) => {} + + // If the read isn't ready yet then restore our "locked" state + // since we haven't finished, then return pending. + Poll::Pending => { + self.set(StdioHandle::Locked(guard)); + return Poll::Pending; + } + } + + // Perform the I/O and delegate on the result. + match op(&mut guard) { + // The I/O succeeded so relinquish the lock on this stream by + // transitioning back to the "Ready" state. + Ok(result) => { + self.set(StdioHandle::Ready(OwnedMutexGuard::mutex(&guard).clone())); + Poll::Ready(Some(Ok(result))) + } + + // The stream is closed, and `take_guard` above already set the + // closed state, so return nothing indicating the closure. + Err(p2::StreamError::Closed) => Poll::Ready(None), + + // The stream failed so propagate the error. Errors should only + // come from the underlying I/O object and thus should cast + // successfully. Additionally `take_guard` replaced our state + // with "closed" above which is the desired state at this point. + Err(p2::StreamError::LastOperationFailed(e)) => { + Poll::Ready(Some(Err(e.downcast().unwrap()))) + } + + // Shouldn't be possible to produce a trap here. + Err(p2::StreamError::Trap(_)) => unreachable!(), + } + } + + fn as_locking( + self: Pin<&mut Self>, + ) -> Option>>> { + // SAFETY: this is a pin-projection from `self` into the `Locking` + // field. + unsafe { + match self.get_unchecked_mut() { + StdioHandle::Locking(future) => Some(Pin::new_unchecked(&mut **future)), + _ => None, + } + } + } + + fn take_guard(self: Pin<&mut Self>) -> Option> { + if !matches!(*self, StdioHandle::Locked(_)) { + return None; + } + // SAFETY: the `Locked` arm is safe to move as it's an invariant of this + // type that it's not pinned. + unsafe { + match mem::replace(self.get_unchecked_mut(), StdioHandle::Closed) { + StdioHandle::Locked(guard) => Some(guard), + _ => unreachable!(), + } + } + } +} diff --git a/crates/wasi/src/cli/mem.rs b/crates/wasi/src/cli/mem.rs new file mode 100644 index 00000000..5d71cb87 --- /dev/null +++ b/crates/wasi/src/cli/mem.rs @@ -0,0 +1,34 @@ +use crate::cli::{IsTerminal, StdinStream, StdoutStream}; +use crate::p2; +use tokio::io::{AsyncRead, AsyncWrite}; +use wasmtime_wasi_io::streams::{InputStream, OutputStream}; + +// Implementation for p2::pipe::MemoryInputPipe +impl IsTerminal for p2::pipe::MemoryInputPipe { + fn is_terminal(&self) -> bool { + false + } +} +impl StdinStream for p2::pipe::MemoryInputPipe { + fn p2_stream(&self) -> Box { + Box::new(self.clone()) + } + fn async_stream(&self) -> Box { + Box::new(self.clone()) + } +} + +// Implementation for p2::pipe::MemoryOutputPipe +impl IsTerminal for p2::pipe::MemoryOutputPipe { + fn is_terminal(&self) -> bool { + false + } +} +impl StdoutStream for p2::pipe::MemoryOutputPipe { + fn p2_stream(&self) -> Box { + Box::new(self.clone()) + } + fn async_stream(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/crates/wasi/src/cli/stdout.rs b/crates/wasi/src/cli/stdout.rs new file mode 100644 index 00000000..2626d161 --- /dev/null +++ b/crates/wasi/src/cli/stdout.rs @@ -0,0 +1,122 @@ +use crate::cli::{IsTerminal, StdoutStream}; +use crate::p2; +use bytes::Bytes; +use std::io::{self, Write}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::AsyncWrite; +use wasmtime_wasi_io::streams::OutputStream; + +// Implementation for tokio::io::Stdout +impl IsTerminal for tokio::io::Stdout { + fn is_terminal(&self) -> bool { + std::io::stdout().is_terminal() + } +} +impl StdoutStream for tokio::io::Stdout { + fn p2_stream(&self) -> Box { + Box::new(StdioOutputStream::Stdout) + } + fn async_stream(&self) -> Box { + Box::new(StdioOutputStream::Stdout) + } +} + +// Implementation for std::io::Stdout +impl IsTerminal for std::io::Stdout { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} +impl StdoutStream for std::io::Stdout { + fn p2_stream(&self) -> Box { + Box::new(StdioOutputStream::Stdout) + } + fn async_stream(&self) -> Box { + Box::new(StdioOutputStream::Stdout) + } +} + +// Implementation for tokio::io::Stderr +impl IsTerminal for tokio::io::Stderr { + fn is_terminal(&self) -> bool { + std::io::stderr().is_terminal() + } +} +impl StdoutStream for tokio::io::Stderr { + fn p2_stream(&self) -> Box { + Box::new(StdioOutputStream::Stderr) + } + fn async_stream(&self) -> Box { + Box::new(StdioOutputStream::Stderr) + } +} + +// Implementation for std::io::Stderr +impl IsTerminal for std::io::Stderr { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} +impl StdoutStream for std::io::Stderr { + fn p2_stream(&self) -> Box { + Box::new(StdioOutputStream::Stderr) + } + fn async_stream(&self) -> Box { + Box::new(StdioOutputStream::Stderr) + } +} + +enum StdioOutputStream { + Stdout, + Stderr, +} + +impl OutputStream for StdioOutputStream { + fn write(&mut self, bytes: Bytes) -> p2::StreamResult<()> { + match self { + StdioOutputStream::Stdout => std::io::stdout().write_all(&bytes), + StdioOutputStream::Stderr => std::io::stderr().write_all(&bytes), + } + .map_err(|e| p2::StreamError::LastOperationFailed(anyhow::anyhow!(e))) + } + + fn flush(&mut self) -> p2::StreamResult<()> { + match self { + StdioOutputStream::Stdout => std::io::stdout().flush(), + StdioOutputStream::Stderr => std::io::stderr().flush(), + } + .map_err(|e| p2::StreamError::LastOperationFailed(anyhow::anyhow!(e))) + } + + fn check_write(&mut self) -> p2::StreamResult { + Ok(1024 * 1024) + } +} + +impl AsyncWrite for StdioOutputStream { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(match *self { + StdioOutputStream::Stdout => std::io::stdout().write(buf), + StdioOutputStream::Stderr => std::io::stderr().write(buf), + }) + } + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(match *self { + StdioOutputStream::Stdout => std::io::stdout().flush(), + StdioOutputStream::Stderr => std::io::stderr().flush(), + }) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[async_trait::async_trait] +impl p2::Pollable for StdioOutputStream { + async fn ready(&mut self) {} +} diff --git a/crates/wasi/src/cli/worker_thread_stdin.rs b/crates/wasi/src/cli/worker_thread_stdin.rs new file mode 100644 index 00000000..10400cd1 --- /dev/null +++ b/crates/wasi/src/cli/worker_thread_stdin.rs @@ -0,0 +1,284 @@ +//! Handling for standard in using a worker task. +//! +//! Standard input is a global singleton resource for the entire program which +//! needs special care. Currently this implementation adheres to a few +//! constraints which make this nontrivial to implement. +//! +//! * Any number of guest wasm programs can read stdin. While this doesn't make +//! a ton of sense semantically they shouldn't block forever. Instead it's a +//! race to see who actually reads which parts of stdin. +//! +//! * Data from stdin isn't actually read unless requested. This is done to try +//! to be a good neighbor to others running in the process. Under the +//! assumption that most programs have one "thing" which reads stdin the +//! actual consumption of bytes is delayed until the wasm guest is dynamically +//! chosen to be that "thing". Before that data from stdin is not consumed to +//! avoid taking it from other components in the process. +//! +//! * Tokio's documentation indicates that "interactive stdin" is best done with +//! a helper thread to avoid blocking shutdown of the event loop. That's +//! respected here where all stdin reading happens on a blocking helper thread +//! that, at this time, is never shut down. +//! +//! This module is one that's likely to change over time though as new systems +//! are encountered along with preexisting bugs. + +use crate::cli::{IsTerminal, StdinStream}; +use bytes::{Bytes, BytesMut}; +use std::io::Read; +use std::mem; +use std::pin::Pin; +use std::sync::{Condvar, Mutex, OnceLock}; +use std::task::{Context, Poll}; +use tokio::io::{self, AsyncRead, ReadBuf}; +use tokio::sync::Notify; +use tokio::sync::futures::Notified; +use wasmtime_wasi_io::{ + poll::Pollable, + streams::{InputStream, StreamError}, +}; + +// Implementation for tokio::io::Stdin +impl IsTerminal for tokio::io::Stdin { + fn is_terminal(&self) -> bool { + std::io::stdin().is_terminal() + } +} +impl StdinStream for tokio::io::Stdin { + fn p2_stream(&self) -> Box { + Box::new(WasiStdin) + } + fn async_stream(&self) -> Box { + Box::new(WasiStdinAsyncRead::Ready) + } +} + +// Implementation for std::io::Stdin +impl IsTerminal for std::io::Stdin { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} +impl StdinStream for std::io::Stdin { + fn p2_stream(&self) -> Box { + Box::new(WasiStdin) + } + fn async_stream(&self) -> Box { + Box::new(WasiStdinAsyncRead::Ready) + } +} + +#[derive(Default)] +struct GlobalStdin { + state: Mutex, + read_requested: Condvar, + read_completed: Notify, +} + +#[derive(Default, Debug)] +enum StdinState { + #[default] + ReadNotRequested, + ReadRequested, + Data(BytesMut), + Error(std::io::Error), + Closed, +} + +impl GlobalStdin { + fn get() -> &'static GlobalStdin { + static STDIN: OnceLock = OnceLock::new(); + STDIN.get_or_init(|| create()) + } +} + +fn create() -> GlobalStdin { + std::thread::spawn(|| { + let state = GlobalStdin::get(); + loop { + // Wait for a read to be requested, but don't hold the lock across + // the blocking read. + let mut lock = state.state.lock().unwrap(); + lock = state + .read_requested + .wait_while(lock, |state| !matches!(state, StdinState::ReadRequested)) + .unwrap(); + drop(lock); + + let mut bytes = BytesMut::zeroed(1024); + let (new_state, done) = match std::io::stdin().read(&mut bytes) { + Ok(0) => (StdinState::Closed, true), + Ok(nbytes) => { + bytes.truncate(nbytes); + (StdinState::Data(bytes), false) + } + Err(e) => (StdinState::Error(e), true), + }; + + // After the blocking read completes the state should not have been + // tampered with. + debug_assert!(matches!( + *state.state.lock().unwrap(), + StdinState::ReadRequested + )); + *state.state.lock().unwrap() = new_state; + state.read_completed.notify_waiters(); + if done { + break; + } + } + }); + + GlobalStdin::default() +} + +struct WasiStdin; + +#[async_trait::async_trait] +impl InputStream for WasiStdin { + fn read(&mut self, size: usize) -> Result { + let g = GlobalStdin::get(); + let mut locked = g.state.lock().unwrap(); + match mem::replace(&mut *locked, StdinState::ReadRequested) { + StdinState::ReadNotRequested => { + g.read_requested.notify_one(); + Ok(Bytes::new()) + } + StdinState::ReadRequested => Ok(Bytes::new()), + StdinState::Data(mut data) => { + let size = data.len().min(size); + let bytes = data.split_to(size); + *locked = if data.is_empty() { + StdinState::ReadNotRequested + } else { + StdinState::Data(data) + }; + Ok(bytes.freeze()) + } + StdinState::Error(e) => { + *locked = StdinState::Closed; + Err(StreamError::LastOperationFailed(e.into())) + } + StdinState::Closed => { + *locked = StdinState::Closed; + Err(StreamError::Closed) + } + } + } +} + +#[async_trait::async_trait] +impl Pollable for WasiStdin { + async fn ready(&mut self) { + let g = GlobalStdin::get(); + + // Scope the synchronous `state.lock()` to this block which does not + // `.await` inside of it. + let notified = { + let mut locked = g.state.lock().unwrap(); + match *locked { + // If a read isn't requested yet + StdinState::ReadNotRequested => { + g.read_requested.notify_one(); + *locked = StdinState::ReadRequested; + g.read_completed.notified() + } + StdinState::ReadRequested => g.read_completed.notified(), + StdinState::Data(_) | StdinState::Closed | StdinState::Error(_) => return, + } + }; + + notified.await; + } +} + +enum WasiStdinAsyncRead { + Ready, + Waiting(Notified<'static>), +} + +impl AsyncRead for WasiStdinAsyncRead { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let g = GlobalStdin::get(); + + // Perform everything below in a `loop` to handle the case that a read + // was stolen by another thread, for example, or perhaps a spurious + // notification to `Notified`. + loop { + // If we were previously blocked on reading a "ready" notification, + // wait for that notification to complete. + if let Some(notified) = self.as_mut().notified_future() { + match notified.poll(cx) { + Poll::Ready(()) => self.set(WasiStdinAsyncRead::Ready), + Poll::Pending => break Poll::Pending, + } + } + + assert!(matches!(*self, WasiStdinAsyncRead::Ready)); + + // Once we're in the "ready" state then take a look at the global + // state of stdin. + let mut locked = g.state.lock().unwrap(); + match mem::replace(&mut *locked, StdinState::ReadRequested) { + // If data is available then drain what we can into `buf`. + StdinState::Data(mut data) => { + let size = data.len().min(buf.remaining()); + let bytes = data.split_to(size); + *locked = if data.is_empty() { + StdinState::ReadNotRequested + } else { + StdinState::Data(data) + }; + buf.put_slice(&bytes); + break Poll::Ready(Ok(())); + } + + // If stdin failed to be read then we fail with that error and + // transition to "closed" + StdinState::Error(e) => { + *locked = StdinState::Closed; + break Poll::Ready(Err(e)); + } + + // If stdin is closed, keep it closed. + StdinState::Closed => { + *locked = StdinState::Closed; + break Poll::Ready(Ok(())); + } + + // For these states we indicate that a read is requested, if it + // wasn't previously requested, and then we transition to + // `Waiting` below by falling through outside this `match`. + StdinState::ReadNotRequested => { + g.read_requested.notify_one(); + } + StdinState::ReadRequested => {} + } + + self.set(WasiStdinAsyncRead::Waiting(g.read_completed.notified())); + + // Intentionally drop the lock after the `notified()` future + // creation just above as to work correctly this needs to happen + // within the lock. + drop(locked); + } + } +} + +impl WasiStdinAsyncRead { + fn notified_future(self: Pin<&mut Self>) -> Option>> { + // SAFETY: this is a pin-projection from `self` to the field `Notified` + // internally. Given that `self` is pinned it should be safe to acquire + // a pinned version of the internal field. + unsafe { + match self.get_unchecked_mut() { + WasiStdinAsyncRead::Ready => None, + WasiStdinAsyncRead::Waiting(notified) => Some(Pin::new_unchecked(notified)), + } + } + } +} diff --git a/crates/wasi/src/clocks.rs b/crates/wasi/src/clocks.rs new file mode 100644 index 00000000..c1c31927 --- /dev/null +++ b/crates/wasi/src/clocks.rs @@ -0,0 +1,181 @@ +use cap_std::time::{Duration, Instant, SystemClock, SystemTime}; +use cap_std::{AmbientAuthority, ambient_authority}; +use cap_time_ext::{MonotonicClockExt as _, SystemClockExt as _}; +use wasmtime::component::{HasData, ResourceTable}; + +/// A helper struct which implements [`HasData`] for the `wasi:clocks` APIs. +/// +/// This can be useful when directly calling `add_to_linker` functions directly, +/// such as [`wash_wasi::p2::bindings::clocks::monotonic_clock::add_to_linker`] as +/// the `D` type parameter. See [`HasData`] for more information about the type +/// parameter's purpose. +/// +/// When using this type you can skip the [`WasiClocksView`] trait, for +/// example. +/// +/// # Examples +/// +/// ``` +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime::{Engine, Result, Config}; +/// use wash_wasi::clocks::*; +/// +/// struct MyStoreState { +/// table: ResourceTable, +/// clocks: WasiClocksCtx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// let mut linker = Linker::new(&engine); +/// +/// wash_wasi::p2::bindings::clocks::monotonic_clock::add_to_linker::( +/// &mut linker, +/// |state| WasiClocksCtxView { +/// table: &mut state.table, +/// ctx: &mut state.clocks, +/// }, +/// )?; +/// Ok(()) +/// } +/// ``` +pub struct WasiClocks; + +impl HasData for WasiClocks { + type Data<'a> = WasiClocksCtxView<'a>; +} + +pub struct WasiClocksCtx { + pub(crate) wall_clock: Box, + pub(crate) monotonic_clock: Box, +} + +impl Default for WasiClocksCtx { + fn default() -> Self { + Self { + wall_clock: wall_clock(), + monotonic_clock: monotonic_clock(), + } + } +} + +pub trait WasiClocksView: Send { + fn clocks(&mut self) -> WasiClocksCtxView<'_>; +} + +pub struct WasiClocksCtxView<'a> { + pub ctx: &'a mut WasiClocksCtx, + pub table: &'a mut ResourceTable, +} + +pub trait HostWallClock: Send { + fn resolution(&self) -> Duration; + fn now(&self) -> Duration; +} + +pub trait HostMonotonicClock: Send { + fn resolution(&self) -> u64; + fn now(&self) -> u64; +} + +pub struct WallClock { + /// The underlying system clock. + clock: cap_std::time::SystemClock, +} + +impl Default for WallClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl WallClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + Self { + clock: cap_std::time::SystemClock::new(ambient_authority), + } + } +} + +impl HostWallClock for WallClock { + fn resolution(&self) -> Duration { + self.clock.resolution() + } + + fn now(&self) -> Duration { + // WASI defines wall clocks to return "Unix time". + self.clock + .now() + .duration_since(SystemClock::UNIX_EPOCH) + .unwrap() + } +} + +pub struct MonotonicClock { + /// The underlying system clock. + clock: cap_std::time::MonotonicClock, + + /// The `Instant` this clock was created. All returned times are + /// durations since that time. + initial: Instant, +} + +impl Default for MonotonicClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl MonotonicClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + let clock = cap_std::time::MonotonicClock::new(ambient_authority); + let initial = clock.now(); + Self { clock, initial } + } +} + +impl HostMonotonicClock for MonotonicClock { + fn resolution(&self) -> u64 { + self.clock.resolution().as_nanos().try_into().unwrap() + } + + fn now(&self) -> u64 { + // Unwrap here and in `resolution` above; a `u64` is wide enough to + // hold over 584 years of nanoseconds. + self.clock + .now() + .duration_since(self.initial) + .as_nanos() + .try_into() + .unwrap() + } +} + +pub fn monotonic_clock() -> Box { + Box::new(MonotonicClock::default()) +} + +pub fn wall_clock() -> Box { + Box::new(WallClock::default()) +} + +pub(crate) struct Datetime { + pub seconds: u64, + pub nanoseconds: u32, +} + +impl TryFrom for Datetime { + type Error = wasmtime::Error; + + fn try_from(time: SystemTime) -> Result { + let duration = + time.duration_since(SystemTime::from_std(std::time::SystemTime::UNIX_EPOCH))?; + + Ok(Self { + seconds: duration.as_secs(), + nanoseconds: duration.subsec_nanos(), + }) + } +} diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs new file mode 100644 index 00000000..b2949743 --- /dev/null +++ b/crates/wasi/src/ctx.rs @@ -0,0 +1,565 @@ +use crate::cli::{StdinStream, StdoutStream, WasiCliCtx}; +use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; +use crate::filesystem::{Dir, WasiFilesystemCtx}; +use crate::random::WasiRandomCtx; +use crate::sockets::{SocketAddrCheck, SocketAddrUse, WasiSocketsCtx}; +use crate::{DirPerms, FilePerms, OpenMode}; +use anyhow::Result; +use cap_rand::RngCore; +use cap_std::ambient_authority; +use std::future::Future; +use std::mem; +use std::net::SocketAddr; +use std::path::Path; +use std::pin::Pin; +use tokio::io::{stderr, stdin, stdout}; + +/// Builder-style structure used to create a [`WasiCtx`]. +/// +/// This type is used to create a [`WasiCtx`] that is considered per-[`Store`] +/// state. The [`build`][WasiCtxBuilder::build] method is used to finish the +/// building process and produce a finalized [`WasiCtx`]. +/// +/// # Examples +/// +/// ``` +/// use wash_wasi::WasiCtx; +/// +/// let mut wasi = WasiCtx::builder(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// let wasi: WasiCtx = wasi.build(); +/// ``` +/// +/// [`Store`]: wasmtime::Store +#[derive(Default)] +pub struct WasiCtxBuilder { + cli: WasiCliCtx, + clocks: WasiClocksCtx, + filesystem: WasiFilesystemCtx, + random: WasiRandomCtx, + sockets: WasiSocketsCtx, + built: bool, +} + +impl WasiCtxBuilder { + /// Creates a builder for a new context with default parameters set. + /// + /// The current defaults are: + /// + /// * stdin is closed + /// * stdout and stderr eat all input and it doesn't go anywhere + /// * no env vars + /// * no arguments + /// * no preopens + /// * clocks use the host implementation of wall/monotonic clocks + /// * RNGs are all initialized with random state and suitable generator + /// quality to satisfy the requirements of WASI APIs. + /// * TCP/UDP are allowed but all addresses are denied by default. + /// * `wasi:sockets/ip-name-lookup` is denied by default. + /// + /// These defaults can all be updated via the various builder configuration + /// methods below. + pub fn new() -> Self { + Self::default() + } + + /// Provides a custom implementation of stdin to use. + /// + /// By default stdin is closed but an example of using the host's native + /// stdin looks like: + /// + /// ``` + /// use wash_wasi::WasiCtx; + /// use wash_wasi::cli::stdin; + /// + /// let mut wasi = WasiCtx::builder(); + /// wasi.stdin(stdin()); + /// ``` + /// + /// Note that inheriting the process's stdin can also be done through + /// [`inherit_stdin`](WasiCtxBuilder::inherit_stdin). + pub fn stdin(&mut self, stdin: impl StdinStream + 'static) -> &mut Self { + self.cli.stdin = Box::new(stdin); + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stdout. + pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) -> &mut Self { + self.cli.stdout = Box::new(stdout); + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stderr. + pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) -> &mut Self { + self.cli.stderr = Box::new(stderr); + self + } + + /// Configures this context's stdin stream to read the host process's + /// stdin. + /// + /// Note that concurrent reads of stdin can produce surprising results so + /// when using this it's typically best to have a single wasm instance in + /// the process using this. + pub fn inherit_stdin(&mut self) -> &mut Self { + self.stdin(stdin()) + } + + /// Configures this context's stdout stream to write to the host process's + /// stdout. + /// + /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) + /// multiple instances printing to stdout works well. + pub fn inherit_stdout(&mut self) -> &mut Self { + self.stdout(stdout()) + } + + /// Configures this context's stderr stream to write to the host process's + /// stderr. + /// + /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) + /// multiple instances printing to stderr works well. + pub fn inherit_stderr(&mut self) -> &mut Self { + self.stderr(stderr()) + } + + /// Configures all of stdin, stdout, and stderr to be inherited from the + /// host process. + /// + /// See [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) for some rationale + /// on why this should only be done in situations of + /// one-instance-per-process. + pub fn inherit_stdio(&mut self) -> &mut Self { + self.inherit_stdin().inherit_stdout().inherit_stderr() + } + + /// Configures whether or not blocking operations made through this + /// `WasiCtx` are allowed to block the current thread. + /// + /// WASI is currently implemented on top of the Rust + /// [Tokio](https://tokio.rs/) library. While most WASI APIs are + /// non-blocking some are instead blocking from the perspective of + /// WebAssembly. For example opening a file is a blocking operation with + /// respect to WebAssembly but it's implemented as an asynchronous operation + /// on the host. This is currently done with Tokio's + /// [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html). + /// + /// When WebAssembly is used in a synchronous context, for example when + /// [`Config::async_support`] is disabled, then this asynchronous operation + /// is quickly turned back into a synchronous operation with a `block_on` in + /// Rust. This switching back-and-forth between a blocking a non-blocking + /// context can have overhead, and this option exists to help alleviate this + /// overhead. + /// + /// This option indicates that for WASI functions that are blocking from the + /// perspective of WebAssembly it's ok to block the native thread as well. + /// This means that this back-and-forth between async and sync won't happen + /// and instead blocking operations are performed on-thread (such as opening + /// a file). This can improve the performance of WASI operations when async + /// support is disabled. + /// + /// [`Config::async_support`]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support + pub fn allow_blocking_current_thread(&mut self, enable: bool) -> &mut Self { + self.filesystem.allow_blocking_current_thread = enable; + self + } + + /// Appends multiple environment variables at once for this builder. + /// + /// All environment variables are appended to the list of environment + /// variables that this builder will configure. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + /// + /// # Examples + /// + /// ``` + /// use wash_wasi::WasiCtxBuilder; + /// + /// let mut wasi = WasiCtxBuilder::new(); + /// wasi.envs(&[ + /// ("FOO", "bar"), + /// ("HOME", "/somewhere"), + /// ]); + /// ``` + pub fn envs(&mut self, env: &[(impl AsRef, impl AsRef)]) -> &mut Self { + self.cli.environment.extend( + env.iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())), + ); + self + } + + /// Appends a single environment variable for this builder. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + /// + /// # Examples + /// + /// ``` + /// use wash_wasi::WasiCtxBuilder; + /// + /// let mut wasi = WasiCtxBuilder::new(); + /// wasi.env("FOO", "bar"); + /// ``` + pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { + self.cli + .environment + .push((k.as_ref().to_owned(), v.as_ref().to_owned())); + self + } + + /// Configures all environment variables to be inherited from the calling + /// process into this configuration. + /// + /// This will use [`envs`](WasiCtxBuilder::envs) to append all host-defined + /// environment variables. + pub fn inherit_env(&mut self) -> &mut Self { + self.envs(&std::env::vars().collect::>()) + } + + /// Appends a list of arguments to the argument array to pass to wasm. + pub fn args(&mut self, args: &[impl AsRef]) -> &mut Self { + self.cli + .arguments + .extend(args.iter().map(|a| a.as_ref().to_owned())); + self + } + + /// Appends a single argument to get passed to wasm. + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.cli.arguments.push(arg.as_ref().to_owned()); + self + } + + /// Appends all host process arguments to the list of arguments to get + /// passed to wasm. + pub fn inherit_args(&mut self) -> &mut Self { + self.args(&std::env::args().collect::>()) + } + + /// Configures a "preopened directory" to be available to WebAssembly. + /// + /// By default WebAssembly does not have access to the filesystem because + /// there are no preopened directories. All filesystem operations, such as + /// opening a file, are done through a preexisting handle. This means that + /// to provide WebAssembly access to a directory it must be configured + /// through this API. + /// + /// WASI will also prevent access outside of files provided here. For + /// example `..` can't be used to traverse up from the `host_path` provided here + /// to the containing directory. + /// + /// * `host_path` - a path to a directory on the host to open and make + /// accessible to WebAssembly. Note that the name of this directory in the + /// guest is configured with `guest_path` below. + /// * `guest_path` - the name of the preopened directory from WebAssembly's + /// perspective. Note that this does not need to match the host's name for + /// the directory. + /// * `dir_perms` - this is the permissions that wasm will have to operate on + /// `guest_path`. This can be used, for example, to provide readonly access to a + /// directory. + /// * `file_perms` - similar to `dir_perms` but corresponds to the maximum set + /// of permissions that can be used for any file in this directory. + /// + /// # Errors + /// + /// This method will return an error if `host_path` cannot be opened. + /// + /// # Examples + /// + /// ``` + /// use wash_wasi::WasiCtxBuilder; + /// use wash_wasi::{DirPerms, FilePerms}; + /// + /// # fn main() {} + /// # fn foo() -> wasmtime::Result<()> { + /// let mut wasi = WasiCtxBuilder::new(); + /// + /// // Make `./host-directory` available in the guest as `.` + /// wasi.preopened_dir("./host-directory", ".", DirPerms::all(), FilePerms::all()); + /// + /// // Make `./readonly` available in the guest as `./ro` + /// wasi.preopened_dir("./readonly", "./ro", DirPerms::READ, FilePerms::READ); + /// # Ok(()) + /// # } + /// ``` + pub fn preopened_dir( + &mut self, + host_path: impl AsRef, + guest_path: impl AsRef, + dir_perms: DirPerms, + file_perms: FilePerms, + ) -> Result<&mut Self> { + let dir = cap_std::fs::Dir::open_ambient_dir(host_path.as_ref(), ambient_authority())?; + let mut open_mode = OpenMode::empty(); + if dir_perms.contains(DirPerms::READ) { + open_mode |= OpenMode::READ; + } + if dir_perms.contains(DirPerms::MUTATE) { + open_mode |= OpenMode::WRITE; + } + self.filesystem.preopens.push(( + Dir::new( + dir, + dir_perms, + file_perms, + open_mode, + self.filesystem.allow_blocking_current_thread, + ), + guest_path.as_ref().to_owned(), + )); + Ok(self) + } + + /// Set the generator for the `wasi:random/random` number generator to the + /// custom generator specified. + /// + /// Note that contexts have a default RNG configured which is a suitable + /// generator for WASI and is configured with a random seed per-context. + /// + /// Guest code may rely on this random number generator to produce fresh + /// unpredictable random data in order to maintain its security invariants, + /// and ideally should use the insecure random API otherwise, so using any + /// prerecorded or otherwise predictable data may compromise security. + pub fn secure_random(&mut self, random: impl RngCore + Send + 'static) -> &mut Self { + self.random.random = Box::new(random); + self + } + + /// Configures the generator for `wasi:random/insecure`. + /// + /// The `insecure_random` generator provided will be used for all randomness + /// requested by the `wasi:random/insecure` interface. + pub fn insecure_random(&mut self, insecure_random: impl RngCore + Send + 'static) -> &mut Self { + self.random.insecure_random = Box::new(insecure_random); + self + } + + /// Configures the seed to be returned from `wasi:random/insecure-seed` to + /// the specified custom value. + /// + /// By default this number is randomly generated when a builder is created. + pub fn insecure_random_seed(&mut self, insecure_random_seed: u128) -> &mut Self { + self.random.insecure_random_seed = insecure_random_seed; + self + } + + /// Configures `wasi:clocks/wall-clock` to use the `clock` specified. + /// + /// By default the host's wall clock is used. + pub fn wall_clock(&mut self, clock: impl HostWallClock + 'static) -> &mut Self { + self.clocks.wall_clock = Box::new(clock); + self + } + + /// Configures `wasi:clocks/monotonic-clock` to use the `clock` specified. + /// + /// By default the host's monotonic clock is used. + pub fn monotonic_clock(&mut self, clock: impl HostMonotonicClock + 'static) -> &mut Self { + self.clocks.monotonic_clock = Box::new(clock); + self + } + + /// Allow all network addresses accessible to the host. + /// + /// This method will inherit all network addresses meaning that any address + /// can be bound by the guest or connected to by the guest using any + /// protocol. + /// + /// See also [`WasiCtxBuilder::socket_addr_check`]. + pub fn inherit_network(&mut self) -> &mut Self { + self.socket_addr_check(|_, _| Box::pin(async { true })) + } + + /// Set loopback network + pub fn loopback_network( + &mut self, + loopback: std::sync::Arc>, + ) -> &mut Self { + self.sockets.loopback = loopback; + self + } + + /// A check that will be called for each socket address that is used. + /// + /// Returning `true` will permit socket connections to the `SocketAddr`, + /// while returning `false` will reject the connection. + pub fn socket_addr_check(&mut self, check: F) -> &mut Self + where + F: Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync + + 'static, + { + self.sockets.socket_addr_check = SocketAddrCheck::new(check); + self + } + + /// Allow usage of `wasi:sockets/ip-name-lookup` + /// + /// By default this is disabled. + pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self { + self.sockets.allowed_network_uses.ip_name_lookup = enable; + self + } + + /// Allow usage of UDP. + /// + /// This is enabled by default, but can be disabled if UDP should be blanket + /// disabled. + pub fn allow_udp(&mut self, enable: bool) -> &mut Self { + self.sockets.allowed_network_uses.udp = enable; + self + } + + /// Allow usage of TCP + /// + /// This is enabled by default, but can be disabled if TCP should be blanket + /// disabled. + pub fn allow_tcp(&mut self, enable: bool) -> &mut Self { + self.sockets.allowed_network_uses.tcp = enable; + self + } + + /// Uses the configured context so far to construct the final [`WasiCtx`]. + /// + /// Note that each `WasiCtxBuilder` can only be used to "build" once, and + /// calling this method twice will panic. + /// + /// # Panics + /// + /// Panics if this method is called twice. Each [`WasiCtxBuilder`] can be + /// used to create only a single [`WasiCtx`]. Repeated usage of this method + /// is not allowed and should use a second builder instead. + pub fn build(&mut self) -> WasiCtx { + assert!(!self.built); + + let Self { + cli, + clocks, + filesystem, + random, + sockets, + built: _, + } = mem::replace(self, Self::new()); + self.built = true; + + WasiCtx { + cli, + clocks, + filesystem, + random, + sockets, + } + } + /// Builds a WASIp1 context instead of a [`WasiCtx`]. + /// + /// This method is the same as [`build`](WasiCtxBuilder::build) but it + /// creates a [`WasiP1Ctx`] instead. This is intended for use with the + /// [`p1`] module of this crate + /// + /// [`WasiP1Ctx`]: crate::p1::WasiP1Ctx + /// [`p1`]: crate::p1 + /// + /// # Panics + /// + /// Panics if this method is called twice. Each [`WasiCtxBuilder`] can be + /// used to create only a single [`WasiCtx`] or [`WasiP1Ctx`]. Repeated + /// usage of this method is not allowed and should use a second builder + /// instead. + #[cfg(feature = "p1")] + pub fn build_p1(&mut self) -> crate::p1::WasiP1Ctx { + let wasi = self.build(); + crate::p1::WasiP1Ctx::new(wasi) + } +} + +/// Per-[`Store`] state which holds state necessary to implement WASI from this +/// crate. +/// +/// This structure is created through [`WasiCtxBuilder`] and is stored within +/// the `T` of [`Store`][`Store`]. Access to the structure is provided +/// through the [`WasiView`](crate::WasiView) trait as an implementation on `T`. +/// +/// Note that this structure itself does not have any accessors, it's here for +/// internal use within the `wasmtime-wasi` crate's implementation of +/// bindgen-generated traits. +/// +/// [`Store`]: wasmtime::Store +/// +/// # Example +/// +/// ``` +/// use wash_wasi::{ResourceTable, WasiCtx, WasiCtxView, WasiView, WasiCtxBuilder}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// let mut wasi = WasiCtxBuilder::new(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// MyState { +/// ctx: wasi.build(), +/// table: ResourceTable::new(), +/// } +/// } +/// } +/// ``` +#[derive(Default)] +pub struct WasiCtx { + pub(crate) cli: WasiCliCtx, + pub(crate) clocks: WasiClocksCtx, + pub(crate) filesystem: WasiFilesystemCtx, + pub(crate) random: WasiRandomCtx, + pub(crate) sockets: WasiSocketsCtx, +} + +impl WasiCtx { + /// Convenience function for calling [`WasiCtxBuilder::new`]. + pub fn builder() -> WasiCtxBuilder { + WasiCtxBuilder::new() + } + + /// Returns access to the underlying [`WasiRandomCtx`]. + pub fn random(&mut self) -> &mut WasiRandomCtx { + &mut self.random + } + + /// Returns access to the underlying [`WasiClocksCtx`]. + pub fn clocks(&mut self) -> &mut WasiClocksCtx { + &mut self.clocks + } + + /// Returns access to the underlying [`WasiFilesystemCtx`]. + pub fn filesystem(&mut self) -> &mut WasiFilesystemCtx { + &mut self.filesystem + } + + /// Returns access to the underlying [`WasiCliCtx`]. + pub fn cli(&mut self) -> &mut WasiCliCtx { + &mut self.cli + } + + /// Returns access to the underlying [`WasiSocketsCtx`]. + pub fn sockets(&mut self) -> &mut WasiSocketsCtx { + &mut self.sockets + } +} diff --git a/crates/wasi/src/error.rs b/crates/wasi/src/error.rs new file mode 100644 index 00000000..f92317c1 --- /dev/null +++ b/crates/wasi/src/error.rs @@ -0,0 +1,97 @@ +use std::error::Error; +use std::fmt; +use std::marker; + +/// An error returned from the `proc_exit` host syscall. +/// +/// Embedders can test if an error returned from wasm is this error, in which +/// case it may signal a non-fatal trap. +#[derive(Debug)] +pub struct I32Exit(pub i32); + +impl fmt::Display for I32Exit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Exited with i32 exit status {}", self.0) + } +} + +impl std::error::Error for I32Exit {} + +/// A helper error type used by many other modules through type aliases. +/// +/// This type is an `Error` itself and is intended to be a representation of +/// either: +/// +/// * A custom error type `T` +/// * A trap, represented as `anyhow::Error` +/// +/// This error is created through either the `::trap` constructor representing a +/// full-fledged trap or the `From` constructor which is intended to be used +/// with `?`. The goal is to make normal errors `T` "automatic" but enable error +/// paths to return a `::trap` error optionally still as necessary without extra +/// boilerplate everywhere else. +/// +/// Note that this type isn't used directly but instead is intended to be used +/// as: +/// +/// ```rust,ignore +/// type MyError = TrappableError; +/// ``` +/// +/// where `MyError` is what you'll use throughout bindings code and +/// `bindgen::TheError` is the type that this represents as generated by the +/// `bindgen!` macro. +#[repr(transparent)] +pub struct TrappableError { + err: anyhow::Error, + _marker: marker::PhantomData, +} + +impl TrappableError { + pub fn trap(err: impl Into) -> TrappableError { + TrappableError { + err: err.into(), + _marker: marker::PhantomData, + } + } + + pub fn downcast(self) -> anyhow::Result + where + T: Error + Send + Sync + 'static, + { + self.err.downcast() + } + + pub fn downcast_ref(&self) -> Option<&T> + where + T: Error + Send + Sync + 'static, + { + self.err.downcast_ref() + } +} + +impl From for TrappableError +where + T: Error + Send + Sync + 'static, +{ + fn from(error: T) -> Self { + Self { + err: error.into(), + _marker: marker::PhantomData, + } + } +} + +impl fmt::Debug for TrappableError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + +impl fmt::Display for TrappableError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.err.fmt(f) + } +} + +impl Error for TrappableError {} diff --git a/crates/wasi/src/filesystem.rs b/crates/wasi/src/filesystem.rs new file mode 100644 index 00000000..f2f5bd61 --- /dev/null +++ b/crates/wasi/src/filesystem.rs @@ -0,0 +1,1165 @@ +use crate::clocks::Datetime; +use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking}; +use anyhow::Context as _; +use cap_fs_ext::{FileTypeExt as _, MetadataExt as _}; +use fs_set_times::SystemTimeSpec; +use std::collections::hash_map; +use std::sync::Arc; +use tracing::debug; +use wasmtime::component::{HasData, Resource, ResourceTable}; + +/// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs. +/// +/// This can be useful when directly calling `add_to_linker` functions directly, +/// such as [`wash_wasi::p2::bindings::filesystem::types::add_to_linker`] as +/// the `D` type parameter. See [`HasData`] for more information about the type +/// parameter's purpose. +/// +/// When using this type you can skip the [`WasiFilesystemView`] trait, for +/// example. +/// +/// # Examples +/// +/// ``` +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime::{Engine, Result, Config}; +/// use wash_wasi::filesystem::*; +/// +/// struct MyStoreState { +/// table: ResourceTable, +/// filesystem: WasiFilesystemCtx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// let mut linker = Linker::new(&engine); +/// +/// wash_wasi::p2::bindings::filesystem::types::add_to_linker::( +/// &mut linker, +/// |state| WasiFilesystemCtxView { +/// table: &mut state.table, +/// ctx: &mut state.filesystem, +/// }, +/// )?; +/// Ok(()) +/// } +/// ``` +pub struct WasiFilesystem; + +impl HasData for WasiFilesystem { + type Data<'a> = WasiFilesystemCtxView<'a>; +} + +#[derive(Clone, Default)] +pub struct WasiFilesystemCtx { + pub(crate) allow_blocking_current_thread: bool, + pub(crate) preopens: Vec<(Dir, String)>, +} + +pub struct WasiFilesystemCtxView<'a> { + pub ctx: &'a mut WasiFilesystemCtx, + pub table: &'a mut ResourceTable, +} + +pub trait WasiFilesystemView: Send { + fn filesystem(&mut self) -> WasiFilesystemCtxView<'_>; +} + +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct FilePerms: usize { + const READ = 0b1; + const WRITE = 0b10; + } +} + +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct OpenMode: usize { + const READ = 0b1; + const WRITE = 0b10; + } +} + +bitflags::bitflags! { + /// Permission bits for operating on a directory. + /// + /// Directories can be limited to being readonly. This will restrict what + /// can be done with them, for example preventing creation of new files. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct DirPerms: usize { + /// This directory can be read, for example its entries can be iterated + /// over and files can be opened. + const READ = 0b1; + + /// This directory can be mutated, for example by creating new files + /// within it. + const MUTATE = 0b10; + } +} + +bitflags::bitflags! { + /// Flags determining the method of how paths are resolved. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub(crate) struct PathFlags: usize { + /// This directory can be read, for example its entries can be iterated + /// over and files can be opened. + const SYMLINK_FOLLOW = 0b1; + } +} + +bitflags::bitflags! { + /// Open flags used by `open-at`. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub(crate) struct OpenFlags: usize { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + const CREATE = 0b1; + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + const DIRECTORY = 0b10; + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + const EXCLUSIVE = 0b100; + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + const TRUNCATE = 0b1000; + } +} + +bitflags::bitflags! { + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub(crate) struct DescriptorFlags: usize { + /// Read mode: Data can be read. + const READ = 0b1; + /// Write mode: Data can be written to. + const WRITE = 0b10; + /// 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. + const FILE_INTEGRITY_SYNC = 0b100; + /// 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. + const DATA_INTEGRITY_SYNC = 0b1000; + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + const REQUESTED_WRITE_SYNC = 0b10000; + /// 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. + const MUTATE_DIRECTORY = 0b100000; + } +} + +/// 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. +#[cfg_attr( + windows, + expect(dead_code, reason = "on Windows, some of these are not used") +)] +pub(crate) enum ErrorCode { + /// Permission denied, similar to `EACCES` in POSIX. + Access, + /// Connection already in progress, similar to `EALREADY` in POSIX. + Already, + /// Bad descriptor, similar to `EBADF` in POSIX. + BadDescriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + Busy, + /// File exists, similar to `EEXIST` in POSIX. + Exist, + /// File too large, similar to `EFBIG` in POSIX. + FileTooLarge, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + IllegalByteSequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + InProgress, + /// 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. + IsDirectory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + Loop, + /// Too many links, similar to `EMLINK` in POSIX. + TooManyLinks, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + NameTooLong, + /// No such file or directory, similar to `ENOENT` in POSIX. + NoEntry, + /// Not enough space, similar to `ENOMEM` in POSIX. + InsufficientMemory, + /// No space left on device, similar to `ENOSPC` in POSIX. + InsufficientSpace, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + NotDirectory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + NotEmpty, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + Unsupported, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + Overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + NotPermitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + Pipe, + /// Invalid seek, similar to `ESPIPE` in POSIX. + InvalidSeek, +} + +fn datetime_from(t: std::time::SystemTime) -> Datetime { + // FIXME make this infallible or handle errors properly + Datetime::try_from(cap_std::time::SystemTime::from_std(t)).unwrap() +} + +/// The type of a filesystem object referenced by a descriptor. +/// +/// Note: This was called `filetype` in earlier versions of WASI. +pub(crate) enum DescriptorType { + /// 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. + BlockDevice, + /// The descriptor refers to a character device inode. + CharacterDevice, + /// The descriptor refers to a directory inode. + Directory, + /// The file refers to a symbolic link inode. + SymbolicLink, + /// The descriptor refers to a regular file inode. + RegularFile, +} + +impl From for DescriptorType { + fn from(ft: cap_std::fs::FileType) -> Self { + if ft.is_dir() { + DescriptorType::Directory + } else if ft.is_symlink() { + DescriptorType::SymbolicLink + } else if ft.is_block_device() { + DescriptorType::BlockDevice + } else if ft.is_char_device() { + DescriptorType::CharacterDevice + } else if ft.is_file() { + DescriptorType::RegularFile + } else { + DescriptorType::Unknown + } + } +} + +/// File attributes. +/// +/// Note: This was called `filestat` in earlier versions of WASI. +pub(crate) struct DescriptorStat { + /// File type. + pub type_: DescriptorType, + /// Number of hard links to the file. + pub link_count: u64, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + pub size: u64, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + pub data_access_timestamp: Option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + pub data_modification_timestamp: Option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + pub status_change_timestamp: Option, +} + +impl From for DescriptorStat { + fn from(meta: cap_std::fs::Metadata) -> Self { + Self { + type_: meta.file_type().into(), + link_count: meta.nlink(), + size: meta.len(), + data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(), + data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(), + status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(), + } + } +} + +/// A 128-bit hash value, split into parts because wasm doesn't have a +/// 128-bit integer type. +pub(crate) struct MetadataHashValue { + /// 64 bits of a 128-bit hash value. + pub lower: u64, + /// Another 64 bits of a 128-bit hash value. + pub upper: u64, +} + +impl From<&cap_std::fs::Metadata> for MetadataHashValue { + fn from(meta: &cap_std::fs::Metadata) -> Self { + use cap_fs_ext::MetadataExt; + // Without incurring any deps, std provides us with a 64 bit hash + // function: + use std::hash::Hasher; + // Note that this means that the metadata hash (which becomes a preview1 ino) may + // change when a different rustc release is used to build this host implementation: + let mut hasher = hash_map::DefaultHasher::new(); + hasher.write_u64(meta.dev()); + hasher.write_u64(meta.ino()); + let lower = hasher.finish(); + // MetadataHashValue has a pair of 64-bit members for representing a + // single 128-bit number. However, we only have 64 bits of entropy. To + // synthesize the upper 64 bits, lets xor the lower half with an arbitrary + // constant, in this case the 64 bit integer corresponding to the IEEE + // double representation of (a number as close as possible to) pi. + // This seems better than just repeating the same bits in the upper and + // lower parts outright, which could make folks wonder if the struct was + // mangled in the ABI, or worse yet, lead to consumers of this interface + // expecting them to be equal. + let upper = lower ^ 4614256656552045848u64; + Self { lower, upper } + } +} + +#[cfg(unix)] +fn from_raw_os_error(err: Option) -> Option { + use rustix::io::Errno as RustixErrno; + if err.is_none() { + return None; + } + Some(match RustixErrno::from_raw_os_error(err.unwrap()) { + RustixErrno::PIPE => ErrorCode::Pipe, + RustixErrno::PERM => ErrorCode::NotPermitted, + RustixErrno::NOENT => ErrorCode::NoEntry, + RustixErrno::NOMEM => ErrorCode::InsufficientMemory, + RustixErrno::IO => ErrorCode::Io, + RustixErrno::BADF => ErrorCode::BadDescriptor, + RustixErrno::BUSY => ErrorCode::Busy, + RustixErrno::ACCESS => ErrorCode::Access, + RustixErrno::NOTDIR => ErrorCode::NotDirectory, + RustixErrno::ISDIR => ErrorCode::IsDirectory, + RustixErrno::INVAL => ErrorCode::Invalid, + RustixErrno::EXIST => ErrorCode::Exist, + RustixErrno::FBIG => ErrorCode::FileTooLarge, + RustixErrno::NOSPC => ErrorCode::InsufficientSpace, + RustixErrno::SPIPE => ErrorCode::InvalidSeek, + RustixErrno::MLINK => ErrorCode::TooManyLinks, + RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong, + RustixErrno::NOTEMPTY => ErrorCode::NotEmpty, + RustixErrno::LOOP => ErrorCode::Loop, + RustixErrno::OVERFLOW => ErrorCode::Overflow, + RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence, + RustixErrno::NOTSUP => ErrorCode::Unsupported, + RustixErrno::ALREADY => ErrorCode::Already, + RustixErrno::INPROGRESS => ErrorCode::InProgress, + RustixErrno::INTR => ErrorCode::Interrupted, + + // On some platforms, these have the same value as other errno values. + #[allow(unreachable_patterns, reason = "see comment")] + RustixErrno::OPNOTSUPP => ErrorCode::Unsupported, + + _ => return None, + }) +} + +#[cfg(windows)] +fn from_raw_os_error(raw_os_error: Option) -> Option { + use windows_sys::Win32::Foundation; + Some(match raw_os_error.map(|code| code as u32) { + Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access, + Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access, + Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted, + Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor, + Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry, + Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty, + Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy, + Some(Foundation::ERROR_BUSY) => ErrorCode::Busy, + Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported, + Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe, + Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong, + Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid, + Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid, + Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory, + Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop, + Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory, + _ => return None, + }) +} + +impl<'a> From<&'a std::io::Error> for ErrorCode { + fn from(err: &'a std::io::Error) -> ErrorCode { + match from_raw_os_error(err.raw_os_error()) { + Some(errno) => errno, + None => { + debug!("unknown raw os error: {err}"); + match err.kind() { + std::io::ErrorKind::NotFound => ErrorCode::NoEntry, + std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted, + std::io::ErrorKind::AlreadyExists => ErrorCode::Exist, + std::io::ErrorKind::InvalidInput => ErrorCode::Invalid, + _ => ErrorCode::Io, + } + } + } + } +} + +impl From for ErrorCode { + fn from(err: std::io::Error) -> ErrorCode { + ErrorCode::from(&err) + } +} + +#[derive(Clone)] +pub enum Descriptor { + File(File), + Dir(Dir), +} + +impl Descriptor { + pub(crate) fn file(&self) -> Result<&File, ErrorCode> { + match self { + Descriptor::File(f) => Ok(f), + Descriptor::Dir(_) => Err(ErrorCode::BadDescriptor), + } + } + + pub(crate) fn dir(&self) -> Result<&Dir, ErrorCode> { + match self { + Descriptor::Dir(d) => Ok(d), + Descriptor::File(_) => Err(ErrorCode::NotDirectory), + } + } + + async fn get_metadata(&self) -> std::io::Result { + match self { + Self::File(f) => { + // No permissions check on metadata: if opened, allowed to stat it + f.run_blocking(|f| f.metadata()).await + } + Self::Dir(d) => { + // No permissions check on metadata: if opened, allowed to stat it + d.run_blocking(|d| d.dir_metadata()).await + } + } + } + + pub(crate) async fn sync_data(&self) -> Result<(), ErrorCode> { + match self { + Self::File(f) => { + match f.run_blocking(|f| f.sync_data()).await { + Ok(()) => Ok(()), + // On windows, `sync_data` uses `FileFlushBuffers` which fails with + // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore + // this error, for POSIX compatibility. + #[cfg(windows)] + Err(err) + if err.raw_os_error() + == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) => + { + Ok(()) + } + Err(err) => Err(err.into()), + } + } + Self::Dir(d) => { + d.run_blocking(|d| { + let d = d.open(std::path::Component::CurDir)?; + d.sync_data()?; + Ok(()) + }) + .await + } + } + } + + pub(crate) async fn get_flags(&self) -> Result { + use system_interface::fs::{FdFlags, GetSetFdFlags}; + + fn get_from_fdflags(flags: FdFlags) -> DescriptorFlags { + let mut out = DescriptorFlags::empty(); + if flags.contains(FdFlags::DSYNC) { + out |= DescriptorFlags::REQUESTED_WRITE_SYNC; + } + if flags.contains(FdFlags::RSYNC) { + out |= DescriptorFlags::DATA_INTEGRITY_SYNC; + } + if flags.contains(FdFlags::SYNC) { + out |= DescriptorFlags::FILE_INTEGRITY_SYNC; + } + out + } + match self { + Self::File(f) => { + let flags = f.run_blocking(|f| f.get_fd_flags()).await?; + let mut flags = get_from_fdflags(flags); + if f.open_mode.contains(OpenMode::READ) { + flags |= DescriptorFlags::READ; + } + if f.open_mode.contains(OpenMode::WRITE) { + flags |= DescriptorFlags::WRITE; + } + Ok(flags) + } + Self::Dir(d) => { + let flags = d.run_blocking(|d| d.get_fd_flags()).await?; + let mut flags = get_from_fdflags(flags); + if d.open_mode.contains(OpenMode::READ) { + flags |= DescriptorFlags::READ; + } + if d.open_mode.contains(OpenMode::WRITE) { + flags |= DescriptorFlags::MUTATE_DIRECTORY; + } + Ok(flags) + } + } + } + + pub(crate) async fn get_type(&self) -> Result { + match self { + Self::File(f) => { + let meta = f.run_blocking(|f| f.metadata()).await?; + Ok(meta.file_type().into()) + } + Self::Dir(_) => Ok(DescriptorType::Directory), + } + } + + pub(crate) async fn set_times( + &self, + atim: Option, + mtim: Option, + ) -> Result<(), ErrorCode> { + use fs_set_times::SetTimes as _; + match self { + Self::File(f) => { + if !f.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted); + } + f.run_blocking(|f| f.set_times(atim, mtim)).await?; + Ok(()) + } + Self::Dir(d) => { + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + d.run_blocking(|d| d.set_times(atim, mtim)).await?; + Ok(()) + } + } + } + + pub(crate) async fn sync(&self) -> Result<(), ErrorCode> { + match self { + Self::File(f) => { + match f.run_blocking(|f| f.sync_all()).await { + Ok(()) => Ok(()), + // On windows, `sync_data` uses `FileFlushBuffers` which fails with + // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore + // this error, for POSIX compatibility. + #[cfg(windows)] + Err(err) + if err.raw_os_error() + == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) => + { + Ok(()) + } + Err(err) => Err(err.into()), + } + } + Self::Dir(d) => { + d.run_blocking(|d| { + let d = d.open(std::path::Component::CurDir)?; + d.sync_all()?; + Ok(()) + }) + .await + } + } + } + + pub(crate) async fn stat(&self) -> Result { + match self { + Self::File(f) => { + // No permissions check on stat: if opened, allowed to stat it + let meta = f.run_blocking(|f| f.metadata()).await?; + Ok(meta.into()) + } + Self::Dir(d) => { + // No permissions check on stat: if opened, allowed to stat it + let meta = d.run_blocking(|d| d.dir_metadata()).await?; + Ok(meta.into()) + } + } + } + + pub(crate) async fn is_same_object(&self, other: &Self) -> wasmtime::Result { + use cap_fs_ext::MetadataExt; + let meta_a = self.get_metadata().await?; + let meta_b = other.get_metadata().await?; + if meta_a.dev() == meta_b.dev() && meta_a.ino() == meta_b.ino() { + // MetadataHashValue does not derive eq, so use a pair of + // comparisons to check equality: + debug_assert_eq!( + MetadataHashValue::from(&meta_a).upper, + MetadataHashValue::from(&meta_b).upper, + ); + debug_assert_eq!( + MetadataHashValue::from(&meta_a).lower, + MetadataHashValue::from(&meta_b).lower, + ); + Ok(true) + } else { + // Hash collisions are possible, so don't assert the negative here + Ok(false) + } + } + + pub(crate) async fn metadata_hash(&self) -> Result { + let meta = self.get_metadata().await?; + Ok(MetadataHashValue::from(&meta)) + } +} + +#[derive(Clone)] +pub struct File { + /// The operating system File this struct is mediating access to. + /// + /// Wrapped in an Arc because the same underlying file is used for + /// implementing the stream types. A copy is also needed for + /// [`spawn_blocking`]. + /// + /// [`spawn_blocking`]: Self::spawn_blocking + pub file: Arc, + /// Permissions to enforce on access to the file. These permissions are + /// specified by a user of the `crate::WasiCtxBuilder`, and are + /// enforced prior to any enforced by the underlying operating system. + pub perms: FilePerms, + /// The mode the file was opened under: bits for reading, and writing. + /// Required to correctly report the DescriptorFlags, because cap-std + /// doesn't presently provide a cross-platform equivalent of reading the + /// oflags back out using fcntl. + pub open_mode: OpenMode, + + allow_blocking_current_thread: bool, +} + +impl File { + pub fn new( + file: cap_std::fs::File, + perms: FilePerms, + open_mode: OpenMode, + allow_blocking_current_thread: bool, + ) -> Self { + Self { + file: Arc::new(file), + perms, + open_mode, + allow_blocking_current_thread, + } + } + + /// Execute the blocking `body` function. + /// + /// Depending on how the WasiCtx was configured, the body may either be: + /// - Executed directly on the current thread. In this case the `async` + /// signature of this method is effectively a lie and the returned + /// Future will always be immediately Ready. Or: + /// - Spawned on a background thread using [`tokio::task::spawn_blocking`] + /// and immediately awaited. + /// + /// Intentionally blocking the executor thread might seem unorthodox, but is + /// not actually a problem for specific workloads. See: + /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`] + /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973) + /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190) + pub(crate) async fn run_blocking(&self, body: F) -> R + where + F: FnOnce(&cap_std::fs::File) -> R + Send + 'static, + R: Send + 'static, + { + match self.as_blocking_file() { + Some(file) => body(file), + None => self.spawn_blocking(body).await, + } + } + + pub(crate) fn spawn_blocking(&self, body: F) -> AbortOnDropJoinHandle + where + F: FnOnce(&cap_std::fs::File) -> R + Send + 'static, + R: Send + 'static, + { + let f = self.file.clone(); + spawn_blocking(move || body(&f)) + } + + /// Returns `Some` when the current thread is allowed to block in filesystem + /// operations, and otherwise returns `None` to indicate that + /// `spawn_blocking` must be used. + pub(crate) fn as_blocking_file(&self) -> Option<&cap_std::fs::File> { + if self.allow_blocking_current_thread { + Some(&self.file) + } else { + None + } + } + + /// Returns reference to the underlying [`cap_std::fs::File`] + #[cfg(feature = "p3")] + pub(crate) fn as_file(&self) -> &Arc { + &self.file + } + + pub(crate) async fn advise( + &self, + offset: u64, + len: u64, + advice: system_interface::fs::Advice, + ) -> Result<(), ErrorCode> { + use system_interface::fs::FileIoExt as _; + self.run_blocking(move |f| f.advise(offset, len, advice)) + .await?; + Ok(()) + } + + pub(crate) async fn set_size(&self, size: u64) -> Result<(), ErrorCode> { + if !self.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted); + } + self.run_blocking(move |f| f.set_len(size)).await?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct Dir { + /// The operating system file descriptor this struct is mediating access + /// to. + /// + /// Wrapped in an Arc because a copy is needed for [`spawn_blocking`]. + /// + /// [`spawn_blocking`]: Self::spawn_blocking + pub dir: Arc, + /// Permissions to enforce on access to this directory. These permissions + /// are specified by a user of the `crate::WasiCtxBuilder`, and + /// are enforced prior to any enforced by the underlying operating system. + /// + /// These permissions are also enforced on any directories opened under + /// this directory. + pub perms: DirPerms, + /// Permissions to enforce on any files opened under this directory. + pub file_perms: FilePerms, + /// The mode the directory was opened under: bits for reading, and writing. + /// Required to correctly report the DescriptorFlags, because cap-std + /// doesn't presently provide a cross-platform equivalent of reading the + /// oflags back out using fcntl. + pub open_mode: OpenMode, + + pub(crate) allow_blocking_current_thread: bool, +} + +impl Dir { + pub fn new( + dir: cap_std::fs::Dir, + perms: DirPerms, + file_perms: FilePerms, + open_mode: OpenMode, + allow_blocking_current_thread: bool, + ) -> Self { + Dir { + dir: Arc::new(dir), + perms, + file_perms, + open_mode, + allow_blocking_current_thread, + } + } + + /// Execute the blocking `body` function. + /// + /// Depending on how the WasiCtx was configured, the body may either be: + /// - Executed directly on the current thread. In this case the `async` + /// signature of this method is effectively a lie and the returned + /// Future will always be immediately Ready. Or: + /// - Spawned on a background thread using [`tokio::task::spawn_blocking`] + /// and immediately awaited. + /// + /// Intentionally blocking the executor thread might seem unorthodox, but is + /// not actually a problem for specific workloads. See: + /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`] + /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973) + /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190) + pub(crate) async fn run_blocking(&self, body: F) -> R + where + F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static, + R: Send + 'static, + { + if self.allow_blocking_current_thread { + body(&self.dir) + } else { + let d = self.dir.clone(); + spawn_blocking(move || body(&d)).await + } + } + + /// Returns reference to the underlying [`cap_std::fs::Dir`] + #[cfg(feature = "p3")] + pub(crate) fn as_dir(&self) -> &Arc { + &self.dir + } + + pub(crate) async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> { + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + self.run_blocking(move |d| d.create_dir(&path)).await?; + Ok(()) + } + + pub(crate) async fn stat_at( + &self, + path_flags: PathFlags, + path: String, + ) -> Result { + if !self.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted); + } + + let meta = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + self.run_blocking(move |d| d.metadata(&path)).await? + } else { + self.run_blocking(move |d| d.symlink_metadata(&path)) + .await? + }; + Ok(meta.into()) + } + + pub(crate) async fn set_times_at( + &self, + path_flags: PathFlags, + path: String, + atim: Option, + mtim: Option, + ) -> Result<(), ErrorCode> { + use cap_fs_ext::DirExt as _; + + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + self.run_blocking(move |d| { + d.set_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + }) + .await?; + } else { + self.run_blocking(move |d| { + d.set_symlink_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + }) + .await?; + } + Ok(()) + } + + pub(crate) async fn link_at( + &self, + old_path_flags: PathFlags, + old_path: String, + new_dir: &Self, + new_path: String, + ) -> Result<(), ErrorCode> { + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + if !new_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + if old_path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + return Err(ErrorCode::Invalid); + } + let new_dir_handle = Arc::clone(&new_dir.dir); + self.run_blocking(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) + .await?; + Ok(()) + } + + pub(crate) async fn open_at( + &self, + path_flags: PathFlags, + path: String, + oflags: OpenFlags, + flags: DescriptorFlags, + allow_blocking_current_thread: bool, + ) -> Result { + use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt}; + use system_interface::fs::{FdFlags, GetSetFdFlags}; + + if !self.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted); + } + + if !self.perms.contains(DirPerms::MUTATE) { + if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) { + return Err(ErrorCode::NotPermitted); + } + if flags.contains(DescriptorFlags::WRITE) { + return Err(ErrorCode::NotPermitted); + } + } + + // Track whether we are creating file, for permission check: + let mut create = false; + // Track open mode, for permission check and recording in created descriptor: + let mut open_mode = OpenMode::empty(); + // Construct the OpenOptions to give the OS: + let mut opts = cap_std::fs::OpenOptions::new(); + opts.maybe_dir(true); + + if oflags.contains(OpenFlags::CREATE) { + if oflags.contains(OpenFlags::EXCLUSIVE) { + opts.create_new(true); + } else { + opts.create(true); + } + create = true; + opts.write(true); + open_mode |= OpenMode::WRITE; + } + + if oflags.contains(OpenFlags::TRUNCATE) { + opts.truncate(true).write(true); + } + if flags.contains(DescriptorFlags::READ) { + opts.read(true); + open_mode |= OpenMode::READ; + } + if flags.contains(DescriptorFlags::WRITE) { + opts.write(true); + open_mode |= OpenMode::WRITE; + } else { + // If not opened write, open read. This way the OS lets us open + // the file, but we can use perms to reject use of the file later. + opts.read(true); + open_mode |= OpenMode::READ; + } + if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + opts.follow(FollowSymlinks::Yes); + } else { + opts.follow(FollowSymlinks::No); + } + + // These flags are not yet supported in cap-std: + if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC) + || flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC) + || flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC) + { + return Err(ErrorCode::Unsupported); + } + + if oflags.contains(OpenFlags::DIRECTORY) { + if oflags.contains(OpenFlags::CREATE) + || oflags.contains(OpenFlags::EXCLUSIVE) + || oflags.contains(OpenFlags::TRUNCATE) + { + return Err(ErrorCode::Invalid); + } + } + + // Now enforce this WasiCtx's permissions before letting the OS have + // its shot: + if !self.perms.contains(DirPerms::MUTATE) && create { + return Err(ErrorCode::NotPermitted); + } + if !self.file_perms.contains(FilePerms::WRITE) && open_mode.contains(OpenMode::WRITE) { + return Err(ErrorCode::NotPermitted); + } + + // Represents each possible outcome from the spawn_blocking operation. + // This makes sure we don't have to give spawn_blocking any way to + // manipulate the table. + enum OpenResult { + Dir(cap_std::fs::Dir), + File(cap_std::fs::File), + NotDir, + } + + let opened = self + .run_blocking::<_, std::io::Result>(move |d| { + let mut opened = d.open_with(&path, &opts)?; + if opened.metadata()?.is_dir() { + Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( + opened.into_std(), + ))) + } else if oflags.contains(OpenFlags::DIRECTORY) { + Ok(OpenResult::NotDir) + } else { + // FIXME cap-std needs a nonblocking open option so that files reads and writes + // are nonblocking. Instead we set it after opening here: + let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; + opened.set_fd_flags(set_fd_flags)?; + Ok(OpenResult::File(opened)) + } + }) + .await?; + + match opened { + OpenResult::Dir(dir) => Ok(Descriptor::Dir(Dir::new( + dir, + self.perms, + self.file_perms, + open_mode, + allow_blocking_current_thread, + ))), + + OpenResult::File(file) => Ok(Descriptor::File(File::new( + file, + self.file_perms, + open_mode, + allow_blocking_current_thread, + ))), + + OpenResult::NotDir => Err(ErrorCode::NotDirectory), + } + } + + pub(crate) async fn readlink_at(&self, path: String) -> Result { + if !self.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted); + } + let link = self.run_blocking(move |d| d.read_link(&path)).await?; + link.into_os_string() + .into_string() + .or(Err(ErrorCode::IllegalByteSequence)) + } + + pub(crate) async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> { + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + self.run_blocking(move |d| d.remove_dir(&path)).await?; + Ok(()) + } + + pub(crate) async fn rename_at( + &self, + old_path: String, + new_dir: &Self, + new_path: String, + ) -> Result<(), ErrorCode> { + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + if !new_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + let new_dir_handle = Arc::clone(&new_dir.dir); + self.run_blocking(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) + .await?; + Ok(()) + } + + pub(crate) async fn symlink_at( + &self, + src_path: String, + dest_path: String, + ) -> Result<(), ErrorCode> { + // On windows, Dir.symlink is provided by DirExt + #[cfg(windows)] + use cap_fs_ext::DirExt; + + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + self.run_blocking(move |d| d.symlink(&src_path, &dest_path)) + .await?; + Ok(()) + } + + pub(crate) async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> { + use cap_fs_ext::DirExt; + + if !self.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted); + } + self.run_blocking(move |d| d.remove_file_or_symlink(&path)) + .await?; + Ok(()) + } + + pub(crate) async fn metadata_hash_at( + &self, + path_flags: PathFlags, + path: String, + ) -> Result { + // No permissions check on metadata: if dir opened, allowed to stat it + let meta = self + .run_blocking(move |d| { + if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + d.metadata(path) + } else { + d.symlink_metadata(path) + } + }) + .await?; + Ok(MetadataHashValue::from(&meta)) + } +} + +impl WasiFilesystemCtxView<'_> { + pub(crate) fn get_directories( + &mut self, + ) -> wasmtime::Result, String)>> { + let preopens = self.ctx.preopens.clone(); + let mut results = Vec::with_capacity(preopens.len()); + for (dir, name) in preopens { + let fd = self + .table + .push(Descriptor::Dir(dir)) + .with_context(|| format!("failed to push preopen {name}"))?; + results.push((fd, name)); + } + Ok(results) + } +} diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs new file mode 100644 index 00000000..6d599f65 --- /dev/null +++ b/crates/wasi/src/lib.rs @@ -0,0 +1,50 @@ +#![allow(clippy::all)] +#![allow(dead_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +//! # Wasmtime's WASI Implementation +//! +//! This crate provides a Wasmtime host implementations of different versions of WASI. +//! WASI is implemented with the Rust crates [`tokio`] and [`cap-std`](cap_std) primarily, meaning that +//! operations are implemented in terms of their native platform equivalents by +//! default. +//! +//! For components and WASIp2, see [`p2`]. +//! For WASIp1 and core modules, see the [`preview1`] module documentation. +//! +//! For WASIp3, see [`p3`]. WASIp3 support is experimental, unstable and incomplete. + +pub mod cli; +pub mod clocks; +mod ctx; +mod error; +pub mod filesystem; +#[cfg(feature = "p1")] +pub mod p0; +#[cfg(feature = "p1")] +pub mod p1; +// FIXME: should gate this module on the `p2` feature but that will require more +// internal refactoring to get that aligned right. +// #[cfg(feature = "p2")] +pub mod p2; +#[cfg(feature = "p3")] +pub mod p3; +pub mod random; +pub mod runtime; +pub mod sockets; +mod view; + +pub use self::clocks::{HostMonotonicClock, HostWallClock}; +pub use self::ctx::{WasiCtx, WasiCtxBuilder}; +pub use self::error::{I32Exit, TrappableError}; +pub use self::filesystem::{DirPerms, FilePerms, OpenMode}; +pub use self::random::{Deterministic, thread_rng}; +pub use self::view::{WasiCtxView, WasiView}; +#[doc(no_inline)] +pub use async_trait::async_trait; +#[doc(no_inline)] +pub use cap_fs_ext::SystemTimeSpec; +#[doc(no_inline)] +pub use cap_rand::RngCore; +#[doc(no_inline)] +pub use wasmtime::component::{ResourceTable, ResourceTableError}; diff --git a/crates/wasi/src/p0.rs b/crates/wasi/src/p0.rs new file mode 100644 index 00000000..cbff399a --- /dev/null +++ b/crates/wasi/src/p0.rs @@ -0,0 +1,983 @@ +//! Bindings for WASIp0 aka Preview 0 aka `wasi_unstable`. +//! +//! This module is purely here for backwards compatibility in the Wasmtime CLI. +//! You probably want to use [`p1`](crate::p1) instead. + +use crate::p0::types::Error; +use crate::p1::WasiP1Ctx; +use crate::p1::types as snapshot1_types; +use crate::p1::wasi_snapshot_preview1::WasiSnapshotPreview1 as Snapshot1; +use wiggle::{GuestError, GuestMemory, GuestPtr}; + +pub fn add_to_linker_async( + linker: &mut wasmtime::Linker, + f: impl Fn(&mut T) -> &mut WasiP1Ctx + Copy + Send + Sync + 'static, +) -> anyhow::Result<()> { + wasi_unstable::add_to_linker(linker, f) +} + +pub fn add_to_linker_sync( + linker: &mut wasmtime::Linker, + f: impl Fn(&mut T) -> &mut WasiP1Ctx + Copy + Send + Sync + 'static, +) -> anyhow::Result<()> { + sync::add_wasi_unstable_to_linker(linker, f) +} + +wiggle::from_witx!({ + witx: ["witx/p0/wasi_unstable.witx"], + async: { + wasi_unstable::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, + errors: { errno => trappable Error }, +}); + +mod sync { + use anyhow::Result; + use std::future::Future; + + wiggle::wasmtime_integration!({ + witx: ["witx/p0/wasi_unstable.witx"], + target: super, + block_on[in_tokio]: { + wasi_unstable::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, + errors: { errno => trappable Error }, + }); + + // Small wrapper around `in_tokio` to add a `Result` layer which is always + // `Ok` + fn in_tokio(future: F) -> Result { + Ok(crate::runtime::in_tokio(future)) + } +} + +impl wiggle::GuestErrorType for types::Errno { + fn success() -> Self { + Self::Success + } +} + +#[wiggle::async_trait] +impl wasi_unstable::WasiUnstable for T { + fn args_get( + &mut self, + memory: &mut GuestMemory<'_>, + argv: GuestPtr>, + argv_buf: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::args_get(self, memory, argv, argv_buf)?; + Ok(()) + } + + fn args_sizes_get( + &mut self, + memory: &mut GuestMemory<'_>, + ) -> Result<(types::Size, types::Size), Error> { + let s = Snapshot1::args_sizes_get(self, memory)?; + Ok(s) + } + + fn environ_get( + &mut self, + memory: &mut GuestMemory<'_>, + environ: GuestPtr>, + environ_buf: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::environ_get(self, memory, environ, environ_buf)?; + Ok(()) + } + + fn environ_sizes_get( + &mut self, + memory: &mut GuestMemory<'_>, + ) -> Result<(types::Size, types::Size), Error> { + let s = Snapshot1::environ_sizes_get(self, memory)?; + Ok(s) + } + + fn clock_res_get( + &mut self, + memory: &mut GuestMemory<'_>, + id: types::Clockid, + ) -> Result { + let t = Snapshot1::clock_res_get(self, memory, id.into())?; + Ok(t) + } + + fn clock_time_get( + &mut self, + memory: &mut GuestMemory<'_>, + id: types::Clockid, + precision: types::Timestamp, + ) -> Result { + let t = Snapshot1::clock_time_get(self, memory, id.into(), precision)?; + Ok(t) + } + + async fn fd_advise( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + offset: types::Filesize, + len: types::Filesize, + advice: types::Advice, + ) -> Result<(), Error> { + Snapshot1::fd_advise(self, memory, fd.into(), offset, len, advice.into()).await?; + Ok(()) + } + + fn fd_allocate( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + offset: types::Filesize, + len: types::Filesize, + ) -> Result<(), Error> { + Snapshot1::fd_allocate(self, memory, fd.into(), offset, len)?; + Ok(()) + } + + async fn fd_close(&mut self, memory: &mut GuestMemory<'_>, fd: types::Fd) -> Result<(), Error> { + Snapshot1::fd_close(self, memory, fd.into()).await?; + Ok(()) + } + + async fn fd_datasync( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result<(), Error> { + Snapshot1::fd_datasync(self, memory, fd.into()).await?; + Ok(()) + } + + async fn fd_fdstat_get( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + Ok(Snapshot1::fd_fdstat_get(self, memory, fd.into()) + .await? + .into()) + } + + fn fd_fdstat_set_flags( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + flags: types::Fdflags, + ) -> Result<(), Error> { + Snapshot1::fd_fdstat_set_flags(self, memory, fd.into(), flags.into())?; + Ok(()) + } + + fn fd_fdstat_set_rights( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + fs_rights_base: types::Rights, + fs_rights_inheriting: types::Rights, + ) -> Result<(), Error> { + Snapshot1::fd_fdstat_set_rights( + self, + memory, + fd.into(), + fs_rights_base.into(), + fs_rights_inheriting.into(), + )?; + Ok(()) + } + + async fn fd_filestat_get( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + Ok(Snapshot1::fd_filestat_get(self, memory, fd.into()) + .await? + .into()) + } + + async fn fd_filestat_set_size( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + size: types::Filesize, + ) -> Result<(), Error> { + Snapshot1::fd_filestat_set_size(self, memory, fd.into(), size).await?; + Ok(()) + } + + async fn fd_filestat_set_times( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + atim: types::Timestamp, + mtim: types::Timestamp, + fst_flags: types::Fstflags, + ) -> Result<(), Error> { + Snapshot1::fd_filestat_set_times(self, memory, fd.into(), atim, mtim, fst_flags.into()) + .await?; + Ok(()) + } + + async fn fd_read( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + iovs: types::IovecArray, + ) -> Result { + assert_iovec_array_same(); + let result = Snapshot1::fd_read(self, memory, fd.into(), iovs.cast()).await?; + Ok(result) + } + + async fn fd_pread( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + iovs: types::IovecArray, + offset: types::Filesize, + ) -> Result { + assert_iovec_array_same(); + let result = Snapshot1::fd_pread(self, memory, fd.into(), iovs.cast(), offset).await?; + Ok(result) + } + + async fn fd_write( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ciovs: types::CiovecArray, + ) -> Result { + assert_ciovec_array_same(); + let result = Snapshot1::fd_write(self, memory, fd.into(), ciovs.cast()).await?; + Ok(result) + } + + async fn fd_pwrite( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ciovs: types::CiovecArray, + offset: types::Filesize, + ) -> Result { + assert_ciovec_array_same(); + let result = Snapshot1::fd_pwrite(self, memory, fd.into(), ciovs.cast(), offset).await?; + Ok(result) + } + + fn fd_prestat_get( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + Ok(Snapshot1::fd_prestat_get(self, memory, fd.into())?.into()) + } + + fn fd_prestat_dir_name( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + path: GuestPtr, + path_max_len: types::Size, + ) -> Result<(), Error> { + Snapshot1::fd_prestat_dir_name(self, memory, fd.into(), path, path_max_len)?; + Ok(()) + } + + fn fd_renumber( + &mut self, + memory: &mut GuestMemory<'_>, + from: types::Fd, + to: types::Fd, + ) -> Result<(), Error> { + Snapshot1::fd_renumber(self, memory, from.into(), to.into())?; + Ok(()) + } + + async fn fd_seek( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + offset: types::Filedelta, + whence: types::Whence, + ) -> Result { + Ok(Snapshot1::fd_seek(self, memory, fd.into(), offset, whence.into()).await?) + } + + async fn fd_sync(&mut self, memory: &mut GuestMemory<'_>, fd: types::Fd) -> Result<(), Error> { + Snapshot1::fd_sync(self, memory, fd.into()).await?; + Ok(()) + } + + fn fd_tell( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + Ok(Snapshot1::fd_tell(self, memory, fd.into())?) + } + + async fn fd_readdir( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + buf: GuestPtr, + buf_len: types::Size, + cookie: types::Dircookie, + ) -> Result { + Ok(Snapshot1::fd_readdir(self, memory, fd.into(), buf, buf_len, cookie).await?) + } + + async fn path_create_directory( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_create_directory(self, memory, dirfd.into(), path).await?; + Ok(()) + } + + async fn path_filestat_get( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + flags: types::Lookupflags, + path: GuestPtr, + ) -> Result { + Ok( + Snapshot1::path_filestat_get(self, memory, dirfd.into(), flags.into(), path) + .await? + .into(), + ) + } + + async fn path_filestat_set_times( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + flags: types::Lookupflags, + path: GuestPtr, + atim: types::Timestamp, + mtim: types::Timestamp, + fst_flags: types::Fstflags, + ) -> Result<(), Error> { + Snapshot1::path_filestat_set_times( + self, + memory, + dirfd.into(), + flags.into(), + path, + atim, + mtim, + fst_flags.into(), + ) + .await?; + Ok(()) + } + + async fn path_link( + &mut self, + memory: &mut GuestMemory<'_>, + src_fd: types::Fd, + src_flags: types::Lookupflags, + src_path: GuestPtr, + target_fd: types::Fd, + target_path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_link( + self, + memory, + src_fd.into(), + src_flags.into(), + src_path, + target_fd.into(), + target_path, + ) + .await?; + Ok(()) + } + + async fn path_open( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + dirflags: types::Lookupflags, + path: GuestPtr, + oflags: types::Oflags, + fs_rights_base: types::Rights, + fs_rights_inheriting: types::Rights, + fdflags: types::Fdflags, + ) -> Result { + Ok(Snapshot1::path_open( + self, + memory, + dirfd.into(), + dirflags.into(), + path, + oflags.into(), + fs_rights_base.into(), + fs_rights_inheriting.into(), + fdflags.into(), + ) + .await? + .into()) + } + + async fn path_readlink( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + buf: GuestPtr, + buf_len: types::Size, + ) -> Result { + Ok(Snapshot1::path_readlink(self, memory, dirfd.into(), path, buf, buf_len).await?) + } + + async fn path_remove_directory( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_remove_directory(self, memory, dirfd.into(), path).await?; + Ok(()) + } + + async fn path_rename( + &mut self, + memory: &mut GuestMemory<'_>, + src_fd: types::Fd, + src_path: GuestPtr, + dest_fd: types::Fd, + dest_path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_rename( + self, + memory, + src_fd.into(), + src_path, + dest_fd.into(), + dest_path, + ) + .await?; + Ok(()) + } + + async fn path_symlink( + &mut self, + memory: &mut GuestMemory<'_>, + src_path: GuestPtr, + dirfd: types::Fd, + dest_path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_symlink(self, memory, src_path, dirfd.into(), dest_path).await?; + Ok(()) + } + + async fn path_unlink_file( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), Error> { + Snapshot1::path_unlink_file(self, memory, dirfd.into(), path).await?; + Ok(()) + } + + // The representation of `SubscriptionClock` is different in p0 and + // p1 so a bit of a hack is employed here. The change was to remove a + // field from `SubscriptionClock` so to implement this without copying too + // much the `subs` field is overwritten with p1-compatible structures + // and then the p1 implementation is used. Before returning though + // the old values are restored to pretend like we didn't overwrite them. + // + // Surely no one would pass overlapping pointers to this API right? + async fn poll_oneoff( + &mut self, + memory: &mut GuestMemory<'_>, + subs: GuestPtr, + events: GuestPtr, + nsubscriptions: types::Size, + ) -> Result { + let subs_array = subs.as_array(nsubscriptions); + let mut old_subs = Vec::new(); + for slot in subs_array.iter() { + let slot = slot?; + let sub = memory.read(slot)?; + old_subs.push(sub.clone()); + memory.write( + slot.cast(), + snapshot1_types::Subscription { + userdata: sub.userdata, + u: match sub.u { + types::SubscriptionU::Clock(c) => { + snapshot1_types::SubscriptionU::Clock(c.into()) + } + types::SubscriptionU::FdRead(c) => { + snapshot1_types::SubscriptionU::FdRead(c.into()) + } + types::SubscriptionU::FdWrite(c) => { + snapshot1_types::SubscriptionU::FdWrite(c.into()) + } + }, + }, + )?; + } + let ret = Snapshot1::poll_oneoff(self, memory, subs.cast(), events.cast(), nsubscriptions) + .await?; + for (sub, slot) in old_subs.into_iter().zip(subs_array.iter()) { + memory.write(slot?, sub)?; + } + Ok(ret) + } + + fn proc_exit( + &mut self, + memory: &mut GuestMemory<'_>, + status: types::Exitcode, + ) -> anyhow::Error { + Snapshot1::proc_exit(self, memory, status) + } + + fn proc_raise( + &mut self, + memory: &mut GuestMemory<'_>, + sig: types::Signal, + ) -> Result<(), Error> { + Snapshot1::proc_raise(self, memory, sig.into())?; + Ok(()) + } + + fn sched_yield(&mut self, memory: &mut GuestMemory<'_>) -> Result<(), Error> { + Snapshot1::sched_yield(self, memory)?; + Ok(()) + } + + fn random_get( + &mut self, + memory: &mut GuestMemory<'_>, + buf: GuestPtr, + buf_len: types::Size, + ) -> Result<(), Error> { + Snapshot1::random_get(self, memory, buf, buf_len)?; + Ok(()) + } + + fn sock_recv( + &mut self, + _memory: &mut GuestMemory<'_>, + _fd: types::Fd, + _ri_data: types::IovecArray, + _ri_flags: types::Riflags, + ) -> Result<(types::Size, types::Roflags), Error> { + Err(Error::trap(anyhow::Error::msg("sock_recv unsupported"))) + } + + fn sock_send( + &mut self, + _memory: &mut GuestMemory<'_>, + _fd: types::Fd, + _si_data: types::CiovecArray, + _si_flags: types::Siflags, + ) -> Result { + Err(Error::trap(anyhow::Error::msg("sock_send unsupported"))) + } + + fn sock_shutdown( + &mut self, + _memory: &mut GuestMemory<'_>, + _fd: types::Fd, + _how: types::Sdflags, + ) -> Result<(), Error> { + Err(Error::trap(anyhow::Error::msg("sock_shutdown unsupported"))) + } +} + +fn assert_iovec_array_same() { + // NB: this isn't enough to assert the types are the same, but it's + // something. Additionally p1 and p0 aren't changing any more + // and it's been manually verified that these two types are the same, so + // it's ok to cast between them. + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() + ); +} + +fn assert_ciovec_array_same() { + // NB: see above too + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() + ); +} + +impl From for Error { + fn from(error: snapshot1_types::Error) -> Error { + match error.downcast() { + Ok(errno) => Error::from(types::Errno::from(errno)), + Err(trap) => Error::trap(trap), + } + } +} + +/// Fd is a newtype wrapper around u32. Unwrap and wrap it. +impl From for snapshot1_types::Fd { + fn from(fd: types::Fd) -> snapshot1_types::Fd { + u32::from(fd).into() + } +} + +/// Fd is a newtype wrapper around u32. Unwrap and wrap it. +impl From for types::Fd { + fn from(fd: snapshot1_types::Fd) -> types::Fd { + u32::from(fd).into() + } +} + +/// Trivial conversion between two c-style enums that have the exact same set of variants. +/// Could we do something unsafe and not list all these variants out? Probably, but doing +/// it this way doesn't bother me much. I copy-pasted the list of variants out of the +/// rendered rustdocs. +/// LLVM ought to compile these From impls into no-ops, inshallah +macro_rules! convert_enum { + ($from:ty, $to:ty, $($var:ident),+) => { + impl From<$from> for $to { + fn from(e: $from) -> $to { + match e { + $( <$from>::$var => <$to>::$var, )+ + } + } + } + } +} +convert_enum!( + snapshot1_types::Errno, + types::Errno, + Success, + TooBig, + Acces, + Addrinuse, + Addrnotavail, + Afnosupport, + Again, + Already, + Badf, + Badmsg, + Busy, + Canceled, + Child, + Connaborted, + Connrefused, + Connreset, + Deadlk, + Destaddrreq, + Dom, + Dquot, + Exist, + Fault, + Fbig, + Hostunreach, + Idrm, + Ilseq, + Inprogress, + Intr, + Inval, + Io, + Isconn, + Isdir, + Loop, + Mfile, + Mlink, + Msgsize, + Multihop, + Nametoolong, + Netdown, + Netreset, + Netunreach, + Nfile, + Nobufs, + Nodev, + Noent, + Noexec, + Nolck, + Nolink, + Nomem, + Nomsg, + Noprotoopt, + Nospc, + Nosys, + Notconn, + Notdir, + Notempty, + Notrecoverable, + Notsock, + Notsup, + Notty, + Nxio, + Overflow, + Ownerdead, + Perm, + Pipe, + Proto, + Protonosupport, + Prototype, + Range, + Rofs, + Spipe, + Srch, + Stale, + Timedout, + Txtbsy, + Xdev, + Notcapable +); +convert_enum!( + types::Clockid, + snapshot1_types::Clockid, + Realtime, + Monotonic, + ProcessCputimeId, + ThreadCputimeId +); + +convert_enum!( + types::Advice, + snapshot1_types::Advice, + Normal, + Sequential, + Random, + Willneed, + Dontneed, + Noreuse +); +convert_enum!( + snapshot1_types::Filetype, + types::Filetype, + Directory, + BlockDevice, + CharacterDevice, + RegularFile, + SocketDgram, + SocketStream, + SymbolicLink, + Unknown +); +convert_enum!(types::Whence, snapshot1_types::Whence, Cur, End, Set); + +convert_enum!( + types::Signal, + snapshot1_types::Signal, + None, + Hup, + Int, + Quit, + Ill, + Trap, + Abrt, + Bus, + Fpe, + Kill, + Usr1, + Segv, + Usr2, + Pipe, + Alrm, + Term, + Chld, + Cont, + Stop, + Tstp, + Ttin, + Ttou, + Urg, + Xcpu, + Xfsz, + Vtalrm, + Prof, + Winch, + Poll, + Pwr, + Sys +); + +/// Prestat isn't a c-style enum, its a union where the variant has a payload. Its the only one of +/// those we need to convert, so write it by hand. +impl From for types::Prestat { + fn from(p: snapshot1_types::Prestat) -> types::Prestat { + match p { + snapshot1_types::Prestat::Dir(d) => types::Prestat::Dir(d.into()), + } + } +} + +/// Trivial conversion between two structs that have the exact same set of fields, +/// with recursive descent into the field types. +macro_rules! convert_struct { + ($from:ty, $to:path, $($field:ident),+) => { + impl From<$from> for $to { + fn from(e: $from) -> $to { + $to { + $( $field: e.$field.into(), )+ + } + } + } + } +} + +convert_struct!(snapshot1_types::PrestatDir, types::PrestatDir, pr_name_len); +convert_struct!( + snapshot1_types::Fdstat, + types::Fdstat, + fs_filetype, + fs_rights_base, + fs_rights_inheriting, + fs_flags +); +convert_struct!( + types::SubscriptionClock, + snapshot1_types::SubscriptionClock, + id, + timeout, + precision, + flags +); +convert_struct!( + types::SubscriptionFdReadwrite, + snapshot1_types::SubscriptionFdReadwrite, + file_descriptor +); + +/// Snapshot1 Filestat is incompatible with Snapshot0 Filestat - the nlink +/// field is u32 on this Filestat, and u64 on theirs. If you've got more than +/// 2^32 links I don't know what to tell you +impl From for types::Filestat { + fn from(f: snapshot1_types::Filestat) -> types::Filestat { + types::Filestat { + dev: f.dev, + ino: f.ino, + filetype: f.filetype.into(), + nlink: f.nlink.try_into().unwrap_or(u32::MAX), + size: f.size, + atim: f.atim, + mtim: f.mtim, + ctim: f.ctim, + } + } +} + +/// Trivial conversion between two bitflags that have the exact same set of flags. +macro_rules! convert_flags { + ($from:ty, $to:ty, $($flag:ident),+) => { + impl From<$from> for $to { + fn from(f: $from) -> $to { + let mut out = <$to>::empty(); + $( + if f.contains(<$from>::$flag) { + out |= <$to>::$flag; + } + )+ + out + } + } + } +} + +/// Need to convert in both directions? This saves listing out the flags twice +macro_rules! convert_flags_bidirectional { + ($from:ty, $to:ty, $($flag:tt)*) => { + convert_flags!($from, $to, $($flag)*); + convert_flags!($to, $from, $($flag)*); + } +} + +convert_flags_bidirectional!( + snapshot1_types::Fdflags, + types::Fdflags, + APPEND, + DSYNC, + NONBLOCK, + RSYNC, + SYNC +); +convert_flags!( + types::Lookupflags, + snapshot1_types::Lookupflags, + SYMLINK_FOLLOW +); +convert_flags!( + types::Fstflags, + snapshot1_types::Fstflags, + ATIM, + ATIM_NOW, + MTIM, + MTIM_NOW +); +convert_flags!( + types::Oflags, + snapshot1_types::Oflags, + CREAT, + DIRECTORY, + EXCL, + TRUNC +); +convert_flags_bidirectional!( + types::Rights, + snapshot1_types::Rights, + FD_DATASYNC, + FD_READ, + FD_SEEK, + FD_FDSTAT_SET_FLAGS, + FD_SYNC, + FD_TELL, + FD_WRITE, + FD_ADVISE, + FD_ALLOCATE, + PATH_CREATE_DIRECTORY, + PATH_CREATE_FILE, + PATH_LINK_SOURCE, + PATH_LINK_TARGET, + PATH_OPEN, + FD_READDIR, + PATH_READLINK, + PATH_RENAME_SOURCE, + PATH_RENAME_TARGET, + PATH_FILESTAT_GET, + PATH_FILESTAT_SET_SIZE, + PATH_FILESTAT_SET_TIMES, + FD_FILESTAT_GET, + FD_FILESTAT_SET_SIZE, + FD_FILESTAT_SET_TIMES, + PATH_SYMLINK, + PATH_REMOVE_DIRECTORY, + PATH_UNLINK_FILE, + POLL_FD_READWRITE, + SOCK_SHUTDOWN +); +convert_flags!( + types::Subclockflags, + snapshot1_types::Subclockflags, + SUBSCRIPTION_CLOCK_ABSTIME +); + +impl From for types::Error { + fn from(err: GuestError) -> Self { + snapshot1_types::Error::from(err).into() + } +} diff --git a/crates/wasi/src/p1.rs b/crates/wasi/src/p1.rs new file mode 100644 index 00000000..0b402990 --- /dev/null +++ b/crates/wasi/src/p1.rs @@ -0,0 +1,2655 @@ +//! Bindings for WASIp1 aka Preview 1 aka `wasi_snapshot_preview1`. +//! +//! This module contains runtime support for configuring and executing +//! WASIp1-using core WebAssembly modules. Support for WASIp1 is built on top of +//! support for WASIp2 available at [the crate root](crate), but that's just an +//! internal implementation detail. +//! +//! Unlike the crate root, support for WASIp1 centers around two APIs: +//! +//! * [`WasiP1Ctx`] +//! * [`add_to_linker_sync`] (or [`add_to_linker_async`]) +//! +//! First a [`WasiCtxBuilder`] will be used and finalized with the [`build_p1`] +//! method to create a [`WasiCtx`]. Next a [`wasmtime::Linker`] is configured +//! with WASI imports by using the `add_to_linker_*` desired (sync or async +//! depending on [`Config::async_support`]). +//! +//! Note that WASIp1 is not as extensible or configurable as WASIp2 so the +//! support in this module is enough to run wasm modules but any customization +//! beyond that [`WasiCtxBuilder`] already supports is not possible yet. +//! +//! [`WasiCtxBuilder`]: crate::p2::WasiCtxBuilder +//! [`build_p1`]: crate::p2::WasiCtxBuilder::build_p1 +//! [`Config::async_support`]: wasmtime::Config::async_support +//! +//! # Components vs Modules +//! +//! Note that WASIp1 does not work for components at this time, only core wasm +//! modules. That means this module is only for users of [`wasmtime::Module`] +//! and [`wasmtime::Linker`], for example. If you're using +//! [`wasmtime::component::Component`] or [`wasmtime::component::Linker`] you'll +//! want the WASIp2 [support this crate has](crate) instead. +//! +//! # Examples +//! +//! ```no_run +//! use wasmtime::{Result, Engine, Linker, Module, Store}; +//! use wash_wasi::p1::{self, WasiP1Ctx}; +//! use wash_wasi::WasiCtxBuilder; +//! +//! // An example of executing a WASIp1 "command" +//! fn main() -> Result<()> { +//! let args = std::env::args().skip(1).collect::>(); +//! let engine = Engine::default(); +//! let module = Module::from_file(&engine, &args[0])?; +//! +//! let mut linker: Linker = Linker::new(&engine); +//! p1::add_to_linker_async(&mut linker, |t| t)?; +//! let pre = linker.instantiate_pre(&module)?; +//! +//! let wasi_ctx = WasiCtxBuilder::new() +//! .inherit_stdio() +//! .inherit_env() +//! .args(&args) +//! .build_p1(); +//! +//! let mut store = Store::new(&engine, wasi_ctx); +//! let instance = pre.instantiate(&mut store)?; +//! let func = instance.get_typed_func::<(), ()>(&mut store, "_start")?; +//! func.call(&mut store, ())?; +//! +//! Ok(()) +//! } +//! ``` + +use crate::cli::WasiCliView as _; +use crate::clocks::WasiClocksView as _; +use crate::filesystem::WasiFilesystemView as _; +use crate::p2::bindings::{ + cli::{ + stderr::Host as _, stdin::Host as _, stdout::Host as _, terminal_input, terminal_output, + terminal_stderr::Host as _, terminal_stdin::Host as _, terminal_stdout::Host as _, + }, + clocks::{monotonic_clock, wall_clock}, + filesystem::types as filesystem, +}; +use crate::p2::{FsError, IsATTY}; +use crate::{ResourceTable, WasiCtx, WasiCtxView, WasiView}; +use anyhow::{Context, bail}; +use std::collections::{BTreeMap, BTreeSet, HashSet, btree_map}; +use std::mem::{self, size_of, size_of_val}; +use std::slice; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use system_interface::fs::FileIoExt; +use wasmtime::component::Resource; +use wasmtime_wasi_io::{ + bindings::wasi::io::streams, + streams::{StreamError, StreamResult}, +}; +use wiggle::tracing::instrument; +use wiggle::{GuestError, GuestMemory, GuestPtr, GuestType}; + +// Bring all WASI traits in scope that this implementation builds on. +use crate::p2::bindings::cli::environment::Host as _; +use crate::p2::bindings::filesystem::types::HostDescriptor as _; +use crate::p2::bindings::random::random::Host as _; +use wasmtime_wasi_io::bindings::wasi::io::poll::Host as _; + +/// Structure containing state for WASIp1. +/// +/// This structure is created through [`WasiCtxBuilder::build_p1`] and is +/// configured through the various methods of [`WasiCtxBuilder`]. This structure +/// itself implements generated traits for WASIp1 as well as [`WasiView`] to +/// have access to WASIp2. +/// +/// Instances of [`WasiP1Ctx`] are typically stored within the `T` of +/// [`Store`](wasmtime::Store). +/// +/// [`WasiCtxBuilder::build_p1`]: crate::p2::WasiCtxBuilder::build_p1 +/// [`WasiCtxBuilder`]: crate::p2::WasiCtxBuilder +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Result, Linker}; +/// use wash_wasi::p1::{self, WasiP1Ctx}; +/// use wash_wasi::WasiCtxBuilder; +/// +/// struct MyState { +/// // ... custom state as necessary ... +/// +/// wasi: WasiP1Ctx, +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// MyState { +/// // .. initialize custom state if needed .. +/// +/// wasi: WasiCtxBuilder::new() +/// .arg("./foo.wasm") +/// // .. more customization if necesssary .. +/// .build_p1(), +/// } +/// } +/// } +/// +/// fn add_to_linker(linker: &mut Linker) -> Result<()> { +/// p1::add_to_linker_sync(linker, |my_state| &mut my_state.wasi)?; +/// Ok(()) +/// } +/// ``` +pub struct WasiP1Ctx { + table: ResourceTable, + wasi: WasiCtx, + adapter: WasiP1Adapter, +} + +impl WasiP1Ctx { + pub(crate) fn new(wasi: WasiCtx) -> Self { + Self { + table: ResourceTable::new(), + wasi, + adapter: WasiP1Adapter::new(), + } + } +} + +impl WasiView for WasiP1Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +#[derive(Debug)] +struct File { + /// The handle to the preview2 descriptor of type [`crate::filesystem::Descriptor::File`]. + fd: Resource, + + /// The current-position pointer. + position: Arc, + + /// In append mode, all writes append to the file. + append: bool, + + /// When blocking, read and write calls dispatch to blocking_read and + /// blocking_check_write on the underlying streams. When false, read and write + /// dispatch to stream's plain read and check_write. + blocking_mode: BlockingMode, +} + +/// NB: p1 files always use blocking writes regardless of what +/// they're configured to use since OSes don't have nonblocking +/// reads/writes anyway. This behavior originated in the first +/// implementation of WASIp1 where flags were propagated to the +/// OS and the OS ignored the nonblocking flag for files +/// generally. +#[derive(Clone, Copy, Debug)] +enum BlockingMode { + Blocking, + NonBlocking, +} +impl BlockingMode { + fn from_fdflags(flags: &types::Fdflags) -> Self { + if flags.contains(types::Fdflags::NONBLOCK) { + BlockingMode::NonBlocking + } else { + BlockingMode::Blocking + } + } + async fn read( + &self, + host: &mut impl streams::HostInputStream, + input_stream: Resource, + max_size: usize, + ) -> Result, types::Error> { + let max_size = max_size.try_into().unwrap_or(u64::MAX); + match streams::HostInputStream::blocking_read(host, input_stream, max_size).await { + Ok(r) if r.is_empty() => Err(types::Errno::Intr.into()), + Ok(r) => Ok(r), + Err(StreamError::Closed) => Ok(Vec::new()), + Err(e) => Err(e.into()), + } + } + async fn write( + &self, + memory: &mut GuestMemory<'_>, + host: &mut impl streams::HostOutputStream, + output_stream: Resource, + bytes: GuestPtr<[u8]>, + ) -> StreamResult { + use streams::HostOutputStream as Streams; + + let bytes = memory + .as_cow(bytes) + .map_err(|e| StreamError::Trap(e.into()))?; + let mut bytes = &bytes[..]; + + let total = bytes.len(); + while !bytes.is_empty() { + // NOTE: blocking_write_and_flush takes at most one 4k buffer. + let len = bytes.len().min(4096); + let (chunk, rest) = bytes.split_at(len); + bytes = rest; + + Streams::blocking_write_and_flush(host, output_stream.borrowed(), Vec::from(chunk)) + .await? + } + + Ok(total) + } +} + +#[derive(Debug)] +enum Descriptor { + Stdin { + stream: Resource, + isatty: IsATTY, + }, + Stdout { + stream: Resource, + isatty: IsATTY, + }, + Stderr { + stream: Resource, + isatty: IsATTY, + }, + /// A fd of type [`crate::filesystem::Descriptor::Dir`] + Directory { + fd: Resource, + /// The path this directory was preopened as. + /// `None` means this directory was opened using `open-at`. + preopen_path: Option, + }, + /// A fd of type [`crate::filesystem::Descriptor::File`] + File(File), +} + +#[derive(Debug, Default)] +struct WasiP1Adapter { + descriptors: Option, +} + +#[derive(Debug, Default)] +struct Descriptors { + used: BTreeMap, + free: BTreeSet, +} + +impl Descriptors { + /// Initializes [Self] using `preopens` + fn new(host: &mut WasiP1Ctx) -> Result { + let mut descriptors = Self::default(); + descriptors.push(Descriptor::Stdin { + stream: host + .cli() + .get_stdin() + .context("failed to call `get-stdin`") + .map_err(types::Error::trap)?, + isatty: if let Some(term_in) = host + .cli() + .get_terminal_stdin() + .context("failed to call `get-terminal-stdin`") + .map_err(types::Error::trap)? + { + terminal_input::HostTerminalInput::drop(&mut host.cli(), term_in) + .context("failed to call `drop-terminal-input`") + .map_err(types::Error::trap)?; + IsATTY::Yes + } else { + IsATTY::No + }, + })?; + descriptors.push(Descriptor::Stdout { + stream: host + .cli() + .get_stdout() + .context("failed to call `get-stdout`") + .map_err(types::Error::trap)?, + isatty: if let Some(term_out) = host + .cli() + .get_terminal_stdout() + .context("failed to call `get-terminal-stdout`") + .map_err(types::Error::trap)? + { + terminal_output::HostTerminalOutput::drop(&mut host.cli(), term_out) + .context("failed to call `drop-terminal-output`") + .map_err(types::Error::trap)?; + IsATTY::Yes + } else { + IsATTY::No + }, + })?; + descriptors.push(Descriptor::Stderr { + stream: host + .cli() + .get_stderr() + .context("failed to call `get-stderr`") + .map_err(types::Error::trap)?, + isatty: if let Some(term_out) = host + .cli() + .get_terminal_stderr() + .context("failed to call `get-terminal-stderr`") + .map_err(types::Error::trap)? + { + terminal_output::HostTerminalOutput::drop(&mut host.cli(), term_out) + .context("failed to call `drop-terminal-output`") + .map_err(types::Error::trap)?; + IsATTY::Yes + } else { + IsATTY::No + }, + })?; + + for dir in host + .filesystem() + .get_directories() + .context("failed to call `get-directories`") + .map_err(types::Error::trap)? + { + descriptors.push(Descriptor::Directory { + fd: dir.0, + preopen_path: Some(dir.1), + })?; + } + Ok(descriptors) + } + + /// Returns next descriptor number, which was never assigned + fn unused(&self) -> Result { + match self.used.last_key_value() { + Some((fd, _)) => { + if let Some(fd) = fd.checked_add(1) { + return Ok(fd); + } + if self.used.len() == u32::MAX as usize { + return Err(types::Errno::Loop.into()); + } + // TODO: Optimize + Ok((0..u32::MAX) + .rev() + .find(|fd| !self.used.contains_key(fd)) + .expect("failed to find an unused file descriptor")) + } + None => Ok(0), + } + } + + /// Pushes the [Descriptor] returning corresponding number. + /// This operation will try to reuse numbers previously removed via [`Self::remove`] + /// and rely on [`Self::unused`] if no free numbers are recorded + fn push(&mut self, desc: Descriptor) -> Result { + let fd = if let Some(fd) = self.free.pop_last() { + fd + } else { + self.unused()? + }; + assert!(self.used.insert(fd, desc).is_none()); + Ok(fd) + } +} + +impl WasiP1Adapter { + fn new() -> Self { + Self::default() + } +} + +/// A mutably-borrowed `WasiP1Ctx`, which provides access to the stored +/// state. It can be thought of as an in-flight [`WasiP1Adapter`] transaction, all +/// changes will be recorded in the underlying [`WasiP1Adapter`] returned by +/// [`WasiPreview1View::adapter_mut`] on [`Drop`] of this struct. +// NOTE: This exists for the most part just due to the fact that `bindgen` generates methods with +// `&mut self` receivers and so this struct lets us extend the lifetime of the `&mut self` borrow +// of the [`WasiPreview1View`] to provide means to return mutably and immutably borrowed [`Descriptors`] +// without having to rely on something like `Arc>`, while also being able to +// call methods like [`Descriptor::is_file`] and hiding complexity from p1 method implementations. +struct Transaction<'a> { + view: &'a mut WasiP1Ctx, + descriptors: Descriptors, +} + +impl Drop for Transaction<'_> { + /// Record changes in the [`WasiP1Adapter`] . + fn drop(&mut self) { + let descriptors = mem::take(&mut self.descriptors); + self.view.adapter.descriptors = Some(descriptors); + } +} + +impl Transaction<'_> { + /// Borrows [`Descriptor`] corresponding to `fd`. + /// + /// # Errors + /// + /// Returns [`types::Errno::Badf`] if no [`Descriptor`] is found + fn get_descriptor(&self, fd: types::Fd) -> Result<&Descriptor> { + let fd = fd.into(); + let desc = self.descriptors.used.get(&fd).ok_or(types::Errno::Badf)?; + Ok(desc) + } + + /// Borrows [`File`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] + fn get_file(&self, fd: types::Fd) -> Result<&File> { + let fd = fd.into(); + match self.descriptors.used.get(&fd) { + Some(Descriptor::File(file)) => Ok(file), + _ => Err(types::Errno::Badf.into()), + } + } + + /// Mutably borrows [`File`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] + fn get_file_mut(&mut self, fd: types::Fd) -> Result<&mut File> { + let fd = fd.into(); + match self.descriptors.used.get_mut(&fd) { + Some(Descriptor::File(file)) => Ok(file), + _ => Err(types::Errno::Badf.into()), + } + } + + /// Borrows [`File`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] + /// + /// # Errors + /// + /// Returns [`types::Errno::Spipe`] if the descriptor corresponds to stdio + fn get_seekable(&self, fd: types::Fd) -> Result<&File> { + let fd = fd.into(); + match self.descriptors.used.get(&fd) { + Some(Descriptor::File(file)) => Ok(file), + Some( + Descriptor::Stdin { .. } | Descriptor::Stdout { .. } | Descriptor::Stderr { .. }, + ) => { + // NOTE: legacy implementation returns SPIPE here + Err(types::Errno::Spipe.into()) + } + _ => Err(types::Errno::Badf.into()), + } + } + + /// Returns [`filesystem::Descriptor`] corresponding to `fd` + fn get_fd(&self, fd: types::Fd) -> Result> { + match self.get_descriptor(fd)? { + Descriptor::File(File { fd, .. }) => Ok(fd.borrowed()), + Descriptor::Directory { fd, .. } => Ok(fd.borrowed()), + Descriptor::Stdin { .. } | Descriptor::Stdout { .. } | Descriptor::Stderr { .. } => { + Err(types::Errno::Badf.into()) + } + } + } + + /// Returns [`filesystem::Descriptor`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] + fn get_file_fd(&self, fd: types::Fd) -> Result> { + self.get_file(fd).map(|File { fd, .. }| fd.borrowed()) + } + + /// Returns [`filesystem::Descriptor`] corresponding to `fd` + /// if it describes a [`Descriptor::Directory`] + fn get_dir_fd(&self, fd: types::Fd) -> Result> { + let fd = fd.into(); + match self.descriptors.used.get(&fd) { + Some(Descriptor::Directory { fd, .. }) => Ok(fd.borrowed()), + _ => Err(types::Errno::Badf.into()), + } + } +} + +impl WasiP1Ctx { + /// Lazily initializes [`WasiP1Adapter`] returned by [`WasiPreview1View::adapter_mut`] + /// and returns [`Transaction`] on success + fn transact(&mut self) -> Result, types::Error> { + let descriptors = if let Some(descriptors) = self.adapter.descriptors.take() { + descriptors + } else { + Descriptors::new(self)? + }; + Ok(Transaction { + view: self, + descriptors, + }) + } + + /// Lazily initializes [`WasiP1Adapter`] returned by [`WasiPreview1View::adapter_mut`] + /// and returns [`filesystem::Descriptor`] corresponding to `fd` + fn get_fd(&mut self, fd: types::Fd) -> Result, types::Error> { + let st = self.transact()?; + let fd = st.get_fd(fd)?; + Ok(fd) + } + + /// Lazily initializes [`WasiP1Adapter`] returned by [`WasiPreview1View::adapter_mut`] + /// and returns [`filesystem::Descriptor`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] of [`crate::p2::filesystem::File`] type + fn get_file_fd( + &mut self, + fd: types::Fd, + ) -> Result, types::Error> { + let st = self.transact()?; + let fd = st.get_file_fd(fd)?; + Ok(fd) + } + + /// Lazily initializes [`WasiP1Adapter`] returned by [`WasiPreview1View::adapter_mut`] + /// and returns [`filesystem::Descriptor`] corresponding to `fd` + /// if it describes a [`Descriptor::File`] or [`Descriptor::PreopenDirectory`] + /// of [`crate::p2::filesystem::Dir`] type + fn get_dir_fd( + &mut self, + fd: types::Fd, + ) -> Result, types::Error> { + let st = self.transact()?; + let fd = st.get_dir_fd(fd)?; + Ok(fd) + } + + /// Shared implementation of `fd_write` and `fd_pwrite`. + async fn fd_write_impl( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ciovs: types::CiovecArray, + write: FdWrite, + ) -> Result { + let t = self.transact()?; + let desc = t.get_descriptor(fd)?; + match desc { + Descriptor::File(File { + fd, + append, + position, + // NB: files always use blocking writes regardless of what + // they're configured to use since OSes don't have nonblocking + // reads/writes anyway. This behavior originated in the first + // implementation of WASIp1 where flags were propagated to the + // OS and the OS ignored the nonblocking flag for files + // generally. + blocking_mode: _, + }) => { + let fd = fd.borrowed(); + let position = position.clone(); + let pos = position.load(Ordering::Relaxed); + let append = *append; + drop(t); + let f = self.table.get(&fd)?.file()?; + let buf = first_non_empty_ciovec(memory, ciovs)?; + + let do_write = move |f: &cap_std::fs::File, buf: &[u8]| match (append, write) { + // Note that this is implementing Linux semantics of + // `pwrite` where the offset is ignored if the file was + // opened in append mode. + (true, _) => f.append(&buf), + (false, FdWrite::At(pos)) => f.write_at(&buf, pos), + (false, FdWrite::AtCur) => f.write_at(&buf, pos), + }; + + let nwritten = match f.as_blocking_file() { + // If we can block then skip the copy out of wasm memory and + // write directly to `f`. + Some(f) => do_write(f, &memory.as_cow(buf)?), + // ... otherwise copy out of wasm memory and use + // `spawn_blocking` to do this write in a thread that can + // block. + None => { + let buf = memory.to_vec(buf)?; + f.run_blocking(move |f| do_write(f, &buf)).await + } + }; + + let nwritten = nwritten.map_err(|e| StreamError::LastOperationFailed(e.into()))?; + + // If this was a write at the current position then update the + // current position with the result, otherwise the current + // position is left unmodified. + if let FdWrite::AtCur = write { + if append { + let len = self.filesystem().stat(fd).await?; + position.store(len.size, Ordering::Relaxed); + } else { + let pos = pos + .checked_add(nwritten as u64) + .ok_or(types::Errno::Overflow)?; + position.store(pos, Ordering::Relaxed); + } + } + Ok(nwritten.try_into()?) + } + Descriptor::Stdout { stream, .. } | Descriptor::Stderr { stream, .. } => { + match write { + // Reject calls to `fd_pwrite` on stdio descriptors... + FdWrite::At(_) => return Err(types::Errno::Spipe.into()), + // ... but allow calls to `fd_write` + FdWrite::AtCur => {} + } + let stream = stream.borrowed(); + drop(t); + let buf = first_non_empty_ciovec(memory, ciovs)?; + let n = BlockingMode::Blocking + .write(memory, &mut self.table, stream, buf) + .await? + .try_into()?; + Ok(n) + } + _ => Err(types::Errno::Badf.into()), + } + } +} + +#[derive(Copy, Clone)] +enum FdWrite { + At(u64), + AtCur, +} + +/// Adds asynchronous versions of all WASIp1 functions to the +/// [`wasmtime::Linker`] provided. +/// +/// This method will add WASIp1 functions to `linker`. Access to [`WasiP1Ctx`] +/// is provided with `f` by projecting from the store-local state of `T` to +/// [`WasiP1Ctx`]. The closure `f` is invoked every time a WASIp1 function is +/// called to get access to [`WasiP1Ctx`] from `T`. The returned [`WasiP1Ctx`] is +/// used to implement I/O and controls what each function will return. +/// +/// It's recommended that [`WasiP1Ctx`] is stored as a field in `T` or that `T = +/// WasiP1Ctx` itself. The closure `f` should be a small projection (e.g. `&mut +/// arg.field`) or something otherwise "small" as it will be executed every time +/// a WASI call is made. +/// +/// Note that this function is intended for use with +/// [`Config::async_support(true)`]. If you're looking for a synchronous version +/// see [`add_to_linker_sync`]. +/// +/// [`Config::async_support(true)`]: wasmtime::Config::async_support +/// +/// # Examples +/// +/// If the `T` in `Linker` is just `WasiP1Ctx`: +/// +/// ```no_run +/// use wasmtime::{Result, Linker, Engine, Config}; +/// use wash_wasi::p1::{self, WasiP1Ctx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker: Linker = Linker::new(&engine); +/// p1::add_to_linker_async(&mut linker, |cx| cx)?; +/// +/// // ... continue to add more to `linker` as necessary and use it ... +/// +/// Ok(()) +/// } +/// ``` +/// +/// If the `T` in `Linker` is custom state: +/// +/// ```no_run +/// use wasmtime::{Result, Linker, Engine, Config}; +/// use wash_wasi::p1::{self, WasiP1Ctx}; +/// +/// struct MyState { +/// // .. other custom state here .. +/// +/// wasi: WasiP1Ctx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker: Linker = Linker::new(&engine); +/// p1::add_to_linker_async(&mut linker, |cx| &mut cx.wasi)?; +/// +/// // ... continue to add more to `linker` as necessary and use it ... +/// +/// Ok(()) +/// } +/// ``` +pub fn add_to_linker_async( + linker: &mut wasmtime::Linker, + f: impl Fn(&mut T) -> &mut WasiP1Ctx + Copy + Send + Sync + 'static, +) -> anyhow::Result<()> { + crate::p1::wasi_snapshot_preview1::add_to_linker(linker, f) +} + +/// Adds synchronous versions of all WASIp1 functions to the +/// [`wasmtime::Linker`] provided. +/// +/// This method will add WASIp1 functions to `linker`. Access to [`WasiP1Ctx`] +/// is provided with `f` by projecting from the store-local state of `T` to +/// [`WasiP1Ctx`]. The closure `f` is invoked every time a WASIp1 function is +/// called to get access to [`WasiP1Ctx`] from `T`. The returned [`WasiP1Ctx`] is +/// used to implement I/O and controls what each function will return. +/// +/// It's recommended that [`WasiP1Ctx`] is stored as a field in `T` or that `T = +/// WasiP1Ctx` itself. The closure `f` should be a small projection (e.g. `&mut +/// arg.field`) or something otherwise "small" as it will be executed every time +/// a WASI call is made. +/// +/// Note that this function is intended for use with +/// [`Config::async_support(false)`]. If you're looking for a synchronous version +/// see [`add_to_linker_async`]. +/// +/// [`Config::async_support(false)`]: wasmtime::Config::async_support +/// +/// # Examples +/// +/// If the `T` in `Linker` is just `WasiP1Ctx`: +/// +/// ```no_run +/// use wasmtime::{Result, Linker, Engine, Config}; +/// use wash_wasi::p1::{self, WasiP1Ctx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker: Linker = Linker::new(&engine); +/// p1::add_to_linker_async(&mut linker, |cx| cx)?; +/// +/// // ... continue to add more to `linker` as necessary and use it ... +/// +/// Ok(()) +/// } +/// ``` +/// +/// If the `T` in `Linker` is custom state: +/// +/// ```no_run +/// use wasmtime::{Result, Linker, Engine, Config}; +/// use wash_wasi::p1::{self, WasiP1Ctx}; +/// +/// struct MyState { +/// // .. other custom state here .. +/// +/// wasi: WasiP1Ctx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker: Linker = Linker::new(&engine); +/// p1::add_to_linker_async(&mut linker, |cx| &mut cx.wasi)?; +/// +/// // ... continue to add more to `linker` as necessary and use it ... +/// +/// Ok(()) +/// } +/// ``` +pub fn add_to_linker_sync( + linker: &mut wasmtime::Linker, + f: impl Fn(&mut T) -> &mut WasiP1Ctx + Copy + Send + Sync + 'static, +) -> anyhow::Result<()> { + sync::add_wasi_snapshot_preview1_to_linker(linker, f) +} + +// Generate the wasi_snapshot_preview1::WasiSnapshotPreview1 trait, +// and the module types. +// None of the generated modules, traits, or types should be used externally +// to this module. +wiggle::from_witx!({ + witx: ["witx/p1/wasi_snapshot_preview1.witx"], + async: { + wasi_snapshot_preview1::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, + errors: { errno => trappable Error }, +}); + +pub(crate) mod sync { + use anyhow::Result; + use std::future::Future; + + wiggle::wasmtime_integration!({ + witx: ["witx/p1/wasi_snapshot_preview1.witx"], + target: super, + block_on[in_tokio]: { + wasi_snapshot_preview1::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, + errors: { errno => trappable Error }, + }); + + // Small wrapper around `in_tokio` to add a `Result` layer which is always + // `Ok` + fn in_tokio(future: F) -> Result { + Ok(crate::runtime::in_tokio(future)) + } +} + +impl wiggle::GuestErrorType for types::Errno { + fn success() -> Self { + Self::Success + } +} + +impl From for types::Error { + fn from(err: StreamError) -> Self { + match err { + StreamError::Closed => types::Errno::Io.into(), + StreamError::LastOperationFailed(e) => match e.downcast::() { + Ok(err) => filesystem::ErrorCode::from(err).into(), + Err(e) => { + tracing::debug!("dropping error {e:?}"); + types::Errno::Io.into() + } + }, + StreamError::Trap(e) => types::Error::trap(e), + } + } +} + +impl From for types::Error { + fn from(err: FsError) -> Self { + match err.downcast() { + Ok(code) => code.into(), + Err(e) => types::Error::trap(e), + } + } +} + +fn systimespec(set: bool, ts: types::Timestamp, now: bool) -> Result { + if set && now { + Err(types::Errno::Inval.into()) + } 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) + } +} + +impl TryFrom for types::Timestamp { + type Error = types::Errno; + + fn try_from( + wall_clock::Datetime { + seconds, + nanoseconds, + }: wall_clock::Datetime, + ) -> Result { + types::Timestamp::from(seconds) + .checked_mul(1_000_000_000) + .and_then(|ns| ns.checked_add(nanoseconds.into())) + .ok_or(types::Errno::Overflow) + } +} + +impl From for filesystem::PathFlags { + fn from(flags: types::Lookupflags) -> Self { + if flags.contains(types::Lookupflags::SYMLINK_FOLLOW) { + filesystem::PathFlags::SYMLINK_FOLLOW + } else { + filesystem::PathFlags::empty() + } + } +} + +impl From for filesystem::OpenFlags { + fn from(flags: types::Oflags) -> Self { + let mut out = filesystem::OpenFlags::empty(); + if flags.contains(types::Oflags::CREAT) { + out |= filesystem::OpenFlags::CREATE; + } + if flags.contains(types::Oflags::DIRECTORY) { + out |= filesystem::OpenFlags::DIRECTORY; + } + if flags.contains(types::Oflags::EXCL) { + out |= filesystem::OpenFlags::EXCLUSIVE; + } + if flags.contains(types::Oflags::TRUNC) { + out |= filesystem::OpenFlags::TRUNCATE; + } + out + } +} + +impl From for filesystem::Advice { + fn from(advice: types::Advice) -> Self { + match advice { + types::Advice::Normal => filesystem::Advice::Normal, + types::Advice::Sequential => filesystem::Advice::Sequential, + types::Advice::Random => filesystem::Advice::Random, + types::Advice::Willneed => filesystem::Advice::WillNeed, + types::Advice::Dontneed => filesystem::Advice::DontNeed, + types::Advice::Noreuse => filesystem::Advice::NoReuse, + } + } +} + +impl TryFrom for types::Filetype { + type Error = anyhow::Error; + + fn try_from(ty: filesystem::DescriptorType) -> Result { + match ty { + filesystem::DescriptorType::RegularFile => Ok(types::Filetype::RegularFile), + filesystem::DescriptorType::Directory => Ok(types::Filetype::Directory), + filesystem::DescriptorType::BlockDevice => Ok(types::Filetype::BlockDevice), + filesystem::DescriptorType::CharacterDevice => Ok(types::Filetype::CharacterDevice), + // p1 never had a FIFO code. + filesystem::DescriptorType::Fifo => Ok(types::Filetype::Unknown), + // TODO: Add a way to disginguish between FILETYPE_SOCKET_STREAM and + // FILETYPE_SOCKET_DGRAM. + filesystem::DescriptorType::Socket => { + bail!("sockets are not currently supported") + } + filesystem::DescriptorType::SymbolicLink => Ok(types::Filetype::SymbolicLink), + filesystem::DescriptorType::Unknown => Ok(types::Filetype::Unknown), + } + } +} + +impl From for types::Filetype { + fn from(isatty: IsATTY) -> Self { + match isatty { + IsATTY::Yes => types::Filetype::CharacterDevice, + IsATTY::No => types::Filetype::Unknown, + } + } +} + +impl From for types::Errno { + fn from(code: crate::filesystem::ErrorCode) -> Self { + match code { + crate::filesystem::ErrorCode::Access => types::Errno::Acces, + crate::filesystem::ErrorCode::Already => types::Errno::Already, + crate::filesystem::ErrorCode::BadDescriptor => types::Errno::Badf, + crate::filesystem::ErrorCode::Busy => types::Errno::Busy, + crate::filesystem::ErrorCode::Exist => types::Errno::Exist, + crate::filesystem::ErrorCode::FileTooLarge => types::Errno::Fbig, + crate::filesystem::ErrorCode::IllegalByteSequence => types::Errno::Ilseq, + crate::filesystem::ErrorCode::InProgress => types::Errno::Inprogress, + crate::filesystem::ErrorCode::Interrupted => types::Errno::Intr, + crate::filesystem::ErrorCode::Invalid => types::Errno::Inval, + crate::filesystem::ErrorCode::Io => types::Errno::Io, + crate::filesystem::ErrorCode::IsDirectory => types::Errno::Isdir, + crate::filesystem::ErrorCode::Loop => types::Errno::Loop, + crate::filesystem::ErrorCode::TooManyLinks => types::Errno::Mlink, + crate::filesystem::ErrorCode::NameTooLong => types::Errno::Nametoolong, + crate::filesystem::ErrorCode::NoEntry => types::Errno::Noent, + crate::filesystem::ErrorCode::InsufficientMemory => types::Errno::Nomem, + crate::filesystem::ErrorCode::InsufficientSpace => types::Errno::Nospc, + crate::filesystem::ErrorCode::Unsupported => types::Errno::Notsup, + crate::filesystem::ErrorCode::NotDirectory => types::Errno::Notdir, + crate::filesystem::ErrorCode::NotEmpty => types::Errno::Notempty, + crate::filesystem::ErrorCode::Overflow => types::Errno::Overflow, + crate::filesystem::ErrorCode::NotPermitted => types::Errno::Perm, + crate::filesystem::ErrorCode::Pipe => types::Errno::Pipe, + crate::filesystem::ErrorCode::InvalidSeek => types::Errno::Spipe, + } + } +} + +impl From for types::Errno { + fn from(code: filesystem::ErrorCode) -> Self { + match code { + filesystem::ErrorCode::Access => types::Errno::Acces, + filesystem::ErrorCode::WouldBlock => types::Errno::Again, + filesystem::ErrorCode::Already => types::Errno::Already, + filesystem::ErrorCode::BadDescriptor => types::Errno::Badf, + filesystem::ErrorCode::Busy => types::Errno::Busy, + filesystem::ErrorCode::Deadlock => types::Errno::Deadlk, + filesystem::ErrorCode::Quota => types::Errno::Dquot, + filesystem::ErrorCode::Exist => types::Errno::Exist, + filesystem::ErrorCode::FileTooLarge => types::Errno::Fbig, + filesystem::ErrorCode::IllegalByteSequence => types::Errno::Ilseq, + filesystem::ErrorCode::InProgress => types::Errno::Inprogress, + filesystem::ErrorCode::Interrupted => types::Errno::Intr, + filesystem::ErrorCode::Invalid => types::Errno::Inval, + filesystem::ErrorCode::Io => types::Errno::Io, + filesystem::ErrorCode::IsDirectory => types::Errno::Isdir, + filesystem::ErrorCode::Loop => types::Errno::Loop, + filesystem::ErrorCode::TooManyLinks => types::Errno::Mlink, + filesystem::ErrorCode::MessageSize => types::Errno::Msgsize, + filesystem::ErrorCode::NameTooLong => types::Errno::Nametoolong, + filesystem::ErrorCode::NoDevice => types::Errno::Nodev, + filesystem::ErrorCode::NoEntry => types::Errno::Noent, + filesystem::ErrorCode::NoLock => types::Errno::Nolck, + filesystem::ErrorCode::InsufficientMemory => types::Errno::Nomem, + filesystem::ErrorCode::InsufficientSpace => types::Errno::Nospc, + filesystem::ErrorCode::Unsupported => types::Errno::Notsup, + filesystem::ErrorCode::NotDirectory => types::Errno::Notdir, + filesystem::ErrorCode::NotEmpty => types::Errno::Notempty, + filesystem::ErrorCode::NotRecoverable => types::Errno::Notrecoverable, + filesystem::ErrorCode::NoTty => types::Errno::Notty, + filesystem::ErrorCode::NoSuchDevice => types::Errno::Nxio, + filesystem::ErrorCode::Overflow => types::Errno::Overflow, + filesystem::ErrorCode::NotPermitted => types::Errno::Perm, + filesystem::ErrorCode::Pipe => types::Errno::Pipe, + filesystem::ErrorCode::ReadOnly => types::Errno::Rofs, + filesystem::ErrorCode::InvalidSeek => types::Errno::Spipe, + filesystem::ErrorCode::TextFileBusy => types::Errno::Txtbsy, + filesystem::ErrorCode::CrossDevice => types::Errno::Xdev, + } + } +} + +impl From for types::Error { + fn from(_: std::num::TryFromIntError) -> Self { + types::Errno::Overflow.into() + } +} + +impl From for types::Error { + fn from(err: GuestError) -> Self { + use wiggle::GuestError::*; + match err { + InvalidFlagValue { .. } => types::Errno::Inval.into(), + InvalidEnumValue { .. } => types::Errno::Inval.into(), + // As per + // https://github.com/WebAssembly/wasi/blob/main/legacy/tools/witx-docs.md#pointers + // + // > If a misaligned pointer is passed to a function, the function + // > shall trap. + // > + // > If an out-of-bounds pointer is passed to a function and the + // > function needs to dereference it, the function shall trap. + // + // so this turns OOB and misalignment errors into traps. + PtrOverflow { .. } | PtrOutOfBounds { .. } | PtrNotAligned { .. } => { + types::Error::trap(err.into()) + } + InvalidUtf8 { .. } => types::Errno::Ilseq.into(), + TryFromIntError { .. } => types::Errno::Overflow.into(), + SliceLengthsDiffer { .. } => types::Errno::Fault.into(), + InFunc { err, .. } => types::Error::from(*err), + } + } +} + +impl From for types::Error { + fn from(code: filesystem::ErrorCode) -> Self { + types::Errno::from(code).into() + } +} + +impl From for types::Error { + fn from(code: crate::filesystem::ErrorCode) -> Self { + types::Errno::from(code).into() + } +} + +impl From for types::Error { + fn from(err: wasmtime::component::ResourceTableError) -> Self { + types::Error::trap(err.into()) + } +} + +type Result = std::result::Result; + +fn write_bytes( + memory: &mut GuestMemory<'_>, + ptr: GuestPtr, + buf: &[u8], +) -> Result, types::Error> { + // NOTE: legacy implementation always returns Inval errno + + let len = u32::try_from(buf.len())?; + + memory.copy_from_slice(buf, ptr.as_array(len))?; + let next = ptr.add(len)?; + Ok(next) +} + +fn write_byte(memory: &mut GuestMemory<'_>, ptr: GuestPtr, byte: u8) -> Result> { + memory.write(ptr, byte)?; + let next = ptr.add(1)?; + Ok(next) +} + +fn read_string<'a>(memory: &'a GuestMemory<'_>, ptr: GuestPtr) -> Result { + Ok(memory.as_cow_str(ptr)?.into_owned()) +} + +// Returns the first non-empty buffer in `ciovs` or a single empty buffer if +// they're all empty. +fn first_non_empty_ciovec( + memory: &GuestMemory<'_>, + ciovs: types::CiovecArray, +) -> Result> { + for iov in ciovs.iter() { + let iov = memory.read(iov?)?; + if iov.buf_len == 0 { + continue; + } + return Ok(iov.buf.as_array(iov.buf_len)); + } + Ok(GuestPtr::new((0, 0))) +} + +// Returns the first non-empty buffer in `iovs` or a single empty buffer if +// they're all empty. +fn first_non_empty_iovec( + memory: &GuestMemory<'_>, + iovs: types::IovecArray, +) -> Result> { + for iov in iovs.iter() { + let iov = memory.read(iov?)?; + if iov.buf_len == 0 { + continue; + } + return Ok(iov.buf.as_array(iov.buf_len)); + } + Ok(GuestPtr::new((0, 0))) +} + +#[async_trait::async_trait] +// Implement the WasiSnapshotPreview1 trait using only the traits that are +// required for T, i.e., in terms of the preview 2 wit interface, and state +// stored in the WasiP1Adapter struct. +impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiP1Ctx { + #[instrument(skip(self, memory))] + fn args_get( + &mut self, + memory: &mut GuestMemory<'_>, + argv: GuestPtr>, + argv_buf: GuestPtr, + ) -> Result<(), types::Error> { + self.cli() + .get_arguments() + .context("failed to call `get-arguments`") + .map_err(types::Error::trap)? + .into_iter() + .try_fold((argv, argv_buf), |(argv, argv_buf), arg| -> Result<_> { + memory.write(argv, argv_buf)?; + let argv = argv.add(1)?; + + let argv_buf = write_bytes(memory, argv_buf, arg.as_bytes())?; + let argv_buf = write_byte(memory, argv_buf, 0)?; + + Ok((argv, argv_buf)) + })?; + Ok(()) + } + + #[instrument(skip(self, _memory))] + fn args_sizes_get( + &mut self, + _memory: &mut GuestMemory<'_>, + ) -> Result<(types::Size, types::Size), types::Error> { + let args = self + .cli() + .get_arguments() + .context("failed to call `get-arguments`") + .map_err(types::Error::trap)?; + let num = args.len().try_into().map_err(|_| types::Errno::Overflow)?; + let len = args + .iter() + .map(|buf| buf.len() + 1) // Each argument is expected to be `\0` terminated. + .sum::() + .try_into() + .map_err(|_| types::Errno::Overflow)?; + Ok((num, len)) + } + + #[instrument(skip(self, memory))] + fn environ_get( + &mut self, + memory: &mut GuestMemory<'_>, + environ: GuestPtr>, + environ_buf: GuestPtr, + ) -> Result<(), types::Error> { + self.cli() + .get_environment() + .context("failed to call `get-environment`") + .map_err(types::Error::trap)? + .into_iter() + .try_fold( + (environ, environ_buf), + |(environ, environ_buf), (k, v)| -> Result<_, types::Error> { + memory.write(environ, environ_buf)?; + let environ = environ.add(1)?; + + let environ_buf = write_bytes(memory, environ_buf, k.as_bytes())?; + let environ_buf = write_byte(memory, environ_buf, b'=')?; + let environ_buf = write_bytes(memory, environ_buf, v.as_bytes())?; + let environ_buf = write_byte(memory, environ_buf, 0)?; + + Ok((environ, environ_buf)) + }, + )?; + Ok(()) + } + + #[instrument(skip(self, _memory))] + fn environ_sizes_get( + &mut self, + _memory: &mut GuestMemory<'_>, + ) -> Result<(types::Size, types::Size), types::Error> { + let environ = self + .cli() + .get_environment() + .context("failed to call `get-environment`") + .map_err(types::Error::trap)?; + let num = environ.len().try_into()?; + let len = environ + .iter() + .map(|(k, v)| k.len() + 1 + v.len() + 1) // Key/value pairs are expected to be joined with `=`s, and terminated with `\0`s. + .sum::() + .try_into()?; + Ok((num, len)) + } + + #[instrument(skip(self, _memory))] + fn clock_res_get( + &mut self, + _memory: &mut GuestMemory<'_>, + id: types::Clockid, + ) -> Result { + let res = match id { + types::Clockid::Realtime => wall_clock::Host::resolution(&mut self.clocks()) + .context("failed to call `wall_clock::resolution`") + .map_err(types::Error::trap)? + .try_into()?, + types::Clockid::Monotonic => monotonic_clock::Host::resolution(&mut self.clocks()) + .context("failed to call `monotonic_clock::resolution`") + .map_err(types::Error::trap)?, + types::Clockid::ProcessCputimeId | types::Clockid::ThreadCputimeId => { + return Err(types::Errno::Badf.into()); + } + }; + Ok(res) + } + + #[instrument(skip(self, _memory))] + fn clock_time_get( + &mut self, + _memory: &mut GuestMemory<'_>, + id: types::Clockid, + _precision: types::Timestamp, + ) -> Result { + let now = match id { + types::Clockid::Realtime => wall_clock::Host::now(&mut self.clocks()) + .context("failed to call `wall_clock::now`") + .map_err(types::Error::trap)? + .try_into()?, + types::Clockid::Monotonic => monotonic_clock::Host::now(&mut self.clocks()) + .context("failed to call `monotonic_clock::now`") + .map_err(types::Error::trap)?, + types::Clockid::ProcessCputimeId | types::Clockid::ThreadCputimeId => { + return Err(types::Errno::Badf.into()); + } + }; + Ok(now) + } + + #[instrument(skip(self, _memory))] + async fn fd_advise( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + offset: types::Filesize, + len: types::Filesize, + advice: types::Advice, + ) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.filesystem() + .advise(fd, offset, len, advice.into()) + .await?; + Ok(()) + } + + /// Force the allocation of space in a file. + /// NOTE: This is similar to `posix_fallocate` in POSIX. + #[instrument(skip(self, _memory))] + fn fd_allocate( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + _offset: types::Filesize, + _len: types::Filesize, + ) -> Result<(), types::Error> { + self.get_file_fd(fd)?; + Err(types::Errno::Notsup.into()) + } + + /// Close a file descriptor. + /// NOTE: This is similar to `close` in POSIX. + #[instrument(skip(self, _memory))] + async fn fd_close( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result<(), types::Error> { + let desc = { + let fd = fd.into(); + let mut st = self.transact()?; + let desc = st.descriptors.used.remove(&fd).ok_or(types::Errno::Badf)?; + st.descriptors.free.insert(fd); + desc + }; + match desc { + Descriptor::Stdin { stream, .. } => { + streams::HostInputStream::drop(&mut self.table, stream) + .await + .context("failed to call `drop` on `input-stream`") + } + Descriptor::Stdout { stream, .. } | Descriptor::Stderr { stream, .. } => { + streams::HostOutputStream::drop(&mut self.table, stream) + .await + .context("failed to call `drop` on `output-stream`") + } + Descriptor::File(File { fd, .. }) | Descriptor::Directory { fd, .. } => { + filesystem::HostDescriptor::drop(&mut self.filesystem(), fd) + .context("failed to call `drop`") + } + } + .map_err(types::Error::trap) + } + + /// Synchronize the data of a file to disk. + /// NOTE: This is similar to `fdatasync` in POSIX. + #[instrument(skip(self, _memory))] + async fn fd_datasync( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.filesystem().sync_data(fd).await?; + 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. + #[instrument(skip(self, _memory))] + async fn fd_fdstat_get( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + let (fd, blocking, append) = match self.transact()?.get_descriptor(fd)? { + Descriptor::Stdin { isatty, .. } => { + let fs_rights_base = types::Rights::FD_READ; + return Ok(types::Fdstat { + fs_filetype: (*isatty).into(), + fs_flags: types::Fdflags::empty(), + fs_rights_base, + fs_rights_inheriting: fs_rights_base, + }); + } + Descriptor::Stdout { isatty, .. } | Descriptor::Stderr { isatty, .. } => { + let fs_rights_base = types::Rights::FD_WRITE; + return Ok(types::Fdstat { + fs_filetype: (*isatty).into(), + fs_flags: types::Fdflags::empty(), + fs_rights_base, + fs_rights_inheriting: fs_rights_base, + }); + } + Descriptor::Directory { + preopen_path: Some(_), + .. + } => { + // Hard-coded set or rights expected by many userlands: + let fs_rights_base = types::Rights::PATH_CREATE_DIRECTORY + | types::Rights::PATH_CREATE_FILE + | types::Rights::PATH_LINK_SOURCE + | types::Rights::PATH_LINK_TARGET + | types::Rights::PATH_OPEN + | types::Rights::FD_READDIR + | types::Rights::PATH_READLINK + | types::Rights::PATH_RENAME_SOURCE + | types::Rights::PATH_RENAME_TARGET + | types::Rights::PATH_SYMLINK + | types::Rights::PATH_REMOVE_DIRECTORY + | types::Rights::PATH_UNLINK_FILE + | types::Rights::PATH_FILESTAT_GET + | types::Rights::PATH_FILESTAT_SET_TIMES + | types::Rights::FD_FILESTAT_GET + | types::Rights::FD_FILESTAT_SET_TIMES; + + let fs_rights_inheriting = fs_rights_base + | types::Rights::FD_DATASYNC + | types::Rights::FD_READ + | types::Rights::FD_SEEK + | types::Rights::FD_FDSTAT_SET_FLAGS + | types::Rights::FD_SYNC + | types::Rights::FD_TELL + | types::Rights::FD_WRITE + | types::Rights::FD_ADVISE + | types::Rights::FD_ALLOCATE + | types::Rights::FD_FILESTAT_GET + | types::Rights::FD_FILESTAT_SET_SIZE + | types::Rights::FD_FILESTAT_SET_TIMES + | types::Rights::POLL_FD_READWRITE; + + return Ok(types::Fdstat { + fs_filetype: types::Filetype::Directory, + fs_flags: types::Fdflags::empty(), + fs_rights_base, + fs_rights_inheriting, + }); + } + Descriptor::Directory { fd, .. } => (fd.borrowed(), BlockingMode::Blocking, false), + Descriptor::File(File { + fd, + blocking_mode, + append, + .. + }) => (fd.borrowed(), *blocking_mode, *append), + }; + let flags = self.filesystem().get_flags(fd.borrowed()).await?; + let fs_filetype = self + .filesystem() + .get_type(fd.borrowed()) + .await? + .try_into() + .map_err(types::Error::trap)?; + let mut fs_flags = types::Fdflags::empty(); + let mut fs_rights_base = types::Rights::all(); + if let types::Filetype::Directory = fs_filetype { + fs_rights_base &= !types::Rights::FD_SEEK; + fs_rights_base &= !types::Rights::FD_FILESTAT_SET_SIZE; + fs_rights_base &= !types::Rights::PATH_FILESTAT_SET_SIZE; + } + if !flags.contains(filesystem::DescriptorFlags::READ) { + fs_rights_base &= !types::Rights::FD_READ; + fs_rights_base &= !types::Rights::FD_READDIR; + } + if !flags.contains(filesystem::DescriptorFlags::WRITE) { + fs_rights_base &= !types::Rights::FD_WRITE; + } + if flags.contains(filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + fs_flags |= types::Fdflags::DSYNC; + } + if flags.contains(filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + fs_flags |= types::Fdflags::RSYNC; + } + if flags.contains(filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + fs_flags |= types::Fdflags::SYNC; + } + if append { + fs_flags |= types::Fdflags::APPEND; + } + if matches!(blocking, BlockingMode::NonBlocking) { + fs_flags |= types::Fdflags::NONBLOCK; + } + Ok(types::Fdstat { + fs_filetype, + fs_flags, + fs_rights_base, + fs_rights_inheriting: fs_rights_base, + }) + } + + /// Adjust the flags associated with a file descriptor. + /// NOTE: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + #[instrument(skip(self, _memory))] + fn fd_fdstat_set_flags( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + flags: types::Fdflags, + ) -> Result<(), types::Error> { + let mut st = self.transact()?; + let File { + append, + blocking_mode, + .. + } = st.get_file_mut(fd)?; + + // Only support changing the NONBLOCK or APPEND flags. + if flags.contains(types::Fdflags::DSYNC) + || flags.contains(types::Fdflags::SYNC) + || flags.contains(types::Fdflags::RSYNC) + { + return Err(types::Errno::Inval.into()); + } + *append = flags.contains(types::Fdflags::APPEND); + *blocking_mode = BlockingMode::from_fdflags(&flags); + Ok(()) + } + + /// Does not do anything if `fd` corresponds to a valid descriptor and returns `[types::Errno::Badf]` error otherwise. + #[instrument(skip(self, _memory))] + fn fd_fdstat_set_rights( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + _fs_rights_base: types::Rights, + _fs_rights_inheriting: types::Rights, + ) -> Result<(), types::Error> { + self.get_fd(fd)?; + Err(types::Errno::Notsup.into()) + } + + /// Return the attributes of an open file. + #[instrument(skip(self, _memory))] + async fn fd_filestat_get( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + let t = self.transact()?; + let desc = t.get_descriptor(fd)?; + match desc { + Descriptor::Stdin { isatty, .. } + | Descriptor::Stdout { isatty, .. } + | Descriptor::Stderr { isatty, .. } => Ok(types::Filestat { + dev: 0, + ino: 0, + filetype: (*isatty).into(), + nlink: 0, + size: 0, + atim: 0, + mtim: 0, + ctim: 0, + }), + Descriptor::Directory { fd, .. } | Descriptor::File(File { fd, .. }) => { + let fd = fd.borrowed(); + drop(t); + let filesystem::DescriptorStat { + type_, + link_count: nlink, + size, + data_access_timestamp, + data_modification_timestamp, + status_change_timestamp, + } = self.filesystem().stat(fd.borrowed()).await?; + let metadata_hash = self.filesystem().metadata_hash(fd).await?; + let filetype = type_.try_into().map_err(types::Error::trap)?; + let zero = wall_clock::Datetime { + seconds: 0, + nanoseconds: 0, + }; + let atim = data_access_timestamp.unwrap_or(zero).try_into()?; + let mtim = data_modification_timestamp.unwrap_or(zero).try_into()?; + let ctim = status_change_timestamp.unwrap_or(zero).try_into()?; + Ok(types::Filestat { + dev: 1, + ino: metadata_hash.lower, + filetype, + nlink, + size, + atim, + mtim, + ctim, + }) + } + } + } + + /// 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. + #[instrument(skip(self, _memory))] + async fn fd_filestat_set_size( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + size: types::Filesize, + ) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.filesystem().set_size(fd, size).await?; + Ok(()) + } + + /// Adjust the timestamps of an open file or directory. + /// NOTE: This is similar to `futimens` in POSIX. + #[instrument(skip(self, _memory))] + async fn fd_filestat_set_times( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + atim: types::Timestamp, + mtim: types::Timestamp, + fst_flags: types::Fstflags, + ) -> Result<(), types::Error> { + let atim = systimespec( + fst_flags.contains(types::Fstflags::ATIM), + atim, + fst_flags.contains(types::Fstflags::ATIM_NOW), + )?; + let mtim = systimespec( + fst_flags.contains(types::Fstflags::MTIM), + mtim, + fst_flags.contains(types::Fstflags::MTIM_NOW), + )?; + + let fd = self.get_fd(fd)?; + self.filesystem().set_times(fd, atim, mtim).await?; + Ok(()) + } + + /// Read from a file descriptor. + /// NOTE: This is similar to `readv` in POSIX. + #[instrument(skip(self, memory))] + async fn fd_read( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + iovs: types::IovecArray, + ) -> Result { + let t = self.transact()?; + let desc = t.get_descriptor(fd)?; + match desc { + Descriptor::File(File { + fd, + position, + // NB: the nonblocking flag is intentionally ignored here and + // blocking reads/writes are always performed. + blocking_mode: _, + .. + }) => { + let fd = fd.borrowed(); + let position = position.clone(); + drop(t); + let pos = position.load(Ordering::Relaxed); + let file = self.table.get(&fd)?.file()?; + let iov = first_non_empty_iovec(memory, iovs)?; + let bytes_read = match (file.as_blocking_file(), memory.as_slice_mut(iov)?) { + // Try to read directly into wasm memory where possible + // when the current thread can block and additionally wasm + // memory isn't shared. + (Some(file), Some(mut buf)) => file + .read_at(&mut buf, pos) + .map_err(|e| StreamError::LastOperationFailed(e.into()))?, + // ... otherwise fall back to performing the read on a + // blocking thread and which copies the data back into wasm + // memory. + (_, buf) => { + drop(buf); + let mut buf = vec![0; iov.len() as usize]; + let buf = file + .run_blocking(move |file| -> Result<_, types::Error> { + let bytes_read = file + .read_at(&mut buf, pos) + .map_err(|e| StreamError::LastOperationFailed(e.into()))?; + buf.truncate(bytes_read); + Ok(buf) + }) + .await?; + let iov = iov.get_range(0..u32::try_from(buf.len())?).unwrap(); + memory.copy_from_slice(&buf, iov)?; + buf.len() + } + }; + + let pos = pos + .checked_add(bytes_read.try_into()?) + .ok_or(types::Errno::Overflow)?; + position.store(pos, Ordering::Relaxed); + + Ok(bytes_read.try_into()?) + } + Descriptor::Stdin { stream, .. } => { + let stream = stream.borrowed(); + drop(t); + let buf = first_non_empty_iovec(memory, iovs)?; + let read = BlockingMode::Blocking + .read(&mut self.table, stream, buf.len().try_into()?) + .await?; + if read.len() > buf.len().try_into()? { + return Err(types::Errno::Range.into()); + } + let buf = buf.get_range(0..u32::try_from(read.len())?).unwrap(); + memory.copy_from_slice(&read, buf)?; + let n = read.len().try_into()?; + Ok(n) + } + _ => return Err(types::Errno::Badf.into()), + } + } + + /// Read from a file descriptor, without using and updating the file descriptor's offset. + /// NOTE: This is similar to `preadv` in POSIX. + #[instrument(skip(self, memory))] + async fn fd_pread( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + iovs: types::IovecArray, + offset: types::Filesize, + ) -> Result { + let t = self.transact()?; + let desc = t.get_descriptor(fd)?; + let (buf, read) = match desc { + Descriptor::File(File { + fd, blocking_mode, .. + }) => { + let fd = fd.borrowed(); + let blocking_mode = *blocking_mode; + drop(t); + let buf = first_non_empty_iovec(memory, iovs)?; + + let stream = self.filesystem().read_via_stream(fd, offset)?; + let read = blocking_mode + .read(&mut self.table, stream.borrowed(), buf.len().try_into()?) + .await; + streams::HostInputStream::drop(&mut self.table, stream) + .await + .map_err(|e| types::Error::trap(e))?; + (buf, read?) + } + Descriptor::Stdin { .. } => { + // NOTE: legacy implementation returns SPIPE here + return Err(types::Errno::Spipe.into()); + } + _ => return Err(types::Errno::Badf.into()), + }; + if read.len() > buf.len().try_into()? { + return Err(types::Errno::Range.into()); + } + let buf = buf.get_range(0..u32::try_from(read.len())?).unwrap(); + memory.copy_from_slice(&read, buf)?; + let n = read.len().try_into()?; + Ok(n) + } + + /// Write to a file descriptor. + /// NOTE: This is similar to `writev` in POSIX. + #[instrument(skip(self, memory))] + async fn fd_write( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ciovs: types::CiovecArray, + ) -> Result { + self.fd_write_impl(memory, fd, ciovs, FdWrite::AtCur).await + } + + /// Write to a file descriptor, without using and updating the file descriptor's offset. + /// NOTE: This is similar to `pwritev` in POSIX. + #[instrument(skip(self, memory))] + async fn fd_pwrite( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + ciovs: types::CiovecArray, + offset: types::Filesize, + ) -> Result { + self.fd_write_impl(memory, fd, ciovs, FdWrite::At(offset)) + .await + } + + /// Return a description of the given preopened file descriptor. + #[instrument(skip(self, _memory))] + fn fd_prestat_get( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + if let Descriptor::Directory { + preopen_path: Some(p), + .. + } = self.transact()?.get_descriptor(fd)? + { + let pr_name_len = p.len().try_into()?; + return Ok(types::Prestat::Dir(types::PrestatDir { pr_name_len })); + } + Err(types::Errno::Badf.into()) // NOTE: legacy implementation returns BADF here + } + + /// Return a description of the given preopened file descriptor. + #[instrument(skip(self, memory))] + fn fd_prestat_dir_name( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + path: GuestPtr, + path_max_len: types::Size, + ) -> Result<(), types::Error> { + let path_max_len = path_max_len.try_into()?; + if let Descriptor::Directory { + preopen_path: Some(p), + .. + } = self.transact()?.get_descriptor(fd)? + { + if p.len() > path_max_len { + return Err(types::Errno::Nametoolong.into()); + } + write_bytes(memory, path, p.as_bytes())?; + return Ok(()); + } + Err(types::Errno::Notdir.into()) // NOTE: legacy implementation returns NOTDIR here + } + + /// Atomically replace a file descriptor by renumbering another file descriptor. + #[instrument(skip(self, _memory))] + fn fd_renumber( + &mut self, + _memory: &mut GuestMemory<'_>, + from: types::Fd, + to: types::Fd, + ) -> Result<(), types::Error> { + let mut st = self.transact()?; + let from = from.into(); + let to = to.into(); + if !st.descriptors.used.contains_key(&to) { + return Err(types::Errno::Badf.into()); + } + let btree_map::Entry::Occupied(desc) = st.descriptors.used.entry(from) else { + return Err(types::Errno::Badf.into()); + }; + if from != to { + let desc = desc.remove(); + st.descriptors.free.insert(from); + st.descriptors.free.remove(&to); + st.descriptors.used.insert(to, desc); + } + Ok(()) + } + + /// Move the offset of a file descriptor. + /// NOTE: This is similar to `lseek` in POSIX. + #[instrument(skip(self, _memory))] + async fn fd_seek( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + offset: types::Filedelta, + whence: types::Whence, + ) -> Result { + let t = self.transact()?; + let File { fd, position, .. } = t.get_seekable(fd)?; + let fd = fd.borrowed(); + let position = position.clone(); + drop(t); + let pos = match whence { + types::Whence::Set if offset >= 0 => { + offset.try_into().map_err(|_| types::Errno::Inval)? + } + types::Whence::Cur => position + .load(Ordering::Relaxed) + .checked_add_signed(offset) + .ok_or(types::Errno::Inval)?, + types::Whence::End => { + let filesystem::DescriptorStat { size, .. } = self.filesystem().stat(fd).await?; + size.checked_add_signed(offset).ok_or(types::Errno::Inval)? + } + _ => return Err(types::Errno::Inval.into()), + }; + position.store(pos, Ordering::Relaxed); + Ok(pos) + } + + /// Synchronize the data and metadata of a file to disk. + /// NOTE: This is similar to `fsync` in POSIX. + #[instrument(skip(self, _memory))] + async fn fd_sync( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.filesystem().sync(fd).await?; + Ok(()) + } + + /// Return the current offset of a file descriptor. + /// NOTE: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + #[instrument(skip(self, _memory))] + fn fd_tell( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ) -> Result { + let pos = self + .transact()? + .get_seekable(fd) + .map(|File { position, .. }| position.load(Ordering::Relaxed))?; + Ok(pos) + } + + #[instrument(skip(self, memory))] + async fn fd_readdir( + &mut self, + memory: &mut GuestMemory<'_>, + fd: types::Fd, + buf: GuestPtr, + buf_len: types::Size, + cookie: types::Dircookie, + ) -> Result { + let fd = self.get_dir_fd(fd)?; + let stream = self.filesystem().read_directory(fd.borrowed()).await?; + let dir_metadata_hash = self.filesystem().metadata_hash(fd.borrowed()).await?; + let cookie = cookie.try_into().map_err(|_| types::Errno::Overflow)?; + + let head = [ + ( + types::Dirent { + d_next: 1u64.to_le(), + d_ino: dir_metadata_hash.lower.to_le(), + d_type: types::Filetype::Directory, + d_namlen: 1u32.to_le(), + }, + ".".into(), + ), + ( + types::Dirent { + d_next: 2u64.to_le(), + d_ino: dir_metadata_hash.lower.to_le(), // NOTE: incorrect, but legacy implementation returns `fd` inode here + d_type: types::Filetype::Directory, + d_namlen: 2u32.to_le(), + }, + "..".into(), + ), + ]; + + let mut dir = Vec::new(); + for (entry, d_next) in self + .table + // remove iterator from table and use it directly: + .delete(stream)? + .into_iter() + .zip(3u64..) + { + let filesystem::DirectoryEntry { type_, name } = entry?; + let metadata_hash = self + .filesystem() + .metadata_hash_at(fd.borrowed(), filesystem::PathFlags::empty(), name.clone()) + .await?; + let d_type = type_.try_into().map_err(types::Error::trap)?; + let d_namlen: u32 = name.len().try_into().map_err(|_| types::Errno::Overflow)?; + dir.push(( + types::Dirent { + d_next: d_next.to_le(), + d_ino: metadata_hash.lower.to_le(), + d_type, // endian-invariant + d_namlen: d_namlen.to_le(), + }, + name, + )) + } + + // assume that `types::Dirent` size always fits in `u32` + const DIRENT_SIZE: u32 = size_of::() as _; + assert_eq!( + types::Dirent::guest_size(), + DIRENT_SIZE, + "Dirent guest repr and host repr should match" + ); + let mut buf = buf; + let mut cap = buf_len; + for (ref entry, path) in head.into_iter().chain(dir.into_iter()).skip(cookie) { + let mut path = path.into_bytes(); + assert_eq!( + 1, + size_of_val(&entry.d_type), + "Dirent member d_type should be endian-invariant" + ); + let entry_len = cap.min(DIRENT_SIZE); + let entry = entry as *const _ as _; + let entry = unsafe { slice::from_raw_parts(entry, entry_len as _) }; + cap = cap.checked_sub(entry_len).unwrap(); + buf = write_bytes(memory, buf, entry)?; + if cap == 0 { + return Ok(buf_len); + } + + if let Ok(cap) = cap.try_into() { + // `path` cannot be longer than `usize`, only truncate if `cap` fits in `usize` + path.truncate(cap); + } + cap = cap.checked_sub(path.len() as _).unwrap(); + buf = write_bytes(memory, buf, &path)?; + if cap == 0 { + return Ok(buf_len); + } + } + Ok(buf_len.checked_sub(cap).unwrap()) + } + + #[instrument(skip(self, memory))] + async fn path_create_directory( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), types::Error> { + let dirfd = self.get_dir_fd(dirfd)?; + let path = read_string(memory, path)?; + self.filesystem() + .create_directory_at(dirfd.borrowed(), path) + .await?; + Ok(()) + } + + /// Return the attributes of a file or directory. + /// NOTE: This is similar to `stat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_filestat_get( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + flags: types::Lookupflags, + path: GuestPtr, + ) -> Result { + let dirfd = self.get_dir_fd(dirfd)?; + let path = read_string(memory, path)?; + let filesystem::DescriptorStat { + type_, + link_count: nlink, + size, + data_access_timestamp, + data_modification_timestamp, + status_change_timestamp, + } = self + .filesystem() + .stat_at(dirfd.borrowed(), flags.into(), path.clone()) + .await?; + let metadata_hash = self + .filesystem() + .metadata_hash_at(dirfd, flags.into(), path) + .await?; + let filetype = type_.try_into().map_err(types::Error::trap)?; + let zero = wall_clock::Datetime { + seconds: 0, + nanoseconds: 0, + }; + let atim = data_access_timestamp.unwrap_or(zero).try_into()?; + let mtim = data_modification_timestamp.unwrap_or(zero).try_into()?; + let ctim = status_change_timestamp.unwrap_or(zero).try_into()?; + Ok(types::Filestat { + dev: 1, + ino: metadata_hash.lower, + filetype, + nlink, + size, + atim, + mtim, + ctim, + }) + } + + /// Adjust the timestamps of a file or directory. + /// NOTE: This is similar to `utimensat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_filestat_set_times( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + flags: types::Lookupflags, + path: GuestPtr, + atim: types::Timestamp, + mtim: types::Timestamp, + fst_flags: types::Fstflags, + ) -> Result<(), types::Error> { + let atim = systimespec( + fst_flags.contains(types::Fstflags::ATIM), + atim, + fst_flags.contains(types::Fstflags::ATIM_NOW), + )?; + let mtim = systimespec( + fst_flags.contains(types::Fstflags::MTIM), + mtim, + fst_flags.contains(types::Fstflags::MTIM_NOW), + )?; + + let dirfd = self.get_dir_fd(dirfd)?; + let path = read_string(memory, path)?; + self.filesystem() + .set_times_at(dirfd, flags.into(), path, atim, mtim) + .await?; + Ok(()) + } + + /// Create a hard link. + /// NOTE: This is similar to `linkat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_link( + &mut self, + memory: &mut GuestMemory<'_>, + src_fd: types::Fd, + src_flags: types::Lookupflags, + src_path: GuestPtr, + target_fd: types::Fd, + target_path: GuestPtr, + ) -> Result<(), types::Error> { + let src_fd = self.get_dir_fd(src_fd)?; + let target_fd = self.get_dir_fd(target_fd)?; + let src_path = read_string(memory, src_path)?; + let target_path = read_string(memory, target_path)?; + self.filesystem() + .link_at(src_fd, src_flags.into(), src_path, target_fd, target_path) + .await?; + Ok(()) + } + + /// Open a file or directory. + /// NOTE: This is similar to `openat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_open( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + dirflags: types::Lookupflags, + path: GuestPtr, + oflags: types::Oflags, + fs_rights_base: types::Rights, + _fs_rights_inheriting: types::Rights, + fdflags: types::Fdflags, + ) -> Result { + let path = read_string(memory, path)?; + + let mut flags = filesystem::DescriptorFlags::empty(); + if fs_rights_base.contains(types::Rights::FD_READ) { + flags |= filesystem::DescriptorFlags::READ; + } + if fs_rights_base.contains(types::Rights::FD_WRITE) { + flags |= filesystem::DescriptorFlags::WRITE; + } + if fdflags.contains(types::Fdflags::SYNC) { + flags |= filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC; + } + if fdflags.contains(types::Fdflags::DSYNC) { + flags |= filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC; + } + if fdflags.contains(types::Fdflags::RSYNC) { + flags |= filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC; + } + + let t = self.transact()?; + let dirfd = match t.get_descriptor(dirfd)? { + Descriptor::Directory { fd, .. } => fd.borrowed(), + Descriptor::File(_) => return Err(types::Errno::Notdir.into()), + _ => return Err(types::Errno::Badf.into()), + }; + drop(t); + let fd = self + .filesystem() + .open_at(dirfd, dirflags.into(), path, oflags.into(), flags) + .await?; + let mut t = self.transact()?; + let desc = match t.view.table.get(&fd)? { + crate::filesystem::Descriptor::Dir(_) => Descriptor::Directory { + fd, + preopen_path: None, + }, + crate::filesystem::Descriptor::File(_) => Descriptor::File(File { + fd, + position: Default::default(), + append: fdflags.contains(types::Fdflags::APPEND), + blocking_mode: BlockingMode::from_fdflags(&fdflags), + }), + }; + let fd = t.descriptors.push(desc)?; + Ok(fd.into()) + } + + /// Read the contents of a symbolic link. + /// NOTE: This is similar to `readlinkat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_readlink( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + buf: GuestPtr, + buf_len: types::Size, + ) -> Result { + let dirfd = self.get_dir_fd(dirfd)?; + let path = read_string(memory, path)?; + let mut path = self + .filesystem() + .readlink_at(dirfd, path) + .await? + .into_bytes(); + if let Ok(buf_len) = buf_len.try_into() { + // `path` cannot be longer than `usize`, only truncate if `buf_len` fits in `usize` + path.truncate(buf_len); + } + let n = path.len().try_into().map_err(|_| types::Errno::Overflow)?; + write_bytes(memory, buf, &path)?; + Ok(n) + } + + #[instrument(skip(self, memory))] + async fn path_remove_directory( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), types::Error> { + let dirfd = self.get_dir_fd(dirfd)?; + let path = read_string(memory, path)?; + self.filesystem().remove_directory_at(dirfd, path).await?; + Ok(()) + } + + /// Rename a file or directory. + /// NOTE: This is similar to `renameat` in POSIX. + #[instrument(skip(self, memory))] + async fn path_rename( + &mut self, + memory: &mut GuestMemory<'_>, + src_fd: types::Fd, + src_path: GuestPtr, + dest_fd: types::Fd, + dest_path: GuestPtr, + ) -> Result<(), types::Error> { + let src_fd = self.get_dir_fd(src_fd)?; + let dest_fd = self.get_dir_fd(dest_fd)?; + let src_path = read_string(memory, src_path)?; + let dest_path = read_string(memory, dest_path)?; + self.filesystem() + .rename_at(src_fd, src_path, dest_fd, dest_path) + .await?; + Ok(()) + } + + #[instrument(skip(self, memory))] + async fn path_symlink( + &mut self, + memory: &mut GuestMemory<'_>, + src_path: GuestPtr, + dirfd: types::Fd, + dest_path: GuestPtr, + ) -> Result<(), types::Error> { + let dirfd = self.get_dir_fd(dirfd)?; + let src_path = read_string(memory, src_path)?; + let dest_path = read_string(memory, dest_path)?; + self.filesystem() + .symlink_at(dirfd.borrowed(), src_path, dest_path) + .await?; + Ok(()) + } + + #[instrument(skip(self, memory))] + async fn path_unlink_file( + &mut self, + memory: &mut GuestMemory<'_>, + dirfd: types::Fd, + path: GuestPtr, + ) -> Result<(), types::Error> { + let dirfd = self.get_dir_fd(dirfd)?; + let path = memory.as_cow_str(path)?.into_owned(); + self.filesystem() + .unlink_file_at(dirfd.borrowed(), path) + .await?; + Ok(()) + } + + #[instrument(skip(self, memory))] + async fn poll_oneoff( + &mut self, + memory: &mut GuestMemory<'_>, + subs: GuestPtr, + events: GuestPtr, + nsubscriptions: types::Size, + ) -> Result { + if nsubscriptions == 0 { + // Indefinite sleeping is not supported in p1. + return Err(types::Errno::Inval.into()); + } + + // This is a special case where `poll_oneoff` is just sleeping + // on a single relative timer event. This special case was added + // after experimental observations showed that std::thread::sleep + // results in more consistent sleep times. This design ensures that + // wasmtime can handle real-time requirements more accurately. + if nsubscriptions == 1 { + let sub = memory.read(subs)?; + if let types::SubscriptionU::Clock(clocksub) = sub.u { + if !clocksub + .flags + .contains(types::Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME) + && self.wasi.filesystem.allow_blocking_current_thread + { + std::thread::sleep(std::time::Duration::from_nanos(clocksub.timeout)); + memory.write( + events, + types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::Clock, + fd_readwrite: types::EventFdReadwrite { + flags: types::Eventrwflags::empty(), + nbytes: 1, + }, + }, + )?; + return Ok(1); + } + } + } + + let subs = subs.as_array(nsubscriptions); + let events = events.as_array(nsubscriptions); + + let n = usize::try_from(nsubscriptions).unwrap_or(usize::MAX); + let mut pollables = Vec::with_capacity(n); + for sub in subs.iter() { + let sub = memory.read(sub?)?; + let p = match sub.u { + types::SubscriptionU::Clock(types::SubscriptionClock { + id, + timeout, + flags, + .. + }) => { + let absolute = flags.contains(types::Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME); + let (timeout, absolute) = match id { + types::Clockid::Monotonic => (timeout, absolute), + types::Clockid::Realtime if !absolute => (timeout, false), + types::Clockid::Realtime => { + let now = wall_clock::Host::now(&mut self.clocks()) + .context("failed to call `wall_clock::now`") + .map_err(types::Error::trap)?; + + // Convert `timeout` to `Datetime` format. + let seconds = timeout / 1_000_000_000; + let nanoseconds = timeout % 1_000_000_000; + + let timeout = if now.seconds < seconds + || now.seconds == seconds + && u64::from(now.nanoseconds) < nanoseconds + { + // `now` is less than `timeout`, which is expressible as u64, + // subtract the nanosecond counts directly + now.seconds * 1_000_000_000 + u64::from(now.nanoseconds) - timeout + } else { + 0 + }; + (timeout, false) + } + _ => return Err(types::Errno::Inval.into()), + }; + if absolute { + monotonic_clock::Host::subscribe_instant(&mut self.clocks(), timeout) + .context("failed to call `monotonic_clock::subscribe_instant`") + .map_err(types::Error::trap)? + } else { + monotonic_clock::Host::subscribe_duration(&mut self.clocks(), timeout) + .context("failed to call `monotonic_clock::subscribe_duration`") + .map_err(types::Error::trap)? + } + } + types::SubscriptionU::FdRead(types::SubscriptionFdReadwrite { + file_descriptor, + }) => { + let stream = { + let t = self.transact()?; + let desc = t.get_descriptor(file_descriptor)?; + match desc { + Descriptor::Stdin { stream, .. } => stream.borrowed(), + Descriptor::File(File { fd, position, .. }) => { + let pos = position.load(Ordering::Relaxed); + let fd = fd.borrowed(); + drop(t); + self.filesystem().read_via_stream(fd, pos)? + } + // TODO: Support sockets + _ => return Err(types::Errno::Badf.into()), + } + }; + streams::HostInputStream::subscribe(&mut self.table, stream) + .context("failed to call `subscribe` on `input-stream`") + .map_err(types::Error::trap)? + } + types::SubscriptionU::FdWrite(types::SubscriptionFdReadwrite { + file_descriptor, + }) => { + let stream = { + let t = self.transact()?; + let desc = t.get_descriptor(file_descriptor)?; + match desc { + Descriptor::Stdout { stream, .. } + | Descriptor::Stderr { stream, .. } => stream.borrowed(), + Descriptor::File(File { + fd, + position, + append, + .. + }) => { + let fd = fd.borrowed(); + let position = position.clone(); + let append = *append; + drop(t); + if append { + self.filesystem().append_via_stream(fd)? + } else { + let pos = position.load(Ordering::Relaxed); + self.filesystem().write_via_stream(fd, pos)? + } + } + // TODO: Support sockets + _ => return Err(types::Errno::Badf.into()), + } + }; + streams::HostOutputStream::subscribe(&mut self.table, stream) + .context("failed to call `subscribe` on `output-stream`") + .map_err(types::Error::trap)? + } + }; + pollables.push(p); + } + let ready: HashSet<_> = self + .table + .poll(pollables) + .await + .context("failed to call `poll-oneoff`") + .map_err(types::Error::trap)? + .into_iter() + .collect(); + + let mut count: types::Size = 0; + for (sub, event) in (0..) + .zip(subs.iter()) + .filter_map(|(idx, sub)| ready.contains(&idx).then_some(sub)) + .zip(events.iter()) + { + let sub = memory.read(sub?)?; + let event = event?; + let e = match sub.u { + types::SubscriptionU::Clock(..) => types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::Clock, + fd_readwrite: types::EventFdReadwrite { + flags: types::Eventrwflags::empty(), + nbytes: 0, + }, + }, + types::SubscriptionU::FdRead(types::SubscriptionFdReadwrite { + file_descriptor, + }) => { + let t = self.transact()?; + let desc = t.get_descriptor(file_descriptor)?; + match desc { + Descriptor::Stdin { .. } => types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::FdRead, + fd_readwrite: types::EventFdReadwrite { + flags: types::Eventrwflags::empty(), + nbytes: 1, + }, + }, + Descriptor::File(File { fd, position, .. }) => { + let fd = fd.borrowed(); + let position = position.clone(); + drop(t); + match self.filesystem().stat(fd).await? { + filesystem::DescriptorStat { size, .. } => { + let pos = position.load(Ordering::Relaxed); + let nbytes = size.saturating_sub(pos); + types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::FdRead, + fd_readwrite: types::EventFdReadwrite { + flags: if nbytes == 0 { + types::Eventrwflags::FD_READWRITE_HANGUP + } else { + types::Eventrwflags::empty() + }, + nbytes: 1, + }, + } + } + } + } + // TODO: Support sockets + _ => return Err(types::Errno::Badf.into()), + } + } + types::SubscriptionU::FdWrite(types::SubscriptionFdReadwrite { + file_descriptor, + }) => { + let t = self.transact()?; + let desc = t.get_descriptor(file_descriptor)?; + match desc { + Descriptor::Stdout { .. } | Descriptor::Stderr { .. } => types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::FdWrite, + fd_readwrite: types::EventFdReadwrite { + flags: types::Eventrwflags::empty(), + nbytes: 1, + }, + }, + Descriptor::File(_) => types::Event { + userdata: sub.userdata, + error: types::Errno::Success, + type_: types::Eventtype::FdWrite, + fd_readwrite: types::EventFdReadwrite { + flags: types::Eventrwflags::empty(), + nbytes: 1, + }, + }, + // TODO: Support sockets + _ => return Err(types::Errno::Badf.into()), + } + } + }; + memory.write(event, e)?; + count = count + .checked_add(1) + .ok_or_else(|| types::Error::from(types::Errno::Overflow))? + } + Ok(count) + } + + #[instrument(skip(self, _memory))] + fn proc_exit( + &mut self, + _memory: &mut GuestMemory<'_>, + status: types::Exitcode, + ) -> anyhow::Error { + // Check that the status is within WASI's range. + if status >= 126 { + return anyhow::Error::msg("exit with invalid exit status outside of [0..126)"); + } + crate::I32Exit(status as i32).into() + } + + #[instrument(skip(self, _memory))] + fn proc_raise( + &mut self, + _memory: &mut GuestMemory<'_>, + _sig: types::Signal, + ) -> Result<(), types::Error> { + Err(types::Errno::Notsup.into()) + } + + #[instrument(skip(self, _memory))] + fn sched_yield(&mut self, _memory: &mut GuestMemory<'_>) -> Result<(), types::Error> { + // No such thing in preview 2. Intentionally left empty. + Ok(()) + } + + #[instrument(skip(self, memory))] + fn random_get( + &mut self, + memory: &mut GuestMemory<'_>, + buf: GuestPtr, + buf_len: types::Size, + ) -> Result<(), types::Error> { + let rand = self + .wasi + .random + .get_random_bytes(buf_len.into()) + .context("failed to call `get-random-bytes`") + .map_err(types::Error::trap)?; + write_bytes(memory, buf, &rand)?; + Ok(()) + } + + #[instrument(skip(self, _memory))] + fn sock_accept( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + flags: types::Fdflags, + ) -> Result { + tracing::warn!("p1 sock_accept is not implemented"); + self.transact()?.get_descriptor(fd)?; + Err(types::Errno::Notsock.into()) + } + + #[instrument(skip(self, _memory))] + fn sock_recv( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + ri_data: types::IovecArray, + ri_flags: types::Riflags, + ) -> Result<(types::Size, types::Roflags), types::Error> { + tracing::warn!("p1 sock_recv is not implemented"); + self.transact()?.get_descriptor(fd)?; + Err(types::Errno::Notsock.into()) + } + + #[instrument(skip(self, _memory))] + fn sock_send( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + si_data: types::CiovecArray, + _si_flags: types::Siflags, + ) -> Result { + tracing::warn!("p1 sock_send is not implemented"); + self.transact()?.get_descriptor(fd)?; + Err(types::Errno::Notsock.into()) + } + + #[instrument(skip(self, _memory))] + fn sock_shutdown( + &mut self, + _memory: &mut GuestMemory<'_>, + fd: types::Fd, + how: types::Sdflags, + ) -> Result<(), types::Error> { + tracing::warn!("p1 sock_shutdown is not implemented"); + self.transact()?.get_descriptor(fd)?; + Err(types::Errno::Notsock.into()) + } +} + +trait ResourceExt { + fn borrowed(&self) -> Resource; +} + +impl ResourceExt for Resource { + fn borrowed(&self) -> Resource { + Resource::new_borrow(self.rep()) + } +} diff --git a/crates/wasi/src/p2/bindings.rs b/crates/wasi/src/p2/bindings.rs new file mode 100644 index 00000000..b880f7a1 --- /dev/null +++ b/crates/wasi/src/p2/bindings.rs @@ -0,0 +1,557 @@ +//! Auto-generated bindings for WASI interfaces. +//! +//! This module contains the output of the [`bindgen!`] macro when run over +//! the `wasi:cli/command` world. That means this module has all the generated +//! types for WASI for all of its base interfaces used by the CLI world. This +//! module itself by default contains bindings for `async`-related traits. The +//! [`sync`] module contains bindings for a non-`async` version of types. +//! +//! [`bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! +//! # Examples +//! +//! If you have a WIT world which refers to WASI interfaces you probably want to +//! use this modules's bindings rather than generate fresh bindings. That can be +//! done using the `with` option to [`bindgen!`]: +//! +//! ```rust +//! use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! use wasmtime::{Result, Engine, Config}; +//! use wasmtime::component::{Linker, ResourceTable, HasSelf}; +//! +//! wasmtime::component::bindgen!({ +//! inline: " +//! package example:wasi; +//! +//! // An example of extending the `wasi:cli/command` world with a +//! // custom host interface. +//! world my-world { +//! include wasi:cli/command@0.2.6; +//! +//! import custom-host; +//! } +//! +//! interface custom-host { +//! my-custom-function: func(); +//! } +//! ", +//! path: "src/p2/wit", +//! with: { +//! "wasi": wash_wasi::p2::bindings, +//! }, +//! imports: { default: async }, +//! }); +//! +//! struct MyState { +//! table: ResourceTable, +//! ctx: WasiCtx, +//! } +//! +//! impl example::wasi::custom_host::Host for MyState { +//! async fn my_custom_function(&mut self) { +//! // .. +//! } +//! } +//! +//! impl WasiView for MyState { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +//! } +//! } +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.async_support(true); +//! let engine = Engine::new(&config)?; +//! let mut linker: Linker = Linker::new(&engine); +//! wash_wasi::p2::add_to_linker_async(&mut linker)?; +//! example::wasi::custom_host::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state)?; +//! +//! // .. use `Linker` to instantiate component ... +//! +//! Ok(()) +//! } +//! ``` + +/// Synchronous-generated bindings for WASI interfaces. +/// +/// This is the same as the top-level [`bindings`](crate::p2::bindings) submodule of +/// this module except that it's for synchronous calls. +/// +/// # Examples +/// +/// If you have a WIT world which refers to WASI interfaces you probably want to +/// use this modules's bindings rather than generate fresh bindings. That can be +/// done using the `with` option to `bindgen!`: +/// +/// ```rust +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime::{Result, Engine}; +/// use wasmtime::component::{Linker, ResourceTable, HasSelf}; +/// +/// wasmtime::component::bindgen!({ +/// inline: " +/// package example:wasi; +/// +/// // An example of extending the `wasi:cli/command` world with a +/// // custom host interface. +/// world my-world { +/// include wasi:cli/command@0.2.6; +/// +/// import custom-host; +/// } +/// +/// interface custom-host { +/// my-custom-function: func(); +/// } +/// ", +/// path: "src/p2/wit", +/// with: { +/// "wasi": wash_wasi::p2::bindings::sync, +/// }, +/// // This is required for bindings using `wasmtime-wasi` and it otherwise +/// // isn't the default for non-async bindings. +/// require_store_data_send: true, +/// }); +/// +/// struct MyState { +/// table: ResourceTable, +/// ctx: WasiCtx, +/// } +/// +/// impl example::wasi::custom_host::Host for MyState { +/// fn my_custom_function(&mut self) { +/// // .. +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// +/// fn main() -> Result<()> { +/// let engine = Engine::default(); +/// let mut linker: Linker = Linker::new(&engine); +/// wash_wasi::p2::add_to_linker_sync(&mut linker)?; +/// example::wasi::custom_host::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state)?; +/// +/// // .. use `Linker` to instantiate component ... +/// +/// Ok(()) +/// } +/// ``` +pub mod sync { + mod generated { + use crate::p2::{FsError, SocketError}; + use wasmtime_wasi_io::streams::StreamError; + + wasmtime::component::bindgen!({ + path: "src/p2/wit", + world: "wasi:cli/command", + trappable_error_type: { + "wasi:io/streams/stream-error" => StreamError, + "wasi:filesystem/types/error-code" => FsError, + "wasi:sockets/network/error-code" => SocketError, + }, + imports: { default: tracing | trappable }, + with: { + // These interfaces contain only synchronous methods, so they + // can be aliased directly + "wasi:clocks": crate::p2::bindings::clocks, + "wasi:random": crate::p2::bindings::random, + "wasi:cli": crate::p2::bindings::cli, + "wasi:filesystem/preopens": crate::p2::bindings::filesystem::preopens, + "wasi:sockets/network": crate::p2::bindings::sockets::network, + + // Configure the resource types of the bound interfaces here + // to be the same as the async versions of the resources, that + // way everything has the same type. + "wasi:filesystem/types/descriptor": crate::filesystem::Descriptor, + "wasi:filesystem/types/directory-entry-stream": super::super::filesystem::types::DirectoryEntryStream, + "wasi:sockets/tcp/tcp-socket": super::super::sockets::tcp::TcpSocket, + "wasi:sockets/udp/incoming-datagram-stream": super::super::sockets::udp::IncomingDatagramStream, + "wasi:sockets/udp/outgoing-datagram-stream": super::super::sockets::udp::OutgoingDatagramStream, + "wasi:sockets/udp/udp-socket": crate::sockets::UdpSocket, + + // Error host trait from wasmtime-wasi-io is synchronous, so we can alias it + "wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error, + // Configure the resource types from wasmtime-wasi-io, though + // this bindgen will make a new synchronous Host traits + "wasi:io/poll/pollable": wasmtime_wasi_io::poll::DynPollable, + "wasi:io/streams/input-stream": wasmtime_wasi_io::streams::DynInputStream, + "wasi:io/streams/output-stream": wasmtime_wasi_io::streams::DynOutputStream, + + }, + require_store_data_send: true, + }); + } + pub use self::generated::exports; + pub use self::generated::wasi::*; + + /// Synchronous bindings to execute and run a `wasi:cli/command`. + /// + /// This structure is automatically generated by `bindgen!` and is intended + /// to be used with [`Config::async_support(false)`][async]. For the + /// asynchronous version see [`bindings::Command`](super::Command). + /// + /// This can be used for a more "typed" view of executing a command + /// component through the [`Command::wasi_cli_run`] method plus + /// [`Guest::call_run`](exports::wasi::cli::run::Guest::call_run). + /// + /// [async]: wasmtime::Config::async_support + /// [`wash_wasi::p2::add_to_linker_sync`]: crate::p2::add_to_linker_sync + /// + /// # Examples + /// + /// ```no_run + /// use wasmtime::{Engine, Result, Store, Config}; + /// use wasmtime::component::{ResourceTable, Linker, Component}; + /// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; + /// use wash_wasi::p2::bindings::sync::Command; + /// + /// // This example is an example shim of executing a component based on the + /// // command line arguments provided to this program. + /// fn main() -> Result<()> { + /// let args = std::env::args().skip(1).collect::>(); + /// + /// // Configure and create `Engine` + /// let engine = Engine::default(); + /// + /// // Configure a `Linker` with WASI, compile a component based on + /// // command line arguments. + /// let mut linker = Linker::::new(&engine); + /// wash_wasi::p2::add_to_linker_sync(&mut linker)?; + /// let component = Component::from_file(&engine, &args[0])?; + /// + /// + /// // Configure a `WasiCtx` based on this program's environment. Then + /// // build a `Store` to instantiate into. + /// let mut builder = WasiCtx::builder(); + /// builder.inherit_stdio().inherit_env().args(&args[2..]); + /// let mut store = Store::new( + /// &engine, + /// MyState { + /// ctx: builder.build(), + /// table: ResourceTable::new(), + /// }, + /// ); + /// + /// // Instantiate the component and we're off to the races. + /// let command = Command::instantiate(&mut store, &component, &linker)?; + /// let program_result = command.wasi_cli_run().call_run(&mut store)?; + /// match program_result { + /// Ok(()) => Ok(()), + /// Err(()) => std::process::exit(1), + /// } + /// } + /// + /// struct MyState { + /// ctx: WasiCtx, + /// table: ResourceTable, + /// } + /// + /// impl WasiView for MyState { + /// fn ctx(&mut self) -> WasiCtxView<'_> { + /// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } + /// } + /// } + /// ``` + /// + /// --- + pub use self::generated::Command; + + /// Pre-instantiated analogue of [`Command`]. + /// + /// This works the same as [`Command`] but enables front-loading work such + /// as export lookup to before instantiation. + /// + /// # Examples + /// + /// ```no_run + /// use wasmtime::{Engine, Result, Store, Config}; + /// use wasmtime::component::{ResourceTable, Linker, Component}; + /// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; + /// use wash_wasi::p2::bindings::sync::CommandPre; + /// + /// // This example is an example shim of executing a component based on the + /// // command line arguments provided to this program. + /// fn main() -> Result<()> { + /// let args = std::env::args().skip(1).collect::>(); + /// + /// // Configure and create `Engine` + /// let engine = Engine::default(); + /// + /// // Configure a `Linker` with WASI, compile a component based on + /// // command line arguments, and then pre-instantiate it. + /// let mut linker = Linker::::new(&engine); + /// wash_wasi::p2::add_to_linker_sync(&mut linker)?; + /// let component = Component::from_file(&engine, &args[0])?; + /// let pre = CommandPre::new(linker.instantiate_pre(&component)?)?; + /// + /// + /// // Configure a `WasiCtx` based on this program's environment. Then + /// // build a `Store` to instantiate into. + /// let mut builder = WasiCtx::builder(); + /// builder.inherit_stdio().inherit_env().args(&args); + /// let mut store = Store::new( + /// &engine, + /// MyState { + /// ctx: builder.build(), + /// table: ResourceTable::new(), + /// }, + /// ); + /// + /// // Instantiate the component and we're off to the races. + /// let command = pre.instantiate(&mut store)?; + /// let program_result = command.wasi_cli_run().call_run(&mut store)?; + /// match program_result { + /// Ok(()) => Ok(()), + /// Err(()) => std::process::exit(1), + /// } + /// } + /// + /// struct MyState { + /// ctx: WasiCtx, + /// table: ResourceTable, + /// } + /// + /// impl WasiView for MyState { + /// fn ctx(&mut self) -> WasiCtxView<'_> { + /// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } + /// } + /// } + /// ``` + /// + /// --- + pub use self::generated::CommandPre; + + pub use self::generated::CommandIndices; + + pub use self::generated::LinkOptions; +} + +mod async_io { + wasmtime::component::bindgen!({ + path: "src/p2/wit", + world: "wasi:cli/command", + imports: { + // Only these functions are `async` and everything else is sync + // meaning that it basically doesn't need to block. These functions + // are the only ones that need to block. + // + // Note that at this time `only_imports` works on function names + // which in theory can be shared across interfaces, so this may + // need fancier syntax in the future. + "wasi:filesystem/types/[method]descriptor.advise": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.create-directory-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.get-flags": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.get-type": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.is-same-object": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.link-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.metadata-hash": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.metadata-hash-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.open-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.read": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.read-directory": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.readlink-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.remove-directory-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.rename-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.set-size": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.set-times": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.set-times-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.stat": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.stat-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.symlink-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.sync": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.sync-data": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.unlink-file-at": async | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.write": async | tracing | trappable, + "wasi:filesystem/types/[method]directory-entry-stream.read-directory-entry": async | tracing | trappable, + "wasi:sockets/tcp/[method]tcp-socket.shutdown": async | tracing | trappable, + "wasi:sockets/tcp/[method]tcp-socket.start-bind": async | tracing | trappable, + "wasi:sockets/tcp/[method]tcp-socket.start-connect": async | tracing | trappable, + "wasi:sockets/udp/[method]udp-socket.start-bind": async | tracing | trappable, + "wasi:sockets/udp/[method]udp-socket.stream": async | tracing | trappable, + "wasi:sockets/udp/[method]outgoing-datagram-stream.send": async | tracing | trappable, + default: tracing | trappable, + }, + exports: { default: async }, + trappable_error_type: { + "wasi:io/streams/stream-error" => wasmtime_wasi_io::streams::StreamError, + "wasi:filesystem/types/error-code" => crate::p2::FsError, + "wasi:sockets/network/error-code" => crate::p2::SocketError, + }, + with: { + // All interfaces in the wasi:io package should be aliased to + // the wasmtime-wasi-io generated code. Note that this will also + // map the resource types to those defined in that crate as well. + "wasi:io/poll": wasmtime_wasi_io::bindings::wasi::io::poll, + "wasi:io/streams": wasmtime_wasi_io::bindings::wasi::io::streams, + "wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error, + + // Configure all other resources to be concrete types defined in + // this crate + "wasi:sockets/network/network": crate::p2::network::Network, + "wasi:sockets/tcp/tcp-socket": crate::sockets::TcpSocket, + "wasi:sockets/udp/udp-socket": crate::sockets::UdpSocket, + "wasi:sockets/udp/incoming-datagram-stream": crate::p2::udp::IncomingDatagramStream, + "wasi:sockets/udp/outgoing-datagram-stream": crate::p2::udp::OutgoingDatagramStream, + "wasi:sockets/ip-name-lookup/resolve-address-stream": crate::p2::ip_name_lookup::ResolveAddressStream, + "wasi:filesystem/types/directory-entry-stream": crate::p2::filesystem::ReaddirIterator, + "wasi:filesystem/types/descriptor": crate::filesystem::Descriptor, + "wasi:cli/terminal-input/terminal-input": crate::p2::stdio::TerminalInput, + "wasi:cli/terminal-output/terminal-output": crate::p2::stdio::TerminalOutput, + }, + }); +} + +pub use self::async_io::LinkOptions; +pub use self::async_io::exports; +pub use self::async_io::wasi::*; + +/// Asynchronous bindings to execute and run a `wasi:cli/command`. +/// +/// This structure is automatically generated by `bindgen!` and is intended to +/// be used with [`Config::async_support(true)`][async]. For the synchronous +/// version see [`bindings::sync::Command`](sync::Command). +/// +/// This can be used for a more "typed" view of executing a command component +/// through the [`Command::wasi_cli_run`] method plus +/// [`Guest::call_run`](exports::wasi::cli::run::Guest::call_run). +/// +/// [async]: wasmtime::Config::async_support +/// [`wash_wasi::p2::add_to_linker_async`]: crate::p2::add_to_linker_async +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker, Component}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wash_wasi::p2::bindings::Command; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p2::add_to_linker_async(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtx::builder(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = Command::instantiate_async(&mut store, &component, &linker).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +/// +/// --- +pub use self::async_io::Command; + +/// Pre-instantiated analog of [`Command`] +/// +/// This can be used to front-load work such as export lookup before +/// instantiation. +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker, Component}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wash_wasi::p2::bindings::CommandPre; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p2::add_to_linker_async(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// let pre = CommandPre::new(linker.instantiate_pre(&component)?)?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtx::builder(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = pre.instantiate_async(&mut store).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +/// +/// --- +pub use self::async_io::CommandPre; + +pub use self::async_io::CommandIndices; diff --git a/crates/wasi/src/p2/filesystem.rs b/crates/wasi/src/p2/filesystem.rs new file mode 100644 index 00000000..608349dc --- /dev/null +++ b/crates/wasi/src/p2/filesystem.rs @@ -0,0 +1,420 @@ +use crate::TrappableError; +use crate::filesystem::File; +use crate::p2::bindings::filesystem::types; +use crate::p2::{InputStream, OutputStream, Pollable, StreamError, StreamResult}; +use crate::runtime::AbortOnDropJoinHandle; +use anyhow::anyhow; +use bytes::{Bytes, BytesMut}; +use std::io; +use std::mem; + +pub type FsResult = Result; + +pub type FsError = TrappableError; + +impl From for types::ErrorCode { + fn from(error: crate::filesystem::ErrorCode) -> Self { + match error { + crate::filesystem::ErrorCode::Access => Self::Access, + crate::filesystem::ErrorCode::Already => Self::Already, + crate::filesystem::ErrorCode::BadDescriptor => Self::BadDescriptor, + crate::filesystem::ErrorCode::Busy => Self::Busy, + crate::filesystem::ErrorCode::Exist => Self::Exist, + crate::filesystem::ErrorCode::FileTooLarge => Self::FileTooLarge, + crate::filesystem::ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, + crate::filesystem::ErrorCode::InProgress => Self::InProgress, + crate::filesystem::ErrorCode::Interrupted => Self::Interrupted, + crate::filesystem::ErrorCode::Invalid => Self::Invalid, + crate::filesystem::ErrorCode::Io => Self::Io, + crate::filesystem::ErrorCode::IsDirectory => Self::IsDirectory, + crate::filesystem::ErrorCode::Loop => Self::Loop, + crate::filesystem::ErrorCode::TooManyLinks => Self::TooManyLinks, + crate::filesystem::ErrorCode::NameTooLong => Self::NameTooLong, + crate::filesystem::ErrorCode::NoEntry => Self::NoEntry, + crate::filesystem::ErrorCode::InsufficientMemory => Self::InsufficientMemory, + crate::filesystem::ErrorCode::InsufficientSpace => Self::InsufficientSpace, + crate::filesystem::ErrorCode::NotDirectory => Self::NotDirectory, + crate::filesystem::ErrorCode::NotEmpty => Self::NotEmpty, + crate::filesystem::ErrorCode::Unsupported => Self::Unsupported, + crate::filesystem::ErrorCode::Overflow => Self::Overflow, + crate::filesystem::ErrorCode::NotPermitted => Self::NotPermitted, + crate::filesystem::ErrorCode::Pipe => Self::Pipe, + crate::filesystem::ErrorCode::InvalidSeek => Self::InvalidSeek, + } + } +} + +impl From for FsError { + fn from(error: crate::filesystem::ErrorCode) -> Self { + types::ErrorCode::from(error).into() + } +} + +impl From for FsError { + fn from(error: wasmtime::component::ResourceTableError) -> Self { + Self::trap(error) + } +} + +impl From for FsError { + fn from(error: io::Error) -> Self { + types::ErrorCode::from(error).into() + } +} + +pub struct FileInputStream { + file: File, + position: u64, + state: ReadState, +} +enum ReadState { + Idle, + Waiting(AbortOnDropJoinHandle), + DataAvailable(Bytes), + Error(io::Error), + Closed, +} +impl FileInputStream { + pub fn new(file: &File, position: u64) -> Self { + Self { + file: file.clone(), + position, + state: ReadState::Idle, + } + } + + fn blocking_read(file: &cap_std::fs::File, offset: u64, size: usize) -> ReadState { + use system_interface::fs::FileIoExt; + + let mut buf = BytesMut::zeroed(size); + loop { + match file.read_at(&mut buf, offset) { + Ok(0) => return ReadState::Closed, + Ok(n) => { + buf.truncate(n); + return ReadState::DataAvailable(buf.freeze()); + } + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => { + // Try again, continue looping + } + Err(e) => return ReadState::Error(e), + } + } + } + + /// Wait for existing background task to finish, without starting any new background reads. + async fn wait_ready(&mut self) { + match &mut self.state { + ReadState::Waiting(task) => { + self.state = task.await; + } + _ => {} + } + } +} +#[async_trait::async_trait] +impl InputStream for FileInputStream { + fn read(&mut self, size: usize) -> StreamResult { + match &mut self.state { + ReadState::Idle => { + if size == 0 { + return Ok(Bytes::new()); + } + + let p = self.position; + self.state = ReadState::Waiting( + self.file + .spawn_blocking(move |f| Self::blocking_read(f, p, size)), + ); + Ok(Bytes::new()) + } + ReadState::DataAvailable(b) => { + let min_len = b.len().min(size); + let chunk = b.split_to(min_len); + if b.len() == 0 { + self.state = ReadState::Idle; + } + self.position += min_len as u64; + Ok(chunk) + } + ReadState::Waiting(_) => Ok(Bytes::new()), + ReadState::Error(_) => match mem::replace(&mut self.state, ReadState::Closed) { + ReadState::Error(e) => Err(StreamError::LastOperationFailed(e.into())), + _ => unreachable!(), + }, + ReadState::Closed => Err(StreamError::Closed), + } + } + /// Specialized blocking_* variant to bypass tokio's task spawning & joining + /// overhead on synchronous file I/O. + async fn blocking_read(&mut self, size: usize) -> StreamResult { + self.wait_ready().await; + + // Before we defer to the regular `read`, make sure it has data ready to go: + if let ReadState::Idle = self.state { + let p = self.position; + self.state = self + .file + .run_blocking(move |f| Self::blocking_read(f, p, size)) + .await; + } + + self.read(size) + } + async fn cancel(&mut self) { + match mem::replace(&mut self.state, ReadState::Closed) { + ReadState::Waiting(task) => { + // The task was created using `spawn_blocking`, so unless we're + // lucky enough that the task hasn't started yet, the abort + // signal won't have any effect and we're forced to wait for it + // to run to completion. + // From the guest's point of view, `input-stream::drop` then + // appears to block. Certainly less than ideal, but arguably still + // better than letting the guest rack up an unbounded number of + // background tasks. Also, the guest is only blocked if + // the stream was dropped mid-read, which we don't expect to + // occur frequently. + task.cancel().await; + } + _ => {} + } + } +} +#[async_trait::async_trait] +impl Pollable for FileInputStream { + async fn ready(&mut self) { + if let ReadState::Idle = self.state { + // The guest hasn't initiated any read, but is nonetheless waiting + // for data to be available. We'll start a read for them: + + const DEFAULT_READ_SIZE: usize = 4096; + let p = self.position; + self.state = ReadState::Waiting( + self.file + .spawn_blocking(move |f| Self::blocking_read(f, p, DEFAULT_READ_SIZE)), + ); + } + + self.wait_ready().await + } +} + +#[derive(Clone, Copy)] +pub(crate) enum FileOutputMode { + Position(u64), + Append, +} + +pub(crate) struct FileOutputStream { + file: File, + mode: FileOutputMode, + state: OutputState, +} + +enum OutputState { + Ready, + /// Allows join future to be awaited in a cancellable manner. Gone variant indicates + /// no task is currently outstanding. + Waiting(AbortOnDropJoinHandle>), + /// The last I/O operation failed with this error. + Error(io::Error), + Closed, +} + +impl FileOutputStream { + pub fn write_at(file: &File, position: u64) -> Self { + Self { + file: file.clone(), + mode: FileOutputMode::Position(position), + state: OutputState::Ready, + } + } + + pub fn append(file: &File) -> Self { + Self { + file: file.clone(), + mode: FileOutputMode::Append, + state: OutputState::Ready, + } + } + + fn blocking_write( + file: &cap_std::fs::File, + mut buf: Bytes, + mode: FileOutputMode, + ) -> io::Result { + use system_interface::fs::FileIoExt; + + match mode { + FileOutputMode::Position(mut p) => { + let mut total = 0; + loop { + let nwritten = file.write_at(buf.as_ref(), p)?; + // afterwards buf contains [nwritten, len): + let _ = buf.split_to(nwritten); + p += nwritten as u64; + total += nwritten; + if buf.is_empty() { + break; + } + } + Ok(total) + } + FileOutputMode::Append => { + let mut total = 0; + loop { + let nwritten = file.append(buf.as_ref())?; + let _ = buf.split_to(nwritten); + total += nwritten; + if buf.is_empty() { + break; + } + } + Ok(total) + } + } + } +} + +// FIXME: configurable? determine from how much space left in file? +const FILE_WRITE_CAPACITY: usize = 1024 * 1024; + +#[async_trait::async_trait] +impl OutputStream for FileOutputStream { + fn write(&mut self, buf: Bytes) -> Result<(), StreamError> { + match self.state { + OutputState::Ready => {} + OutputState::Closed => return Err(StreamError::Closed), + OutputState::Waiting(_) | OutputState::Error(_) => { + // a write is pending - this call was not permitted + return Err(StreamError::Trap(anyhow!( + "write not permitted: check_write not called first" + ))); + } + } + + let m = self.mode; + self.state = OutputState::Waiting( + self.file + .spawn_blocking(move |f| Self::blocking_write(f, buf, m)), + ); + Ok(()) + } + /// Specialized blocking_* variant to bypass tokio's task spawning & joining + /// overhead on synchronous file I/O. + async fn blocking_write_and_flush(&mut self, buf: Bytes) -> StreamResult<()> { + self.ready().await; + + match self.state { + OutputState::Ready => {} + OutputState::Closed => return Err(StreamError::Closed), + OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) { + OutputState::Error(e) => return Err(StreamError::LastOperationFailed(e.into())), + _ => unreachable!(), + }, + OutputState::Waiting(_) => unreachable!("we've just waited for readiness"), + } + + let m = self.mode; + match self + .file + .run_blocking(move |f| Self::blocking_write(f, buf, m)) + .await + { + Ok(nwritten) => { + if let FileOutputMode::Position(p) = &mut self.mode { + *p += nwritten as u64; + } + self.state = OutputState::Ready; + Ok(()) + } + Err(e) => { + self.state = OutputState::Closed; + Err(StreamError::LastOperationFailed(e.into())) + } + } + } + fn flush(&mut self) -> Result<(), StreamError> { + match self.state { + // Only userland buffering of file writes is in the blocking task, + // so there's nothing extra that needs to be done to request a + // flush. + OutputState::Ready | OutputState::Waiting(_) => Ok(()), + OutputState::Closed => Err(StreamError::Closed), + OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) { + OutputState::Error(e) => Err(StreamError::LastOperationFailed(e.into())), + _ => unreachable!(), + }, + } + } + fn check_write(&mut self) -> Result { + match self.state { + OutputState::Ready => Ok(FILE_WRITE_CAPACITY), + OutputState::Closed => Err(StreamError::Closed), + OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) { + OutputState::Error(e) => Err(StreamError::LastOperationFailed(e.into())), + _ => unreachable!(), + }, + OutputState::Waiting(_) => Ok(0), + } + } + async fn cancel(&mut self) { + match mem::replace(&mut self.state, OutputState::Closed) { + OutputState::Waiting(task) => { + // The task was created using `spawn_blocking`, so unless we're + // lucky enough that the task hasn't started yet, the abort + // signal won't have any effect and we're forced to wait for it + // to run to completion. + // From the guest's point of view, `output-stream::drop` then + // appears to block. Certainly less than ideal, but arguably still + // better than letting the guest rack up an unbounded number of + // background tasks. Also, the guest is only blocked if + // the stream was dropped mid-write, which we don't expect to + // occur frequently. + task.cancel().await; + } + _ => {} + } + } +} + +#[async_trait::async_trait] +impl Pollable for FileOutputStream { + async fn ready(&mut self) { + if let OutputState::Waiting(task) = &mut self.state { + self.state = match task.await { + Ok(nwritten) => { + if let FileOutputMode::Position(p) = &mut self.mode { + *p += nwritten as u64; + } + OutputState::Ready + } + Err(e) => OutputState::Error(e), + }; + } + } +} + +pub struct ReaddirIterator( + std::sync::Mutex> + Send + 'static>>, +); + +impl ReaddirIterator { + pub(crate) fn new( + i: impl Iterator> + Send + 'static, + ) -> Self { + ReaddirIterator(std::sync::Mutex::new(Box::new(i))) + } + pub(crate) fn next(&self) -> FsResult> { + self.0.lock().unwrap().next().transpose() + } +} + +impl IntoIterator for ReaddirIterator { + type Item = FsResult; + type IntoIter = Box + Send>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_inner().unwrap() + } +} diff --git a/crates/wasi/src/p2/host/clocks.rs b/crates/wasi/src/p2/host/clocks.rs new file mode 100644 index 00000000..95dee36f --- /dev/null +++ b/crates/wasi/src/p2/host/clocks.rs @@ -0,0 +1,128 @@ +use crate::clocks::WasiClocksCtxView; +use crate::p2::DynPollable; +use crate::p2::bindings::{ + clocks::monotonic_clock::{self, Duration as WasiDuration, Instant}, + clocks::wall_clock::{self, Datetime}, +}; +use cap_std::time::SystemTime; +use std::time::Duration; +use wasmtime::component::Resource; +use wasmtime_wasi_io::poll::{Pollable, subscribe}; + +impl From for Datetime { + fn from( + crate::clocks::Datetime { + seconds, + nanoseconds, + }: crate::clocks::Datetime, + ) -> Self { + Self { + seconds, + nanoseconds, + } + } +} + +impl From for crate::clocks::Datetime { + fn from( + Datetime { + seconds, + nanoseconds, + }: Datetime, + ) -> Self { + Self { + seconds, + nanoseconds, + } + } +} + +impl TryFrom for Datetime { + type Error = wasmtime::Error; + + fn try_from(time: SystemTime) -> Result { + let time = crate::clocks::Datetime::try_from(time)?; + Ok(time.into()) + } +} + +impl wall_clock::Host for WasiClocksCtxView<'_> { + fn now(&mut self) -> anyhow::Result { + let now = self.ctx.wall_clock.now(); + Ok(Datetime { + seconds: now.as_secs(), + nanoseconds: now.subsec_nanos(), + }) + } + + fn resolution(&mut self) -> anyhow::Result { + let res = self.ctx.wall_clock.resolution(); + Ok(Datetime { + seconds: res.as_secs(), + nanoseconds: res.subsec_nanos(), + }) + } +} + +fn subscribe_to_duration( + table: &mut wasmtime::component::ResourceTable, + duration: tokio::time::Duration, +) -> anyhow::Result> { + let sleep = if duration.is_zero() { + table.push(Deadline::Past)? + } else if let Some(deadline) = tokio::time::Instant::now().checked_add(duration) { + // NB: this resource created here is not actually exposed to wasm, it's + // only an internal implementation detail used to match the signature + // expected by `subscribe`. + table.push(Deadline::Instant(deadline))? + } else { + // If the user specifies a time so far in the future we can't + // represent it, wait forever rather than trap. + table.push(Deadline::Never)? + }; + subscribe(table, sleep) +} + +impl monotonic_clock::Host for WasiClocksCtxView<'_> { + fn now(&mut self) -> anyhow::Result { + Ok(self.ctx.monotonic_clock.now()) + } + + fn resolution(&mut self) -> anyhow::Result { + Ok(self.ctx.monotonic_clock.resolution()) + } + + fn subscribe_instant(&mut self, when: Instant) -> anyhow::Result> { + let clock_now = self.ctx.monotonic_clock.now(); + let duration = if when > clock_now { + Duration::from_nanos(when - clock_now) + } else { + Duration::from_nanos(0) + }; + subscribe_to_duration(self.table, duration) + } + + fn subscribe_duration( + &mut self, + duration: WasiDuration, + ) -> anyhow::Result> { + subscribe_to_duration(self.table, Duration::from_nanos(duration)) + } +} + +enum Deadline { + Past, + Instant(tokio::time::Instant), + Never, +} + +#[async_trait::async_trait] +impl Pollable for Deadline { + async fn ready(&mut self) { + match self { + Deadline::Past => {} + Deadline::Instant(instant) => tokio::time::sleep_until(*instant).await, + Deadline::Never => std::future::pending().await, + } + } +} diff --git a/crates/wasi/src/p2/host/env.rs b/crates/wasi/src/p2/host/env.rs new file mode 100644 index 00000000..0ca8d1c7 --- /dev/null +++ b/crates/wasi/src/p2/host/env.rs @@ -0,0 +1,14 @@ +use crate::cli::WasiCliCtxView; +use crate::p2::bindings::cli::environment; + +impl environment::Host for WasiCliCtxView<'_> { + fn get_environment(&mut self) -> anyhow::Result> { + Ok(self.ctx.environment.clone()) + } + fn get_arguments(&mut self) -> anyhow::Result> { + Ok(self.ctx.arguments.clone()) + } + fn initial_cwd(&mut self) -> anyhow::Result> { + Ok(self.ctx.initial_cwd.clone()) + } +} diff --git a/crates/wasi/src/p2/host/exit.rs b/crates/wasi/src/p2/host/exit.rs new file mode 100644 index 00000000..3e5253f4 --- /dev/null +++ b/crates/wasi/src/p2/host/exit.rs @@ -0,0 +1,17 @@ +use crate::I32Exit; +use crate::cli::WasiCliCtxView; +use crate::p2::bindings::cli::exit; + +impl exit::Host for WasiCliCtxView<'_> { + fn exit(&mut self, status: Result<(), ()>) -> anyhow::Result<()> { + let status = match status { + Ok(()) => 0, + Err(()) => 1, + }; + Err(anyhow::anyhow!(I32Exit(status))) + } + + fn exit_with_code(&mut self, status_code: u8) -> anyhow::Result<()> { + Err(anyhow::anyhow!(I32Exit(status_code.into()))) + } +} diff --git a/crates/wasi/src/p2/host/filesystem.rs b/crates/wasi/src/p2/host/filesystem.rs new file mode 100644 index 00000000..7e10d6d6 --- /dev/null +++ b/crates/wasi/src/p2/host/filesystem.rs @@ -0,0 +1,775 @@ +use crate::filesystem::{Descriptor, WasiFilesystemCtxView}; +use crate::p2::bindings::clocks::wall_clock; +use crate::p2::bindings::filesystem::preopens; +use crate::p2::bindings::filesystem::types::{ + self, ErrorCode, HostDescriptor, HostDirectoryEntryStream, +}; +use crate::p2::filesystem::{FileInputStream, FileOutputStream, ReaddirIterator}; +use crate::p2::{FsError, FsResult}; +use crate::{DirPerms, FilePerms}; +use wasmtime::component::Resource; +use wasmtime_wasi_io::streams::{DynInputStream, DynOutputStream}; + +mod sync; + +impl preopens::Host for WasiFilesystemCtxView<'_> { + fn get_directories(&mut self) -> wasmtime::Result, String)>> { + self.get_directories() + } +} + +impl types::Host for WasiFilesystemCtxView<'_> { + fn convert_error_code(&mut self, err: FsError) -> anyhow::Result { + err.downcast() + } + + fn filesystem_error_code( + &mut self, + err: Resource, + ) -> anyhow::Result> { + let err = self.table.get(&err)?; + + // Currently `err` always comes from the stream implementation which + // uses standard reads/writes so only check for `std::io::Error` here. + if let Some(err) = err.downcast_ref::() { + return Ok(Some(ErrorCode::from(err))); + } + + Ok(None) + } +} + +impl HostDescriptor for WasiFilesystemCtxView<'_> { + async fn advise( + &mut self, + fd: Resource, + offset: types::Filesize, + len: types::Filesize, + advice: types::Advice, + ) -> FsResult<()> { + let f = self.table.get(&fd)?.file()?; + f.advise(offset, len, advice.into()).await?; + Ok(()) + } + + async fn sync_data(&mut self, fd: Resource) -> FsResult<()> { + let descriptor = self.table.get(&fd)?; + descriptor.sync_data().await?; + Ok(()) + } + + async fn get_flags( + &mut self, + fd: Resource, + ) -> FsResult { + let descriptor = self.table.get(&fd)?; + let flags = descriptor.get_flags().await?; + Ok(flags.into()) + } + + async fn get_type( + &mut self, + fd: Resource, + ) -> FsResult { + let descriptor = self.table.get(&fd)?; + let ty = descriptor.get_type().await?; + Ok(ty.into()) + } + + async fn set_size( + &mut self, + fd: Resource, + size: types::Filesize, + ) -> FsResult<()> { + let f = self.table.get(&fd)?.file()?; + f.set_size(size).await?; + Ok(()) + } + + async fn set_times( + &mut self, + fd: Resource, + atim: types::NewTimestamp, + mtim: types::NewTimestamp, + ) -> FsResult<()> { + let descriptor = self.table.get(&fd)?; + let atim = systemtimespec_from(atim)?; + let mtim = systemtimespec_from(mtim)?; + descriptor.set_times(atim, mtim).await?; + Ok(()) + } + + async fn read( + &mut self, + fd: Resource, + len: types::Filesize, + offset: types::Filesize, + ) -> FsResult<(Vec, bool)> { + use std::io::IoSliceMut; + use system_interface::fs::FileIoExt; + + let f = self.table.get(&fd)?.file()?; + if !f.perms.contains(FilePerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + + let (mut buffer, r) = f + .run_blocking(move |f| { + let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; + let r = f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset); + (buffer, r) + }) + .await; + + let (bytes_read, state) = match r? { + 0 => (0, true), + n => (n, false), + }; + + buffer.truncate(bytes_read); + + Ok((buffer, state)) + } + + async fn write( + &mut self, + fd: Resource, + buf: Vec, + offset: types::Filesize, + ) -> FsResult { + use std::io::IoSlice; + use system_interface::fs::FileIoExt; + + let f = self.table.get(&fd)?.file()?; + if !f.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted.into()); + } + + let bytes_written = f + .run_blocking(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) + .await?; + + Ok(types::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) + } + + async fn read_directory( + &mut self, + fd: Resource, + ) -> FsResult> { + let d = self.table.get(&fd)?.dir()?; + if !d.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + + enum ReaddirError { + Io(std::io::Error), + IllegalSequence, + } + impl From for ReaddirError { + fn from(e: std::io::Error) -> ReaddirError { + ReaddirError::Io(e) + } + } + + let entries = d + .run_blocking(|d| { + // Both `entries` and `metadata` perform syscalls, which is why they are done + // within this `block` call, rather than delay calculating the metadata + // for entries when they're demanded later in the iterator chain. + Ok::<_, std::io::Error>( + d.entries()? + .map(|entry| { + let entry = entry?; + let meta = entry.metadata()?; + let type_ = descriptortype_from(meta.file_type()); + let name = entry + .file_name() + .into_string() + .map_err(|_| ReaddirError::IllegalSequence)?; + Ok(types::DirectoryEntry { type_, name }) + }) + .collect::>>(), + ) + }) + .await? + .into_iter(); + + // On windows, filter out files like `C:\DumpStack.log.tmp` which we + // can't get full metadata for. + #[cfg(windows)] + let entries = entries.filter(|entry| { + use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION}; + if let Err(ReaddirError::Io(err)) = entry { + if err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) + || err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) + { + return false; + } + } + true + }); + let entries = entries.map(|r| match r { + Ok(r) => Ok(r), + Err(ReaddirError::Io(e)) => Err(e.into()), + Err(ReaddirError::IllegalSequence) => Err(ErrorCode::IllegalByteSequence.into()), + }); + Ok(self.table.push(ReaddirIterator::new(entries))?) + } + + async fn sync(&mut self, fd: Resource) -> FsResult<()> { + let descriptor = self.table.get(&fd)?; + descriptor.sync().await?; + Ok(()) + } + + async fn create_directory_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + let d = self.table.get(&fd)?.dir()?; + d.create_directory_at(path).await?; + Ok(()) + } + + async fn stat(&mut self, fd: Resource) -> FsResult { + let descriptor = self.table.get(&fd)?; + let stat = descriptor.stat().await?; + Ok(stat.into()) + } + + async fn stat_at( + &mut self, + fd: Resource, + path_flags: types::PathFlags, + path: String, + ) -> FsResult { + let d = self.table.get(&fd)?.dir()?; + let stat = d.stat_at(path_flags.into(), path).await?; + Ok(stat.into()) + } + + async fn set_times_at( + &mut self, + fd: Resource, + path_flags: types::PathFlags, + path: String, + atim: types::NewTimestamp, + mtim: types::NewTimestamp, + ) -> FsResult<()> { + let d = self.table.get(&fd)?.dir()?; + let atim = systemtimespec_from(atim)?; + let mtim = systemtimespec_from(mtim)?; + d.set_times_at(path_flags.into(), path, atim, mtim).await?; + Ok(()) + } + + async fn link_at( + &mut self, + fd: Resource, + // TODO delete the path flags from this function + old_path_flags: types::PathFlags, + old_path: String, + new_descriptor: Resource, + new_path: String, + ) -> FsResult<()> { + let old_dir = self.table.get(&fd)?.dir()?; + let new_dir = self.table.get(&new_descriptor)?.dir()?; + old_dir + .link_at(old_path_flags.into(), old_path, new_dir, new_path) + .await?; + Ok(()) + } + + async fn open_at( + &mut self, + fd: Resource, + path_flags: types::PathFlags, + path: String, + oflags: types::OpenFlags, + flags: types::DescriptorFlags, + ) -> FsResult> { + let d = self.table.get(&fd)?.dir()?; + let fd = d + .open_at( + path_flags.into(), + path, + oflags.into(), + flags.into(), + self.ctx.allow_blocking_current_thread, + ) + .await?; + let fd = self.table.push(fd)?; + Ok(fd) + } + + fn drop(&mut self, fd: Resource) -> anyhow::Result<()> { + // The Drop will close the file/dir, but if the close syscall + // blocks the thread, I will face god and walk backwards into hell. + // tokio::fs::File just uses std::fs::File's Drop impl to close, so + // it doesn't appear anyone else has found this to be a problem. + // (Not that they could solve it without async drop...) + self.table.delete(fd)?; + + Ok(()) + } + + async fn readlink_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult { + let d = self.table.get(&fd)?.dir()?; + let path = d.readlink_at(path).await?; + Ok(path) + } + + async fn remove_directory_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + let d = self.table.get(&fd)?.dir()?; + d.remove_directory_at(path).await?; + Ok(()) + } + + async fn rename_at( + &mut self, + fd: Resource, + old_path: String, + new_fd: Resource, + new_path: String, + ) -> FsResult<()> { + let old_dir = self.table.get(&fd)?.dir()?; + let new_dir = self.table.get(&new_fd)?.dir()?; + old_dir.rename_at(old_path, new_dir, new_path).await?; + Ok(()) + } + + async fn symlink_at( + &mut self, + fd: Resource, + src_path: String, + dest_path: String, + ) -> FsResult<()> { + let d = self.table.get(&fd)?.dir()?; + d.symlink_at(src_path, dest_path).await?; + Ok(()) + } + + async fn unlink_file_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + let d = self.table.get(&fd)?.dir()?; + d.unlink_file_at(path).await?; + Ok(()) + } + + fn read_via_stream( + &mut self, + fd: Resource, + offset: types::Filesize, + ) -> FsResult> { + // Trap if fd lookup fails: + let f = self.table.get(&fd)?.file()?; + + if !f.perms.contains(FilePerms::READ) { + Err(types::ErrorCode::BadDescriptor)?; + } + + // Create a stream view for it. + let reader: DynInputStream = Box::new(FileInputStream::new(f, offset)); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table.push(reader)?; + + Ok(index) + } + + fn write_via_stream( + &mut self, + fd: Resource, + offset: types::Filesize, + ) -> FsResult> { + // Trap if fd lookup fails: + let f = self.table.get(&fd)?.file()?; + + if !f.perms.contains(FilePerms::WRITE) { + Err(types::ErrorCode::BadDescriptor)?; + } + + // Create a stream view for it. + let writer = FileOutputStream::write_at(f, offset); + let writer: DynOutputStream = Box::new(writer); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table.push(writer)?; + + Ok(index) + } + + fn append_via_stream( + &mut self, + fd: Resource, + ) -> FsResult> { + // Trap if fd lookup fails: + let f = self.table.get(&fd)?.file()?; + + if !f.perms.contains(FilePerms::WRITE) { + Err(types::ErrorCode::BadDescriptor)?; + } + + // Create a stream view for it. + let appender = FileOutputStream::append(f); + let appender: DynOutputStream = Box::new(appender); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table.push(appender)?; + + Ok(index) + } + + async fn is_same_object( + &mut self, + a: Resource, + b: Resource, + ) -> anyhow::Result { + let descriptor_a = self.table.get(&a)?; + let descriptor_b = self.table.get(&b)?; + descriptor_a.is_same_object(descriptor_b).await + } + async fn metadata_hash( + &mut self, + fd: Resource, + ) -> FsResult { + let fd = self.table.get(&fd)?; + let meta = fd.metadata_hash().await?; + Ok(meta.into()) + } + async fn metadata_hash_at( + &mut self, + fd: Resource, + path_flags: types::PathFlags, + path: String, + ) -> FsResult { + let d = self.table.get(&fd)?.dir()?; + let meta = d.metadata_hash_at(path_flags.into(), path).await?; + Ok(meta.into()) + } +} + +impl HostDirectoryEntryStream for WasiFilesystemCtxView<'_> { + async fn read_directory_entry( + &mut self, + stream: Resource, + ) -> FsResult> { + let readdir = self.table.get(&stream)?; + readdir.next() + } + + fn drop(&mut self, stream: Resource) -> anyhow::Result<()> { + self.table.delete(stream)?; + Ok(()) + } +} + +impl From for system_interface::fs::Advice { + fn from(advice: types::Advice) -> Self { + match advice { + types::Advice::Normal => Self::Normal, + types::Advice::Sequential => Self::Sequential, + types::Advice::Random => Self::Random, + types::Advice::WillNeed => Self::WillNeed, + types::Advice::DontNeed => Self::DontNeed, + types::Advice::NoReuse => Self::NoReuse, + } + } +} + +impl From for crate::filesystem::OpenFlags { + fn from(flags: types::OpenFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::OpenFlags::CREATE) { + out |= Self::CREATE; + } + if flags.contains(types::OpenFlags::DIRECTORY) { + out |= Self::DIRECTORY; + } + if flags.contains(types::OpenFlags::EXCLUSIVE) { + out |= Self::EXCLUSIVE; + } + if flags.contains(types::OpenFlags::TRUNCATE) { + out |= Self::TRUNCATE; + } + out + } +} + +impl From for crate::filesystem::PathFlags { + fn from(flags: types::PathFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::PathFlags::SYMLINK_FOLLOW) { + out |= Self::SYMLINK_FOLLOW; + } + out + } +} + +impl From for types::DescriptorFlags { + fn from(flags: crate::filesystem::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(crate::filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if flags.contains(crate::filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if flags.contains(crate::filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} + +impl From for crate::filesystem::DescriptorFlags { + fn from(flags: types::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::DescriptorFlags::READ) { + out |= Self::READ; + } + if flags.contains(types::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if flags.contains(types::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if flags.contains(types::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if flags.contains(types::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if flags.contains(types::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} + +impl From for types::MetadataHashValue { + fn from( + crate::filesystem::MetadataHashValue { lower, upper }: crate::filesystem::MetadataHashValue, + ) -> Self { + Self { lower, upper } + } +} + +impl From for types::DescriptorStat { + fn from( + crate::filesystem::DescriptorStat { + type_, + link_count, + size, + data_access_timestamp, + data_modification_timestamp, + status_change_timestamp, + }: crate::filesystem::DescriptorStat, + ) -> Self { + Self { + type_: type_.into(), + link_count, + size, + data_access_timestamp: data_access_timestamp.map(Into::into), + data_modification_timestamp: data_modification_timestamp.map(Into::into), + status_change_timestamp: status_change_timestamp.map(Into::into), + } + } +} + +impl From for types::DescriptorType { + fn from(ty: crate::filesystem::DescriptorType) -> Self { + match ty { + crate::filesystem::DescriptorType::Unknown => Self::Unknown, + crate::filesystem::DescriptorType::BlockDevice => Self::BlockDevice, + crate::filesystem::DescriptorType::CharacterDevice => Self::CharacterDevice, + crate::filesystem::DescriptorType::Directory => Self::Directory, + crate::filesystem::DescriptorType::SymbolicLink => Self::SymbolicLink, + crate::filesystem::DescriptorType::RegularFile => Self::RegularFile, + } + } +} + +#[cfg(unix)] +fn from_raw_os_error(err: Option) -> Option { + use rustix::io::Errno as RustixErrno; + if err.is_none() { + return None; + } + Some(match RustixErrno::from_raw_os_error(err.unwrap()) { + RustixErrno::PIPE => ErrorCode::Pipe, + RustixErrno::PERM => ErrorCode::NotPermitted, + RustixErrno::NOENT => ErrorCode::NoEntry, + RustixErrno::NOMEM => ErrorCode::InsufficientMemory, + RustixErrno::IO => ErrorCode::Io, + RustixErrno::BADF => ErrorCode::BadDescriptor, + RustixErrno::BUSY => ErrorCode::Busy, + RustixErrno::ACCESS => ErrorCode::Access, + RustixErrno::NOTDIR => ErrorCode::NotDirectory, + RustixErrno::ISDIR => ErrorCode::IsDirectory, + RustixErrno::INVAL => ErrorCode::Invalid, + RustixErrno::EXIST => ErrorCode::Exist, + RustixErrno::FBIG => ErrorCode::FileTooLarge, + RustixErrno::NOSPC => ErrorCode::InsufficientSpace, + RustixErrno::SPIPE => ErrorCode::InvalidSeek, + RustixErrno::MLINK => ErrorCode::TooManyLinks, + RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong, + RustixErrno::NOTEMPTY => ErrorCode::NotEmpty, + RustixErrno::LOOP => ErrorCode::Loop, + RustixErrno::OVERFLOW => ErrorCode::Overflow, + RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence, + RustixErrno::NOTSUP => ErrorCode::Unsupported, + RustixErrno::ALREADY => ErrorCode::Already, + RustixErrno::INPROGRESS => ErrorCode::InProgress, + RustixErrno::INTR => ErrorCode::Interrupted, + + #[allow( + unreachable_patterns, + reason = "on some platforms, these have the same value as other errno values" + )] + RustixErrno::OPNOTSUPP => ErrorCode::Unsupported, + + _ => return None, + }) +} +#[cfg(windows)] +fn from_raw_os_error(raw_os_error: Option) -> Option { + use windows_sys::Win32::Foundation; + Some(match raw_os_error.map(|code| code as u32) { + Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry, + Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access, + Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access, + Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted, + Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor, + Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry, + Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory, + Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty, + Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy, + Some(Foundation::ERROR_BUSY) => ErrorCode::Busy, + Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported, + Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe, + Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong, + Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid, + Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid, + Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory, + Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist, + Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop, + Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory, + _ => return None, + }) +} + +impl From for ErrorCode { + fn from(err: std::io::Error) -> ErrorCode { + ErrorCode::from(&err) + } +} + +impl<'a> From<&'a std::io::Error> for ErrorCode { + fn from(err: &'a std::io::Error) -> ErrorCode { + match from_raw_os_error(err.raw_os_error()) { + Some(errno) => errno, + None => { + tracing::debug!("unknown raw os error: {err}"); + match err.kind() { + std::io::ErrorKind::NotFound => ErrorCode::NoEntry, + std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted, + std::io::ErrorKind::AlreadyExists => ErrorCode::Exist, + std::io::ErrorKind::InvalidInput => ErrorCode::Invalid, + _ => ErrorCode::Io, + } + } + } + } +} + +impl From for ErrorCode { + fn from(err: cap_rand::Error) -> ErrorCode { + // I picked Error::Io as a 'reasonable default', FIXME dan is this ok? + from_raw_os_error(err.raw_os_error()).unwrap_or(ErrorCode::Io) + } +} + +impl From for ErrorCode { + fn from(_err: std::num::TryFromIntError) -> ErrorCode { + ErrorCode::Overflow + } +} + +fn descriptortype_from(ft: cap_std::fs::FileType) -> types::DescriptorType { + use cap_fs_ext::FileTypeExt; + use types::DescriptorType; + if ft.is_dir() { + DescriptorType::Directory + } else if ft.is_symlink() { + DescriptorType::SymbolicLink + } else if ft.is_block_device() { + DescriptorType::BlockDevice + } else if ft.is_char_device() { + DescriptorType::CharacterDevice + } else if ft.is_file() { + DescriptorType::RegularFile + } else { + DescriptorType::Unknown + } +} + +fn systemtime_from(t: wall_clock::Datetime) -> Result { + std::time::SystemTime::UNIX_EPOCH + .checked_add(core::time::Duration::new(t.seconds, t.nanoseconds)) + .ok_or(ErrorCode::Overflow) +} + +fn systemtimespec_from( + t: types::NewTimestamp, +) -> Result, ErrorCode> { + use fs_set_times::SystemTimeSpec; + match t { + types::NewTimestamp::NoChange => Ok(None), + types::NewTimestamp::Now => Ok(Some(SystemTimeSpec::SymbolicNow)), + types::NewTimestamp::Timestamp(st) => { + let st = systemtime_from(st)?; + Ok(Some(SystemTimeSpec::Absolute(st))) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use wasmtime::component::ResourceTable; + + #[test] + fn table_readdir_works() { + let mut table = ResourceTable::new(); + let ix = table + .push(ReaddirIterator::new(std::iter::empty())) + .unwrap(); + let _ = table.get(&ix).unwrap(); + table.delete(ix).unwrap(); + } +} diff --git a/crates/wasi/src/p2/host/filesystem/sync.rs b/crates/wasi/src/p2/host/filesystem/sync.rs new file mode 100644 index 00000000..f20a986b --- /dev/null +++ b/crates/wasi/src/p2/host/filesystem/sync.rs @@ -0,0 +1,517 @@ +use crate::filesystem::WasiFilesystemCtxView; +use crate::p2::bindings::filesystem::types as async_filesystem; +use crate::p2::bindings::sync::filesystem::types as sync_filesystem; +use crate::p2::bindings::sync::io::streams; +use crate::p2::{FsError, FsResult}; +use crate::runtime::in_tokio; +use wasmtime::component::Resource; + +impl sync_filesystem::Host for WasiFilesystemCtxView<'_> { + fn convert_error_code(&mut self, err: FsError) -> anyhow::Result { + Ok(async_filesystem::Host::convert_error_code(self, err)?.into()) + } + + fn filesystem_error_code( + &mut self, + err: Resource, + ) -> anyhow::Result> { + Ok(async_filesystem::Host::filesystem_error_code(self, err)?.map(|e| e.into())) + } +} + +impl sync_filesystem::HostDescriptor for WasiFilesystemCtxView<'_> { + fn advise( + &mut self, + fd: Resource, + offset: sync_filesystem::Filesize, + len: sync_filesystem::Filesize, + advice: sync_filesystem::Advice, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::advise(self, fd, offset, len, advice.into()).await + }) + } + + fn sync_data(&mut self, fd: Resource) -> FsResult<()> { + in_tokio(async { async_filesystem::HostDescriptor::sync_data(self, fd).await }) + } + + fn get_flags( + &mut self, + fd: Resource, + ) -> FsResult { + Ok(in_tokio(async { async_filesystem::HostDescriptor::get_flags(self, fd).await })?.into()) + } + + fn get_type( + &mut self, + fd: Resource, + ) -> FsResult { + Ok(in_tokio(async { async_filesystem::HostDescriptor::get_type(self, fd).await })?.into()) + } + + fn set_size( + &mut self, + fd: Resource, + size: sync_filesystem::Filesize, + ) -> FsResult<()> { + in_tokio(async { async_filesystem::HostDescriptor::set_size(self, fd, size).await }) + } + + fn set_times( + &mut self, + fd: Resource, + atim: sync_filesystem::NewTimestamp, + mtim: sync_filesystem::NewTimestamp, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::set_times(self, fd, atim.into(), mtim.into()).await + }) + } + + fn read( + &mut self, + fd: Resource, + len: sync_filesystem::Filesize, + offset: sync_filesystem::Filesize, + ) -> FsResult<(Vec, bool)> { + in_tokio(async { async_filesystem::HostDescriptor::read(self, fd, len, offset).await }) + } + + fn write( + &mut self, + fd: Resource, + buf: Vec, + offset: sync_filesystem::Filesize, + ) -> FsResult { + in_tokio(async { async_filesystem::HostDescriptor::write(self, fd, buf, offset).await }) + } + + fn read_directory( + &mut self, + fd: Resource, + ) -> FsResult> { + in_tokio(async { async_filesystem::HostDescriptor::read_directory(self, fd).await }) + } + + fn sync(&mut self, fd: Resource) -> FsResult<()> { + in_tokio(async { async_filesystem::HostDescriptor::sync(self, fd).await }) + } + + fn create_directory_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::create_directory_at(self, fd, path).await + }) + } + + fn stat( + &mut self, + fd: Resource, + ) -> FsResult { + Ok(in_tokio(async { async_filesystem::HostDescriptor::stat(self, fd).await })?.into()) + } + + fn stat_at( + &mut self, + fd: Resource, + path_flags: sync_filesystem::PathFlags, + path: String, + ) -> FsResult { + Ok(in_tokio(async { + async_filesystem::HostDescriptor::stat_at(self, fd, path_flags.into(), path).await + })? + .into()) + } + + fn set_times_at( + &mut self, + fd: Resource, + path_flags: sync_filesystem::PathFlags, + path: String, + atim: sync_filesystem::NewTimestamp, + mtim: sync_filesystem::NewTimestamp, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::set_times_at( + self, + fd, + path_flags.into(), + path, + atim.into(), + mtim.into(), + ) + .await + }) + } + + fn link_at( + &mut self, + fd: Resource, + // TODO delete the path flags from this function + old_path_flags: sync_filesystem::PathFlags, + old_path: String, + new_descriptor: Resource, + new_path: String, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::link_at( + self, + fd, + old_path_flags.into(), + old_path, + new_descriptor, + new_path, + ) + .await + }) + } + + fn open_at( + &mut self, + fd: Resource, + path_flags: sync_filesystem::PathFlags, + path: String, + oflags: sync_filesystem::OpenFlags, + flags: sync_filesystem::DescriptorFlags, + ) -> FsResult> { + in_tokio(async { + async_filesystem::HostDescriptor::open_at( + self, + fd, + path_flags.into(), + path, + oflags.into(), + flags.into(), + ) + .await + }) + } + + fn drop(&mut self, fd: Resource) -> anyhow::Result<()> { + async_filesystem::HostDescriptor::drop(self, fd) + } + + fn readlink_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult { + in_tokio(async { async_filesystem::HostDescriptor::readlink_at(self, fd, path).await }) + } + + fn remove_directory_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::remove_directory_at(self, fd, path).await + }) + } + + fn rename_at( + &mut self, + fd: Resource, + old_path: String, + new_fd: Resource, + new_path: String, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::rename_at(self, fd, old_path, new_fd, new_path).await + }) + } + + fn symlink_at( + &mut self, + fd: Resource, + src_path: String, + dest_path: String, + ) -> FsResult<()> { + in_tokio(async { + async_filesystem::HostDescriptor::symlink_at(self, fd, src_path, dest_path).await + }) + } + + fn unlink_file_at( + &mut self, + fd: Resource, + path: String, + ) -> FsResult<()> { + in_tokio(async { async_filesystem::HostDescriptor::unlink_file_at(self, fd, path).await }) + } + + fn read_via_stream( + &mut self, + fd: Resource, + offset: sync_filesystem::Filesize, + ) -> FsResult> { + Ok(async_filesystem::HostDescriptor::read_via_stream( + self, fd, offset, + )?) + } + + fn write_via_stream( + &mut self, + fd: Resource, + offset: sync_filesystem::Filesize, + ) -> FsResult> { + Ok(async_filesystem::HostDescriptor::write_via_stream( + self, fd, offset, + )?) + } + + fn append_via_stream( + &mut self, + fd: Resource, + ) -> FsResult> { + Ok(async_filesystem::HostDescriptor::append_via_stream( + self, fd, + )?) + } + + fn is_same_object( + &mut self, + a: Resource, + b: Resource, + ) -> anyhow::Result { + in_tokio(async { async_filesystem::HostDescriptor::is_same_object(self, a, b).await }) + } + fn metadata_hash( + &mut self, + fd: Resource, + ) -> FsResult { + Ok( + in_tokio(async { async_filesystem::HostDescriptor::metadata_hash(self, fd).await })? + .into(), + ) + } + fn metadata_hash_at( + &mut self, + fd: Resource, + path_flags: sync_filesystem::PathFlags, + path: String, + ) -> FsResult { + Ok(in_tokio(async { + async_filesystem::HostDescriptor::metadata_hash_at(self, fd, path_flags.into(), path) + .await + })? + .into()) + } +} + +impl sync_filesystem::HostDirectoryEntryStream for WasiFilesystemCtxView<'_> { + fn read_directory_entry( + &mut self, + stream: Resource, + ) -> FsResult> { + Ok(in_tokio(async { + async_filesystem::HostDirectoryEntryStream::read_directory_entry(self, stream).await + })? + .map(|e| e.into())) + } + + fn drop( + &mut self, + stream: Resource, + ) -> anyhow::Result<()> { + async_filesystem::HostDirectoryEntryStream::drop(self, stream) + } +} + +impl From for sync_filesystem::ErrorCode { + fn from(other: async_filesystem::ErrorCode) -> Self { + use async_filesystem::ErrorCode; + match other { + ErrorCode::Access => Self::Access, + ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::Already => Self::Already, + ErrorCode::BadDescriptor => Self::BadDescriptor, + ErrorCode::Busy => Self::Busy, + ErrorCode::Deadlock => Self::Deadlock, + ErrorCode::Quota => Self::Quota, + ErrorCode::Exist => Self::Exist, + ErrorCode::FileTooLarge => Self::FileTooLarge, + ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, + ErrorCode::InProgress => Self::InProgress, + ErrorCode::Interrupted => Self::Interrupted, + ErrorCode::Invalid => Self::Invalid, + ErrorCode::Io => Self::Io, + ErrorCode::IsDirectory => Self::IsDirectory, + ErrorCode::Loop => Self::Loop, + ErrorCode::TooManyLinks => Self::TooManyLinks, + ErrorCode::MessageSize => Self::MessageSize, + ErrorCode::NameTooLong => Self::NameTooLong, + ErrorCode::NoDevice => Self::NoDevice, + ErrorCode::NoEntry => Self::NoEntry, + ErrorCode::NoLock => Self::NoLock, + ErrorCode::InsufficientMemory => Self::InsufficientMemory, + ErrorCode::InsufficientSpace => Self::InsufficientSpace, + ErrorCode::NotDirectory => Self::NotDirectory, + ErrorCode::NotEmpty => Self::NotEmpty, + ErrorCode::NotRecoverable => Self::NotRecoverable, + ErrorCode::Unsupported => Self::Unsupported, + ErrorCode::NoTty => Self::NoTty, + ErrorCode::NoSuchDevice => Self::NoSuchDevice, + ErrorCode::Overflow => Self::Overflow, + ErrorCode::NotPermitted => Self::NotPermitted, + ErrorCode::Pipe => Self::Pipe, + ErrorCode::ReadOnly => Self::ReadOnly, + ErrorCode::InvalidSeek => Self::InvalidSeek, + ErrorCode::TextFileBusy => Self::TextFileBusy, + ErrorCode::CrossDevice => Self::CrossDevice, + } + } +} + +impl From for async_filesystem::Advice { + fn from(other: sync_filesystem::Advice) -> Self { + use sync_filesystem::Advice; + match other { + Advice::Normal => Self::Normal, + Advice::Sequential => Self::Sequential, + Advice::Random => Self::Random, + Advice::WillNeed => Self::WillNeed, + Advice::DontNeed => Self::DontNeed, + Advice::NoReuse => Self::NoReuse, + } + } +} + +impl From for sync_filesystem::DescriptorFlags { + fn from(other: async_filesystem::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if other.contains(async_filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if other.contains(async_filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if other.contains(async_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if other.contains(async_filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if other.contains(async_filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if other.contains(async_filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} + +impl From for sync_filesystem::DescriptorType { + fn from(other: async_filesystem::DescriptorType) -> Self { + use async_filesystem::DescriptorType; + match other { + DescriptorType::RegularFile => Self::RegularFile, + DescriptorType::Directory => Self::Directory, + DescriptorType::BlockDevice => Self::BlockDevice, + DescriptorType::CharacterDevice => Self::CharacterDevice, + DescriptorType::Fifo => Self::Fifo, + DescriptorType::Socket => Self::Socket, + DescriptorType::SymbolicLink => Self::SymbolicLink, + DescriptorType::Unknown => Self::Unknown, + } + } +} + +impl From for sync_filesystem::DirectoryEntry { + fn from(other: async_filesystem::DirectoryEntry) -> Self { + Self { + type_: other.type_.into(), + name: other.name, + } + } +} + +impl From for sync_filesystem::DescriptorStat { + fn from(other: async_filesystem::DescriptorStat) -> Self { + Self { + type_: other.type_.into(), + link_count: other.link_count, + size: other.size, + data_access_timestamp: other.data_access_timestamp, + data_modification_timestamp: other.data_modification_timestamp, + status_change_timestamp: other.status_change_timestamp, + } + } +} + +impl From for async_filesystem::PathFlags { + fn from(other: sync_filesystem::PathFlags) -> Self { + let mut out = Self::empty(); + if other.contains(sync_filesystem::PathFlags::SYMLINK_FOLLOW) { + out |= Self::SYMLINK_FOLLOW; + } + out + } +} + +impl From for async_filesystem::NewTimestamp { + fn from(other: sync_filesystem::NewTimestamp) -> Self { + use sync_filesystem::NewTimestamp; + match other { + NewTimestamp::NoChange => Self::NoChange, + NewTimestamp::Now => Self::Now, + NewTimestamp::Timestamp(datetime) => Self::Timestamp(datetime), + } + } +} + +impl From for async_filesystem::OpenFlags { + fn from(other: sync_filesystem::OpenFlags) -> Self { + let mut out = Self::empty(); + if other.contains(sync_filesystem::OpenFlags::CREATE) { + out |= Self::CREATE; + } + if other.contains(sync_filesystem::OpenFlags::DIRECTORY) { + out |= Self::DIRECTORY; + } + if other.contains(sync_filesystem::OpenFlags::EXCLUSIVE) { + out |= Self::EXCLUSIVE; + } + if other.contains(sync_filesystem::OpenFlags::TRUNCATE) { + out |= Self::TRUNCATE; + } + out + } +} +impl From for async_filesystem::DescriptorFlags { + fn from(other: sync_filesystem::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if other.contains(sync_filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if other.contains(sync_filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if other.contains(sync_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} +impl From for sync_filesystem::MetadataHashValue { + fn from(other: async_filesystem::MetadataHashValue) -> Self { + Self { + lower: other.lower, + upper: other.upper, + } + } +} diff --git a/crates/wasi/src/p2/host/instance_network.rs b/crates/wasi/src/p2/host/instance_network.rs new file mode 100644 index 00000000..9029181d --- /dev/null +++ b/crates/wasi/src/p2/host/instance_network.rs @@ -0,0 +1,15 @@ +use crate::p2::bindings::sockets::instance_network; +use crate::p2::network::Network; +use crate::sockets::WasiSocketsCtxView; +use wasmtime::component::Resource; + +impl instance_network::Host for WasiSocketsCtxView<'_> { + fn instance_network(&mut self) -> Result, anyhow::Error> { + let network = Network { + socket_addr_check: self.ctx.socket_addr_check.clone(), + allow_ip_name_lookup: self.ctx.allowed_network_uses.ip_name_lookup, + }; + let network = self.table.push(network)?; + Ok(network) + } +} diff --git a/crates/wasi/src/p2/host/io.rs b/crates/wasi/src/p2/host/io.rs new file mode 100644 index 00000000..1baa67e0 --- /dev/null +++ b/crates/wasi/src/p2/host/io.rs @@ -0,0 +1,125 @@ +use crate::p2::{ + StreamError, StreamResult, + bindings::sync::io::poll::Pollable, + bindings::sync::io::streams::{self, InputStream, OutputStream}, +}; +use crate::runtime::in_tokio; +use wasmtime::component::{Resource, ResourceTable}; +use wasmtime_wasi_io::bindings::wasi::io::streams::{ + self as async_streams, Host as AsyncHost, HostInputStream as AsyncHostInputStream, + HostOutputStream as AsyncHostOutputStream, +}; + +impl From for streams::StreamError { + fn from(other: async_streams::StreamError) -> Self { + match other { + async_streams::StreamError::LastOperationFailed(e) => Self::LastOperationFailed(e), + async_streams::StreamError::Closed => Self::Closed, + } + } +} + +impl streams::Host for ResourceTable { + fn convert_stream_error(&mut self, err: StreamError) -> anyhow::Result { + Ok(AsyncHost::convert_stream_error(self, err)?.into()) + } +} + +impl streams::HostOutputStream for ResourceTable { + fn drop(&mut self, stream: Resource) -> anyhow::Result<()> { + in_tokio(async { AsyncHostOutputStream::drop(self, stream).await }) + } + + fn check_write(&mut self, stream: Resource) -> StreamResult { + Ok(AsyncHostOutputStream::check_write(self, stream)?) + } + + fn write(&mut self, stream: Resource, bytes: Vec) -> StreamResult<()> { + Ok(AsyncHostOutputStream::write(self, stream, bytes)?) + } + + fn blocking_write_and_flush( + &mut self, + stream: Resource, + bytes: Vec, + ) -> StreamResult<()> { + in_tokio(async { + AsyncHostOutputStream::blocking_write_and_flush(self, stream, bytes).await + }) + } + + fn blocking_write_zeroes_and_flush( + &mut self, + stream: Resource, + len: u64, + ) -> StreamResult<()> { + in_tokio(async { + AsyncHostOutputStream::blocking_write_zeroes_and_flush(self, stream, len).await + }) + } + + fn subscribe(&mut self, stream: Resource) -> anyhow::Result> { + Ok(AsyncHostOutputStream::subscribe(self, stream)?) + } + + fn write_zeroes(&mut self, stream: Resource, len: u64) -> StreamResult<()> { + Ok(AsyncHostOutputStream::write_zeroes(self, stream, len)?) + } + + fn flush(&mut self, stream: Resource) -> StreamResult<()> { + Ok(AsyncHostOutputStream::flush( + self, + Resource::new_borrow(stream.rep()), + )?) + } + + fn blocking_flush(&mut self, stream: Resource) -> StreamResult<()> { + in_tokio(async { + AsyncHostOutputStream::blocking_flush(self, Resource::new_borrow(stream.rep())).await + }) + } + + fn splice( + &mut self, + dst: Resource, + src: Resource, + len: u64, + ) -> StreamResult { + AsyncHostOutputStream::splice(self, dst, src, len) + } + + fn blocking_splice( + &mut self, + dst: Resource, + src: Resource, + len: u64, + ) -> StreamResult { + in_tokio(async { AsyncHostOutputStream::blocking_splice(self, dst, src, len).await }) + } +} + +impl streams::HostInputStream for ResourceTable { + fn drop(&mut self, stream: Resource) -> anyhow::Result<()> { + in_tokio(async { AsyncHostInputStream::drop(self, stream).await }) + } + + fn read(&mut self, stream: Resource, len: u64) -> StreamResult> { + AsyncHostInputStream::read(self, stream, len) + } + + fn blocking_read(&mut self, stream: Resource, len: u64) -> StreamResult> { + in_tokio(async { AsyncHostInputStream::blocking_read(self, stream, len).await }) + } + + fn skip(&mut self, stream: Resource, len: u64) -> StreamResult { + AsyncHostInputStream::skip(self, stream, len) + } + + fn blocking_skip(&mut self, stream: Resource, len: u64) -> StreamResult { + in_tokio(async { AsyncHostInputStream::blocking_skip(self, stream, len).await }) + } + + fn subscribe(&mut self, stream: Resource) -> anyhow::Result> { + AsyncHostInputStream::subscribe(self, stream) + } +} diff --git a/crates/wasi/src/p2/host/mod.rs b/crates/wasi/src/p2/host/mod.rs new file mode 100644 index 00000000..7aa4a874 --- /dev/null +++ b/crates/wasi/src/p2/host/mod.rs @@ -0,0 +1,12 @@ +mod clocks; +mod env; +mod exit; +pub(crate) mod filesystem; +mod instance_network; +mod io; +pub(crate) mod network; +mod random; +mod tcp; +mod tcp_create_socket; +mod udp; +mod udp_create_socket; diff --git a/crates/wasi/src/p2/host/network.rs b/crates/wasi/src/p2/host/network.rs new file mode 100644 index 00000000..e0fd31f4 --- /dev/null +++ b/crates/wasi/src/p2/host/network.rs @@ -0,0 +1,237 @@ +use crate::p2::SocketError; +use crate::p2::bindings::sockets::network::{ + self, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, + Ipv6SocketAddress, +}; +use crate::sockets::WasiSocketsCtxView; +use crate::sockets::util::{from_ipv4_addr, from_ipv6_addr, to_ipv4_addr, to_ipv6_addr}; +use anyhow::Error; +use rustix::io::Errno; +use std::io; +use wasmtime::component::Resource; + +impl network::Host for WasiSocketsCtxView<'_> { + fn convert_error_code(&mut self, error: SocketError) -> anyhow::Result { + error.downcast() + } + + fn network_error_code(&mut self, err: Resource) -> anyhow::Result> { + let err = self.table.get(&err)?; + + if let Some(err) = err.downcast_ref::() { + return Ok(Some(ErrorCode::from(err))); + } + + Ok(None) + } +} + +impl crate::p2::bindings::sockets::network::HostNetwork for WasiSocketsCtxView<'_> { + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + self.table.delete(this)?; + + Ok(()) + } +} + +impl From for ErrorCode { + fn from(value: io::Error) -> Self { + (&value).into() + } +} + +impl From<&io::Error> for ErrorCode { + fn from(value: &io::Error) -> Self { + // Attempt the more detailed native error code first: + if let Some(errno) = Errno::from_io_error(value) { + return errno.into(); + } + + match value.kind() { + std::io::ErrorKind::AddrInUse => ErrorCode::AddressInUse, + std::io::ErrorKind::AddrNotAvailable => ErrorCode::AddressNotBindable, + std::io::ErrorKind::ConnectionAborted => ErrorCode::ConnectionAborted, + std::io::ErrorKind::ConnectionRefused => ErrorCode::ConnectionRefused, + std::io::ErrorKind::ConnectionReset => ErrorCode::ConnectionReset, + std::io::ErrorKind::Interrupted => ErrorCode::WouldBlock, + std::io::ErrorKind::InvalidInput => ErrorCode::InvalidArgument, + std::io::ErrorKind::NotConnected => ErrorCode::InvalidState, + std::io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory, + std::io::ErrorKind::PermissionDenied => ErrorCode::AccessDenied, + std::io::ErrorKind::TimedOut => ErrorCode::Timeout, + std::io::ErrorKind::Unsupported => ErrorCode::NotSupported, + std::io::ErrorKind::WouldBlock => ErrorCode::WouldBlock, + + _ => { + tracing::debug!("unknown I/O error: {value}"); + ErrorCode::Unknown + } + } + } +} + +impl From for ErrorCode { + fn from(value: Errno) -> Self { + (&value).into() + } +} + +impl From<&Errno> for ErrorCode { + fn from(value: &Errno) -> Self { + match *value { + Errno::WOULDBLOCK => ErrorCode::WouldBlock, + #[allow( + unreachable_patterns, + reason = "EWOULDBLOCK and EAGAIN can have the same value" + )] + Errno::AGAIN => ErrorCode::WouldBlock, + Errno::INTR => ErrorCode::WouldBlock, + #[cfg(not(windows))] + Errno::PERM => ErrorCode::AccessDenied, + Errno::ACCESS => ErrorCode::AccessDenied, + Errno::ADDRINUSE => ErrorCode::AddressInUse, + Errno::ADDRNOTAVAIL => ErrorCode::AddressNotBindable, + Errno::ALREADY => ErrorCode::ConcurrencyConflict, + Errno::TIMEDOUT => ErrorCode::Timeout, + Errno::CONNREFUSED => ErrorCode::ConnectionRefused, + Errno::CONNRESET => ErrorCode::ConnectionReset, + Errno::CONNABORTED => ErrorCode::ConnectionAborted, + Errno::INVAL => ErrorCode::InvalidArgument, + Errno::HOSTUNREACH => ErrorCode::RemoteUnreachable, + Errno::HOSTDOWN => ErrorCode::RemoteUnreachable, + Errno::NETDOWN => ErrorCode::RemoteUnreachable, + Errno::NETUNREACH => ErrorCode::RemoteUnreachable, + #[cfg(target_os = "linux")] + Errno::NONET => ErrorCode::RemoteUnreachable, + Errno::ISCONN => ErrorCode::InvalidState, + Errno::NOTCONN => ErrorCode::InvalidState, + Errno::DESTADDRREQ => ErrorCode::InvalidState, + #[cfg(not(windows))] + Errno::NFILE => ErrorCode::NewSocketLimit, + Errno::MFILE => ErrorCode::NewSocketLimit, + Errno::MSGSIZE => ErrorCode::DatagramTooLarge, + #[cfg(not(windows))] + Errno::NOMEM => ErrorCode::OutOfMemory, + Errno::NOBUFS => ErrorCode::OutOfMemory, + Errno::OPNOTSUPP => ErrorCode::NotSupported, + Errno::NOPROTOOPT => ErrorCode::NotSupported, + Errno::PFNOSUPPORT => ErrorCode::NotSupported, + Errno::PROTONOSUPPORT => ErrorCode::NotSupported, + Errno::PROTOTYPE => ErrorCode::NotSupported, + Errno::SOCKTNOSUPPORT => ErrorCode::NotSupported, + Errno::AFNOSUPPORT => ErrorCode::NotSupported, + + // FYI, EINPROGRESS should have already been handled by connect. + _ => { + tracing::debug!("unknown I/O error: {value}"); + ErrorCode::Unknown + } + } + } +} + +impl From for IpAddress { + fn from(addr: std::net::IpAddr) -> Self { + match addr { + std::net::IpAddr::V4(v4) => Self::Ipv4(from_ipv4_addr(v4)), + std::net::IpAddr::V6(v6) => Self::Ipv6(from_ipv6_addr(v6)), + } + } +} + +impl From for std::net::SocketAddr { + fn from(addr: IpSocketAddress) -> Self { + match addr { + IpSocketAddress::Ipv4(ipv4) => Self::V4(ipv4.into()), + IpSocketAddress::Ipv6(ipv6) => Self::V6(ipv6.into()), + } + } +} + +impl From for IpSocketAddress { + fn from(addr: std::net::SocketAddr) -> Self { + match addr { + std::net::SocketAddr::V4(v4) => Self::Ipv4(v4.into()), + std::net::SocketAddr::V6(v6) => Self::Ipv6(v6.into()), + } + } +} + +impl From for std::net::SocketAddrV4 { + fn from(addr: Ipv4SocketAddress) -> Self { + Self::new(to_ipv4_addr(addr.address), addr.port) + } +} + +impl From for Ipv4SocketAddress { + fn from(addr: std::net::SocketAddrV4) -> Self { + Self { + address: from_ipv4_addr(*addr.ip()), + port: addr.port(), + } + } +} + +impl From for std::net::SocketAddrV6 { + fn from(addr: Ipv6SocketAddress) -> Self { + Self::new( + to_ipv6_addr(addr.address), + addr.port, + addr.flow_info, + addr.scope_id, + ) + } +} + +impl From for Ipv6SocketAddress { + fn from(addr: std::net::SocketAddrV6) -> Self { + Self { + address: from_ipv6_addr(*addr.ip()), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + } + } +} + +impl std::net::ToSocketAddrs for IpSocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddr::from(*self).to_socket_addrs() + } +} + +impl std::net::ToSocketAddrs for Ipv4SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddrV4::from(*self).to_socket_addrs() + } +} + +impl std::net::ToSocketAddrs for Ipv6SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddrV6::from(*self).to_socket_addrs() + } +} + +impl From for cap_net_ext::AddressFamily { + fn from(family: IpAddressFamily) -> Self { + match family { + IpAddressFamily::Ipv4 => cap_net_ext::AddressFamily::Ipv4, + IpAddressFamily::Ipv6 => cap_net_ext::AddressFamily::Ipv6, + } + } +} + +impl From for IpAddressFamily { + fn from(family: cap_net_ext::AddressFamily) -> Self { + match family { + cap_net_ext::AddressFamily::Ipv4 => IpAddressFamily::Ipv4, + cap_net_ext::AddressFamily::Ipv6 => IpAddressFamily::Ipv6, + } + } +} diff --git a/crates/wasi/src/p2/host/random.rs b/crates/wasi/src/p2/host/random.rs new file mode 100644 index 00000000..aac8457a --- /dev/null +++ b/crates/wasi/src/p2/host/random.rs @@ -0,0 +1,36 @@ +use crate::p2::bindings::random::{insecure, insecure_seed, random}; +use crate::random::WasiRandomCtx; +use cap_rand::{Rng, distributions::Standard}; + +impl random::Host for WasiRandomCtx { + fn get_random_bytes(&mut self, len: u64) -> anyhow::Result> { + Ok((&mut self.random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_random_u64(&mut self) -> anyhow::Result { + Ok(self.random.sample(Standard)) + } +} + +impl insecure::Host for WasiRandomCtx { + fn get_insecure_random_bytes(&mut self, len: u64) -> anyhow::Result> { + Ok((&mut self.insecure_random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_insecure_random_u64(&mut self) -> anyhow::Result { + Ok(self.insecure_random.sample(Standard)) + } +} + +impl insecure_seed::Host for WasiRandomCtx { + fn insecure_seed(&mut self) -> anyhow::Result<(u64, u64)> { + let seed: u128 = self.insecure_random_seed; + Ok((seed as u64, (seed >> 64) as u64)) + } +} diff --git a/crates/wasi/src/p2/host/tcp.rs b/crates/wasi/src/p2/host/tcp.rs new file mode 100644 index 00000000..ca027233 --- /dev/null +++ b/crates/wasi/src/p2/host/tcp.rs @@ -0,0 +1,549 @@ +use crate::p2::bindings::{ + sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::tcp::{self, ShutdownType}, +}; +use crate::p2::{Pollable, SocketResult}; +use crate::sockets::{SocketAddrUse, TcpSocket, WasiSocketsCtxView}; +use std::net::SocketAddr; +use wasmtime::component::Resource; +use wasmtime_wasi_io::{ + poll::DynPollable, + streams::{DynInputStream, DynOutputStream}, +}; + +impl tcp::Host for WasiSocketsCtxView<'_> {} + +impl crate::p2::host::tcp::tcp::HostTcpSocket for WasiSocketsCtxView<'_> { + async fn start_bind( + &mut self, + this: Resource, + network: Resource, + local_address: IpSocketAddress, + ) -> SocketResult<()> { + let network = self.table.get(&network)?; + let local_address: SocketAddr = local_address.into(); + + // Ensure that we're allowed to connect to this address. + network + .check_socket_addr(local_address, SocketAddrUse::TcpBind) + .await?; + + let mut loopback = self.ctx.loopback.lock().unwrap(); + // Bind to the address. + self.table + .get_mut(&this)? + .start_bind(local_address, &mut loopback)?; + + Ok(()) + } + + fn finish_bind(&mut self, this: Resource) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.finish_bind()?; + Ok(()) + } + + async fn start_connect( + &mut self, + this: Resource, + network: Resource, + remote_address: IpSocketAddress, + ) -> SocketResult<()> { + let network = self.table.get(&network)?; + let remote_address: SocketAddr = remote_address.into(); + + // Ensure that we're allowed to connect to this address. + network + .check_socket_addr(remote_address, SocketAddrUse::TcpConnect) + .await?; + + // Start connection + let socket = self.table.get_mut(&this)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + let future = socket + .start_connect(&remote_address, &mut loopback)? + .connect(remote_address); + socket.set_pending_connect(future)?; + + Ok(()) + } + + fn finish_connect( + &mut self, + this: Resource, + ) -> SocketResult<(Resource, Resource)> { + let socket = self.table.get_mut(&this)?; + + let result = socket + .take_pending_connect()? + .ok_or(ErrorCode::WouldBlock)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.finish_connect(result, &mut loopback)?; + let (input, output) = socket.p2_streams()?; + let input = self.table.push_child(input, &this)?; + let output = self.table.push_child(output, &this)?; + Ok((input, output)) + } + + fn start_listen(&mut self, this: Resource) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.start_listen(&mut loopback)?; + Ok(()) + } + + fn finish_listen(&mut self, this: Resource) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.finish_listen()?; + Ok(()) + } + + fn accept( + &mut self, + this: Resource, + ) -> SocketResult<( + Resource, + Resource, + Resource, + )> { + let socket = self.table.get_mut(&this)?; + + let mut tcp_socket = socket.accept()?.ok_or(ErrorCode::WouldBlock)?; + let (input, output) = tcp_socket.p2_streams()?; + + let tcp_socket = self.table.push(tcp_socket)?; + let input_stream = self.table.push_child(input, &tcp_socket)?; + let output_stream = self.table.push_child(output, &tcp_socket)?; + + Ok((tcp_socket, input_stream, output_stream)) + } + + fn local_address(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.local_address()?.into()) + } + + fn remote_address(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.remote_address()?.into()) + } + + fn is_listening(&mut self, this: Resource) -> Result { + let socket = self.table.get(&this)?; + + Ok(socket.is_listening()) + } + + fn address_family( + &mut self, + this: Resource, + ) -> Result { + let socket = self.table.get(&this)?; + Ok(socket.address_family().into()) + } + + fn set_listen_backlog_size( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_listen_backlog_size(value)?; + Ok(()) + } + + fn keep_alive_enabled(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.keep_alive_enabled()?) + } + + fn set_keep_alive_enabled( + &mut self, + this: Resource, + value: bool, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_keep_alive_enabled(value)?; + Ok(()) + } + + fn keep_alive_idle_time(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.keep_alive_idle_time()?) + } + + fn set_keep_alive_idle_time( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_keep_alive_idle_time(value)?; + Ok(()) + } + + fn keep_alive_interval(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.keep_alive_interval()?) + } + + fn set_keep_alive_interval( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_keep_alive_interval(value)?; + Ok(()) + } + + fn keep_alive_count(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.keep_alive_count()?) + } + + fn set_keep_alive_count(&mut self, this: Resource, value: u32) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_keep_alive_count(value)?; + Ok(()) + } + + fn hop_limit(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.hop_limit()?) + } + + fn set_hop_limit(&mut self, this: Resource, value: u8) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_hop_limit(value)?; + Ok(()) + } + + fn receive_buffer_size(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.receive_buffer_size()?) + } + + fn set_receive_buffer_size( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_receive_buffer_size(value)?; + Ok(()) + } + + fn send_buffer_size(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.send_buffer_size()?) + } + + fn set_send_buffer_size(&mut self, this: Resource, value: u64) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_send_buffer_size(value)?; + Ok(()) + } + + fn subscribe(&mut self, this: Resource) -> anyhow::Result> { + wasmtime_wasi_io::poll::subscribe(self.table, this) + } + + async fn shutdown( + &mut self, + this: Resource, + shutdown_type: ShutdownType, + ) -> SocketResult<()> { + let socket = self.table.get(&this)?; + + match socket { + TcpSocket::Network(socket) => { + let how = match shutdown_type { + ShutdownType::Receive => std::net::Shutdown::Read, + ShutdownType::Send => std::net::Shutdown::Write, + ShutdownType::Both => std::net::Shutdown::Both, + }; + let state = socket.p2_streaming_state()?; + state.shutdown(how)?; + Ok(()) + } + TcpSocket::Loopback(socket) => { + use crate::sockets::loopback::TcpState; + match &socket.state { + TcpState::P2Streaming { tx, rx, .. } => { + match shutdown_type { + ShutdownType::Receive => { + if let Some(mut rx) = rx.lock().await.take() { + rx.close(); + } + } + ShutdownType::Send => { + tx.lock().unwrap().take(); + } + ShutdownType::Both => { + tx.lock().unwrap().take(); + if let Some(mut rx) = rx.lock().await.take() { + rx.close(); + } + } + } + Ok(()) + } + //#[cfg(feature = "p3")] + //TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState.into()), + } + } + TcpSocket::Unspecified { .. } => Err(ErrorCode::InvalidState.into()), + } + } + + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let socket = self.table.delete(this)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.drop(&mut loopback)?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Pollable for TcpSocket { + async fn ready(&mut self) { + ::ready(self).await; + } +} + +pub mod sync { + use crate::p2::{ + SocketError, + bindings::{ + sockets::{ + network::Network, + tcp::{self as async_tcp, HostTcpSocket as AsyncHostTcpSocket}, + }, + sync::sockets::tcp::{ + self, Duration, HostTcpSocket, InputStream, IpAddressFamily, IpSocketAddress, + OutputStream, Pollable, ShutdownType, TcpSocket, + }, + }, + }; + use crate::runtime::in_tokio; + use crate::sockets::WasiSocketsCtxView; + use wasmtime::component::Resource; + + impl tcp::Host for WasiSocketsCtxView<'_> {} + + impl HostTcpSocket for WasiSocketsCtxView<'_> { + fn start_bind( + &mut self, + self_: Resource, + network: Resource, + local_address: IpSocketAddress, + ) -> Result<(), SocketError> { + in_tokio(async { + AsyncHostTcpSocket::start_bind(self, self_, network, local_address).await + }) + } + + fn finish_bind(&mut self, self_: Resource) -> Result<(), SocketError> { + AsyncHostTcpSocket::finish_bind(self, self_) + } + + fn start_connect( + &mut self, + self_: Resource, + network: Resource, + remote_address: IpSocketAddress, + ) -> Result<(), SocketError> { + in_tokio(async { + AsyncHostTcpSocket::start_connect(self, self_, network, remote_address).await + }) + } + + fn finish_connect( + &mut self, + self_: Resource, + ) -> Result<(Resource, Resource), SocketError> { + AsyncHostTcpSocket::finish_connect(self, self_) + } + + fn start_listen(&mut self, self_: Resource) -> Result<(), SocketError> { + AsyncHostTcpSocket::start_listen(self, self_) + } + + fn finish_listen(&mut self, self_: Resource) -> Result<(), SocketError> { + AsyncHostTcpSocket::finish_listen(self, self_) + } + + fn accept( + &mut self, + self_: Resource, + ) -> Result< + ( + Resource, + Resource, + Resource, + ), + SocketError, + > { + AsyncHostTcpSocket::accept(self, self_) + } + + fn local_address( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostTcpSocket::local_address(self, self_) + } + + fn remote_address( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostTcpSocket::remote_address(self, self_) + } + + fn is_listening(&mut self, self_: Resource) -> wasmtime::Result { + AsyncHostTcpSocket::is_listening(self, self_) + } + + fn address_family( + &mut self, + self_: Resource, + ) -> wasmtime::Result { + AsyncHostTcpSocket::address_family(self, self_) + } + + fn set_listen_backlog_size( + &mut self, + self_: Resource, + value: u64, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_listen_backlog_size(self, self_, value) + } + + fn keep_alive_enabled(&mut self, self_: Resource) -> Result { + AsyncHostTcpSocket::keep_alive_enabled(self, self_) + } + + fn set_keep_alive_enabled( + &mut self, + self_: Resource, + value: bool, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_keep_alive_enabled(self, self_, value) + } + + fn keep_alive_idle_time( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostTcpSocket::keep_alive_idle_time(self, self_) + } + + fn set_keep_alive_idle_time( + &mut self, + self_: Resource, + value: Duration, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_keep_alive_idle_time(self, self_, value) + } + + fn keep_alive_interval( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostTcpSocket::keep_alive_interval(self, self_) + } + + fn set_keep_alive_interval( + &mut self, + self_: Resource, + value: Duration, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_keep_alive_interval(self, self_, value) + } + + fn keep_alive_count(&mut self, self_: Resource) -> Result { + AsyncHostTcpSocket::keep_alive_count(self, self_) + } + + fn set_keep_alive_count( + &mut self, + self_: Resource, + value: u32, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_keep_alive_count(self, self_, value) + } + + fn hop_limit(&mut self, self_: Resource) -> Result { + AsyncHostTcpSocket::hop_limit(self, self_) + } + + fn set_hop_limit( + &mut self, + self_: Resource, + value: u8, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_hop_limit(self, self_, value) + } + + fn receive_buffer_size(&mut self, self_: Resource) -> Result { + AsyncHostTcpSocket::receive_buffer_size(self, self_) + } + + fn set_receive_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_receive_buffer_size(self, self_, value) + } + + fn send_buffer_size(&mut self, self_: Resource) -> Result { + AsyncHostTcpSocket::send_buffer_size(self, self_) + } + + fn set_send_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> Result<(), SocketError> { + AsyncHostTcpSocket::set_send_buffer_size(self, self_, value) + } + + fn subscribe( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + AsyncHostTcpSocket::subscribe(self, self_) + } + + fn shutdown( + &mut self, + self_: Resource, + shutdown_type: ShutdownType, + ) -> Result<(), SocketError> { + in_tokio(async { + AsyncHostTcpSocket::shutdown(self, self_, shutdown_type.into()).await + }) + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + AsyncHostTcpSocket::drop(self, rep) + } + } + + impl From for async_tcp::ShutdownType { + fn from(other: ShutdownType) -> Self { + match other { + ShutdownType::Receive => async_tcp::ShutdownType::Receive, + ShutdownType::Send => async_tcp::ShutdownType::Send, + ShutdownType::Both => async_tcp::ShutdownType::Both, + } + } + } +} diff --git a/crates/wasi/src/p2/host/tcp_create_socket.rs b/crates/wasi/src/p2/host/tcp_create_socket.rs new file mode 100644 index 00000000..10fcc75e --- /dev/null +++ b/crates/wasi/src/p2/host/tcp_create_socket.rs @@ -0,0 +1,24 @@ +use crate::p2::SocketResult; +use crate::p2::bindings::{sockets::network::IpAddressFamily, sockets::tcp_create_socket}; +use crate::sockets::{SocketAddressFamily, TcpSocket, WasiSocketsCtxView}; +use wasmtime::component::Resource; + +impl tcp_create_socket::Host for WasiSocketsCtxView<'_> { + fn create_tcp_socket( + &mut self, + address_family: IpAddressFamily, + ) -> SocketResult> { + let socket = TcpSocket::new(self.ctx, address_family.into())?; + let socket = self.table.push(socket)?; + Ok(socket) + } +} + +impl From for SocketAddressFamily { + fn from(family: IpAddressFamily) -> SocketAddressFamily { + match family { + IpAddressFamily::Ipv4 => Self::Ipv4, + IpAddressFamily::Ipv6 => Self::Ipv6, + } + } +} diff --git a/crates/wasi/src/p2/host/udp.rs b/crates/wasi/src/p2/host/udp.rs new file mode 100644 index 00000000..9e237737 --- /dev/null +++ b/crates/wasi/src/p2/host/udp.rs @@ -0,0 +1,825 @@ +use crate::p2::bindings::sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}; +use crate::p2::bindings::sockets::udp; +use crate::p2::udp::{IncomingDatagramStream, OutgoingDatagramStream, SendState}; +use crate::p2::{Pollable, SocketError, SocketResult}; +use crate::sockets::util::{is_valid_address_family, is_valid_remote_address}; +use crate::sockets::{ + MAX_UDP_DATAGRAM_SIZE, SocketAddrUse, SocketAddressFamily, UdpSocket, WasiSocketsCtxView, +}; +use anyhow::anyhow; +use async_trait::async_trait; +use std::net::SocketAddr; +use tokio::io::Interest; +use wasmtime::component::Resource; +use wasmtime_wasi_io::poll::DynPollable; + +impl udp::Host for WasiSocketsCtxView<'_> {} + +impl udp::HostUdpSocket for WasiSocketsCtxView<'_> { + async fn start_bind( + &mut self, + this: Resource, + network: Resource, + local_address: IpSocketAddress, + ) -> SocketResult<()> { + let local_address = SocketAddr::from(local_address); + let check = self.table.get(&network)?.socket_addr_check.clone(); + check.check(local_address, SocketAddrUse::UdpBind).await?; + + let socket = self.table.get_mut(&this)?; + + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.bind(local_address, &mut loopback)?; + socket.set_socket_addr_check(Some(check)); + + Ok(()) + } + + fn finish_bind(&mut self, this: Resource) -> SocketResult<()> { + self.table.get_mut(&this)?.finish_bind()?; + Ok(()) + } + + async fn stream( + &mut self, + this: Resource, + remote_address: Option, + ) -> SocketResult<( + Resource, + Resource, + )> { + let has_active_streams = self + .table + .iter_children(&this)? + .any(|c| c.is::() || c.is::()); + + if has_active_streams { + return Err(SocketError::trap(anyhow!("UDP streams not dropped yet"))); + } + + let socket = self.table.get_mut(&this)?; + let remote_address = remote_address.map(SocketAddr::from); + + if !socket.is_bound() { + return Err(ErrorCode::InvalidState.into()); + } + + // We disconnect & (re)connect in two distinct steps for two reasons: + // - To leave our socket instance in a consistent state in case the + // connect fails. + // - When reconnecting to a different address, Linux sometimes fails + // if there isn't a disconnect in between. + + // Step #1: Disconnect + if socket.is_connected() { + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.disconnect(&mut loopback)?; + } + + // Step #2: (Re)connect + if let Some(connect_addr) = remote_address { + let Some(check) = socket.socket_addr_check() else { + return Err(ErrorCode::InvalidState.into()); + }; + check.check(connect_addr, SocketAddrUse::UdpConnect).await?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.connect(connect_addr, &mut loopback)?; + } + let is_loopback = remote_address.map(|addr| addr.ip().to_canonical().is_loopback()); + + let (incoming_stream, outgoing_stream) = match (socket, is_loopback) { + (UdpSocket::Network(socket), ..) + | (UdpSocket::Unspecified { net: socket, .. }, Some(false)) => ( + IncomingDatagramStream::Network(crate::p2::udp::NetworkIncomingDatagramStream { + inner: socket.socket().clone(), + remote_address, + }), + OutgoingDatagramStream::Network(crate::p2::udp::NetworkOutgoingDatagramStream { + inner: socket.socket().clone(), + remote_address, + family: socket.address_family(), + send_state: SendState::Idle, + socket_addr_check: socket.socket_addr_check().cloned(), + }), + ), + (UdpSocket::Loopback(socket), ..) + | (UdpSocket::Unspecified { lo: socket, .. }, Some(true)) => { + let (rx, tx) = socket.p2_udp_streams(remote_address)?; + ( + IncomingDatagramStream::Loopback(rx), + OutgoingDatagramStream::Loopback(tx), + ) + } + (UdpSocket::Unspecified { lo, net }, None) => { + let (lo_rx, lo_tx) = lo.p2_udp_streams(remote_address)?; + ( + IncomingDatagramStream::Unspecified { + lo: lo_rx, + net: crate::p2::udp::NetworkIncomingDatagramStream { + inner: net.socket().clone(), + remote_address, + }, + }, + OutgoingDatagramStream::Unspecified { + lo: lo_tx, + net: crate::p2::udp::NetworkOutgoingDatagramStream { + inner: net.socket().clone(), + remote_address, + family: net.address_family(), + send_state: SendState::Idle, + socket_addr_check: net.socket_addr_check().cloned(), + }, + }, + ) + } + }; + Ok(( + self.table.push_child(incoming_stream, &this)?, + self.table.push_child(outgoing_stream, &this)?, + )) + } + + fn local_address(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.local_address()?.into()) + } + + fn remote_address(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.remote_address()?.into()) + } + + fn address_family( + &mut self, + this: Resource, + ) -> Result { + let socket = self.table.get(&this)?; + Ok(socket.address_family().into()) + } + + fn unicast_hop_limit(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.unicast_hop_limit()?) + } + + fn set_unicast_hop_limit( + &mut self, + this: Resource, + value: u8, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_unicast_hop_limit(value)?; + Ok(()) + } + + fn receive_buffer_size(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.receive_buffer_size()?) + } + + fn set_receive_buffer_size( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_receive_buffer_size(value)?; + Ok(()) + } + + fn send_buffer_size(&mut self, this: Resource) -> SocketResult { + let socket = self.table.get(&this)?; + Ok(socket.send_buffer_size()?) + } + + fn set_send_buffer_size(&mut self, this: Resource, value: u64) -> SocketResult<()> { + let socket = self.table.get_mut(&this)?; + socket.set_send_buffer_size(value)?; + Ok(()) + } + + fn subscribe(&mut self, this: Resource) -> anyhow::Result> { + wasmtime_wasi_io::poll::subscribe(self.table, this) + } + + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let socket = self.table.delete(this)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.drop(&mut loopback)?; + + Ok(()) + } +} + +#[async_trait] +impl Pollable for UdpSocket { + async fn ready(&mut self) { + // None of the socket-level operations block natively + } +} + +impl udp::HostIncomingDatagramStream for WasiSocketsCtxView<'_> { + fn receive( + &mut self, + this: Resource, + max_results: u64, + ) -> SocketResult> { + // Returns Ok(None) when the message was dropped. + fn recv_one( + stream: &crate::p2::udp::NetworkIncomingDatagramStream, + ) -> SocketResult> { + let mut buf = [0; MAX_UDP_DATAGRAM_SIZE]; + let (size, received_addr) = stream.inner.try_recv_from(&mut buf)?; + debug_assert!(size <= buf.len()); + + match stream.remote_address { + Some(connected_addr) if connected_addr != received_addr => { + // Normally, this should have already been checked for us by the OS. + return Ok(None); + } + _ => {} + } + + Ok(Some(udp::IncomingDatagram { + data: buf[..size].into(), + remote_address: received_addr.into(), + })) + } + + let max_results: usize = max_results.try_into().unwrap_or(usize::MAX); + + if max_results == 0 { + return Ok(vec![]); + } + + let mut datagrams = vec![]; + + let stream = self.table.get_mut(&this)?; + let stream = match stream { + IncomingDatagramStream::Network(stream) => stream, + IncomingDatagramStream::Loopback(stream) => { + stream.recv(&mut datagrams, max_results)?; + return Ok(datagrams); + } + IncomingDatagramStream::Unspecified { net, lo } => { + lo.recv(&mut datagrams, max_results)?; + net + } + }; + + while datagrams.len() < max_results { + match recv_one(stream) { + Ok(Some(datagram)) => { + datagrams.push(datagram); + } + Ok(None) => { + // Message was dropped + } + Err(_) if datagrams.len() > 0 => { + return Ok(datagrams); + } + Err(e) if matches!(e.downcast_ref(), Some(ErrorCode::WouldBlock)) => { + return Ok(datagrams); + } + Err(e) => { + return Err(e); + } + } + } + + Ok(datagrams) + } + + fn subscribe( + &mut self, + this: Resource, + ) -> anyhow::Result> { + wasmtime_wasi_io::poll::subscribe(self.table, this) + } + + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let dropped = self.table.delete(this)?; + drop(dropped); + + Ok(()) + } +} + +#[async_trait] +impl Pollable for IncomingDatagramStream { + async fn ready(&mut self) { + let stream = match self { + IncomingDatagramStream::Network(stream) => stream, + IncomingDatagramStream::Loopback(stream) => { + let mut rx = stream.rx.lock().await; + stream.received = rx.recv().await; + return; + } + IncomingDatagramStream::Unspecified { net, lo } => { + let mut lo_rx = lo.rx.lock().await; + let mut net_ready = core::pin::pin!(async { + // FIXME: Add `Interest::ERROR` when we update to tokio 1.32. + net.inner + .ready(Interest::READABLE) + .await + .expect("failed to await UDP socket readiness"); + }); + core::future::poll_fn(|cx| match lo_rx.poll_recv(cx) { + core::task::Poll::Ready(received) => { + lo.received = received; + core::task::Poll::Ready(()) + } + core::task::Poll::Pending => net_ready.as_mut().poll(cx), + }) + .await; + return; + } + }; + // FIXME: Add `Interest::ERROR` when we update to tokio 1.32. + stream + .inner + .ready(Interest::READABLE) + .await + .expect("failed to await UDP socket readiness"); + } +} + +impl udp::HostOutgoingDatagramStream for WasiSocketsCtxView<'_> { + fn check_send(&mut self, this: Resource) -> SocketResult { + let stream = self.table.get_mut(&this)?; + let is_unspecified = matches!(stream, &mut OutgoingDatagramStream::Unspecified { .. }); + let stream = match stream { + OutgoingDatagramStream::Network(stream) => stream, + OutgoingDatagramStream::Loopback(lo) => return Ok(lo.check_send().into()), + OutgoingDatagramStream::Unspecified { net, lo } => { + if !lo.check_send() { + return Ok(0); + } + net + } + }; + + let permit = match stream.send_state { + SendState::Idle => { + const PERMIT: usize = 16; + stream.send_state = SendState::Permitted(PERMIT); + PERMIT + } + SendState::Permitted(n) => n, + SendState::Waiting => 0, + }; + if permit > 1 && is_unspecified { + return Ok(1); + } + Ok(permit.try_into().unwrap()) + } + + async fn send( + &mut self, + this: Resource, + datagrams: Vec, + ) -> SocketResult { + async fn prepare_one( + remote_address: Option, + family: SocketAddressFamily, + socket_addr_check: Option<&crate::sockets::SocketAddrCheck>, + datagram: &udp::OutgoingDatagram, + ) -> SocketResult { + if datagram.data.len() > MAX_UDP_DATAGRAM_SIZE { + return Err(ErrorCode::DatagramTooLarge.into()); + } + + let provided_addr = datagram.remote_address.map(SocketAddr::from); + let addr = match (remote_address, provided_addr) { + (None, Some(addr)) => { + let Some(check) = socket_addr_check else { + return Err(ErrorCode::InvalidState.into()); + }; + check + .check(addr, SocketAddrUse::UdpOutgoingDatagram) + .await?; + addr + } + (Some(addr), None) => addr, + (Some(connected_addr), Some(provided_addr)) if connected_addr == provided_addr => { + connected_addr + } + _ => return Err(ErrorCode::InvalidArgument.into()), + }; + + if !is_valid_remote_address(addr) || !is_valid_address_family(addr.ip(), family) { + return Err(ErrorCode::InvalidArgument.into()); + } + Ok(addr) + } + + fn send_one_net( + stream: &crate::p2::udp::NetworkOutgoingDatagramStream, + datagram: &udp::OutgoingDatagram, + addr: SocketAddr, + ) -> SocketResult<()> { + if stream.remote_address == Some(addr) { + stream.inner.try_send(&datagram.data)?; + } else { + stream.inner.try_send_to(&datagram.data, addr)?; + } + + Ok(()) + } + + async fn send_one_lo( + stream: &mut crate::p2::udp::LoopbackOutgoingDatagramStream, + datagram: udp::OutgoingDatagram, + loopback: &std::sync::Mutex, + ) -> SocketResult<()> { + let addr = prepare_one( + stream.remote_address, + stream.family, + stream.socket_addr_check.as_ref(), + &datagram, + ) + .await?; + let Some(mut permit) = stream.permit.take() else { + return Err(SocketError::trap(anyhow::anyhow!( + "unpermitted: must call check-send first" + ))); + }; + if permit.num_permits() < datagram.data.len() { + return Err(ErrorCode::DatagramTooLarge.into()); + } + let required = core::num::NonZeroUsize::new(datagram.data.len()) + .unwrap_or(core::num::NonZeroUsize::MIN); + let Some(unused) = permit.num_permits().checked_sub(required.into()) else { + return Err(ErrorCode::DatagramTooLarge.into()); + }; + if unused > 0 { + _ = permit.split(unused); + } + let mut loopback = loopback.lock().unwrap(); + if let Some(tx) = loopback.connect_udp(&stream.local_address, &addr)? { + _ = tx.send(( + crate::sockets::loopback::UdpDatagram { + source_address: stream.local_address, + data: datagram.data, + }, + permit, + )); + } + Ok(()) + } + + let stream = self.table.get_mut(&this)?; + let (mut lo, stream) = match stream { + OutgoingDatagramStream::Network(stream) => (None, stream), + OutgoingDatagramStream::Loopback(stream) => { + let mut datagrams = datagrams.into_iter(); + let datagram = match core::array::from_fn(|_| datagrams.next()) { + [None, None] => return Ok(0), + [Some(datagram), None] => datagram, + _ => { + return Err(SocketError::trap(anyhow::anyhow!( + "unpermitted: argument exceeds permitted size" + ))); + } + }; + send_one_lo(stream, datagram, &self.ctx.loopback).await?; + return Ok(1); + } + OutgoingDatagramStream::Unspecified { lo, net } => { + if datagrams.len() > 1 { + return Err(SocketError::trap(anyhow::anyhow!( + "unpermitted: argument exceeds permitted size" + ))); + } + (Some(lo), net) + } + }; + + match stream.send_state { + SendState::Permitted(n) if n >= datagrams.len() => { + stream.send_state = SendState::Idle; + } + SendState::Permitted(_) => { + return Err(SocketError::trap(anyhow::anyhow!( + "unpermitted: argument exceeds permitted size" + ))); + } + SendState::Idle | SendState::Waiting => { + return Err(SocketError::trap(anyhow::anyhow!( + "unpermitted: must call check-send first" + ))); + } + } + + if datagrams.is_empty() { + return Ok(0); + } + + let mut count = 0; + + for datagram in datagrams { + let addr = prepare_one( + stream.remote_address, + stream.family, + stream.socket_addr_check.as_ref(), + &datagram, + ) + .await?; + + if addr.ip().to_canonical().is_loopback() { + if let Some(stream) = lo.as_mut() { + send_one_lo(stream, datagram, &self.ctx.loopback).await?; + count += 1; + continue; + } + } + match send_one_net(stream, &datagram, addr) { + Ok(_) => count += 1, + Err(_) if count > 0 => { + // WIT: "If at least one datagram has been sent successfully, this function never returns an error." + return Ok(count); + } + Err(e) if matches!(e.downcast_ref(), Some(ErrorCode::WouldBlock)) => { + stream.send_state = SendState::Waiting; + return Ok(count); + } + Err(e) => { + return Err(e); + } + } + } + + Ok(count) + } + + fn subscribe( + &mut self, + this: Resource, + ) -> anyhow::Result> { + wasmtime_wasi_io::poll::subscribe(self.table, this) + } + + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let dropped = self.table.delete(this)?; + drop(dropped); + + Ok(()) + } +} + +#[async_trait] +impl Pollable for OutgoingDatagramStream { + async fn ready(&mut self) { + let stream = match self { + OutgoingDatagramStream::Network(stream) => stream, + OutgoingDatagramStream::Loopback(stream) => { + if stream.permit.is_none() { + _ = stream.permits.acquire().await; + } + return; + } + OutgoingDatagramStream::Unspecified { net, lo } => { + if lo.permit.is_none() { + _ = lo.permits.acquire().await; + } + net + } + }; + match stream.send_state { + SendState::Idle | SendState::Permitted(_) => {} + SendState::Waiting => { + // FIXME: Add `Interest::ERROR` when we update to tokio 1.32. + stream + .inner + .ready(Interest::WRITABLE) + .await + .expect("failed to await UDP socket readiness"); + stream.send_state = SendState::Idle; + } + } + } +} + +impl From for IpAddressFamily { + fn from(family: SocketAddressFamily) -> IpAddressFamily { + match family { + SocketAddressFamily::Ipv4 => IpAddressFamily::Ipv4, + SocketAddressFamily::Ipv6 => IpAddressFamily::Ipv6, + } + } +} + +pub mod sync { + use wasmtime::component::Resource; + + use crate::p2::{ + SocketError, + bindings::{ + sockets::{ + network::Network, + udp::{ + self as async_udp, + HostIncomingDatagramStream as AsyncHostIncomingDatagramStream, + HostOutgoingDatagramStream as AsyncHostOutgoingDatagramStream, + HostUdpSocket as AsyncHostUdpSocket, IncomingDatagramStream, + OutgoingDatagramStream, + }, + }, + sync::sockets::udp::{ + self, HostIncomingDatagramStream, HostOutgoingDatagramStream, HostUdpSocket, + IncomingDatagram, IpAddressFamily, IpSocketAddress, OutgoingDatagram, Pollable, + UdpSocket, + }, + }, + }; + use crate::runtime::in_tokio; + use crate::sockets::WasiSocketsCtxView; + + impl udp::Host for WasiSocketsCtxView<'_> {} + + impl HostUdpSocket for WasiSocketsCtxView<'_> { + fn start_bind( + &mut self, + self_: Resource, + network: Resource, + local_address: IpSocketAddress, + ) -> Result<(), SocketError> { + in_tokio(async { + AsyncHostUdpSocket::start_bind(self, self_, network, local_address).await + }) + } + + fn finish_bind(&mut self, self_: Resource) -> Result<(), SocketError> { + AsyncHostUdpSocket::finish_bind(self, self_) + } + + fn stream( + &mut self, + self_: Resource, + remote_address: Option, + ) -> Result< + ( + Resource, + Resource, + ), + SocketError, + > { + in_tokio(async { AsyncHostUdpSocket::stream(self, self_, remote_address).await }) + } + + fn local_address( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostUdpSocket::local_address(self, self_) + } + + fn remote_address( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostUdpSocket::remote_address(self, self_) + } + + fn address_family( + &mut self, + self_: Resource, + ) -> wasmtime::Result { + AsyncHostUdpSocket::address_family(self, self_) + } + + fn unicast_hop_limit(&mut self, self_: Resource) -> Result { + AsyncHostUdpSocket::unicast_hop_limit(self, self_) + } + + fn set_unicast_hop_limit( + &mut self, + self_: Resource, + value: u8, + ) -> Result<(), SocketError> { + AsyncHostUdpSocket::set_unicast_hop_limit(self, self_, value) + } + + fn receive_buffer_size(&mut self, self_: Resource) -> Result { + AsyncHostUdpSocket::receive_buffer_size(self, self_) + } + + fn set_receive_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> Result<(), SocketError> { + AsyncHostUdpSocket::set_receive_buffer_size(self, self_, value) + } + + fn send_buffer_size(&mut self, self_: Resource) -> Result { + AsyncHostUdpSocket::send_buffer_size(self, self_) + } + + fn set_send_buffer_size( + &mut self, + self_: Resource, + value: u64, + ) -> Result<(), SocketError> { + AsyncHostUdpSocket::set_send_buffer_size(self, self_, value) + } + + fn subscribe( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + AsyncHostUdpSocket::subscribe(self, self_) + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + AsyncHostUdpSocket::drop(self, rep) + } + } + + impl HostIncomingDatagramStream for WasiSocketsCtxView<'_> { + fn receive( + &mut self, + self_: Resource, + max_results: u64, + ) -> Result, SocketError> { + Ok( + AsyncHostIncomingDatagramStream::receive(self, self_, max_results)? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + fn subscribe( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + AsyncHostIncomingDatagramStream::subscribe(self, self_) + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + AsyncHostIncomingDatagramStream::drop(self, rep) + } + } + + impl From for IncomingDatagram { + fn from(other: async_udp::IncomingDatagram) -> Self { + let async_udp::IncomingDatagram { + data, + remote_address, + } = other; + Self { + data, + remote_address, + } + } + } + + impl HostOutgoingDatagramStream for WasiSocketsCtxView<'_> { + fn check_send( + &mut self, + self_: Resource, + ) -> Result { + AsyncHostOutgoingDatagramStream::check_send(self, self_) + } + + fn send( + &mut self, + self_: Resource, + datagrams: Vec, + ) -> Result { + let datagrams = datagrams.into_iter().map(Into::into).collect(); + in_tokio(async { AsyncHostOutgoingDatagramStream::send(self, self_, datagrams).await }) + } + + fn subscribe( + &mut self, + self_: Resource, + ) -> wasmtime::Result> { + AsyncHostOutgoingDatagramStream::subscribe(self, self_) + } + + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + AsyncHostOutgoingDatagramStream::drop(self, rep) + } + } + + impl From for async_udp::OutgoingDatagram { + fn from(other: OutgoingDatagram) -> Self { + let OutgoingDatagram { + data, + remote_address, + } = other; + Self { + data, + remote_address, + } + } + } +} diff --git a/crates/wasi/src/p2/host/udp_create_socket.rs b/crates/wasi/src/p2/host/udp_create_socket.rs new file mode 100644 index 00000000..ac05dc51 --- /dev/null +++ b/crates/wasi/src/p2/host/udp_create_socket.rs @@ -0,0 +1,16 @@ +use crate::p2::SocketResult; +use crate::p2::bindings::{sockets::network::IpAddressFamily, sockets::udp_create_socket}; +use crate::sockets::UdpSocket; +use crate::sockets::WasiSocketsCtxView; +use wasmtime::component::Resource; + +impl udp_create_socket::Host for WasiSocketsCtxView<'_> { + fn create_udp_socket( + &mut self, + address_family: IpAddressFamily, + ) -> SocketResult> { + let socket = UdpSocket::new(self.ctx, address_family.into())?; + let socket = self.table.push(socket)?; + Ok(socket) + } +} diff --git a/crates/wasi/src/p2/ip_name_lookup.rs b/crates/wasi/src/p2/ip_name_lookup.rs new file mode 100644 index 00000000..b8cc6398 --- /dev/null +++ b/crates/wasi/src/p2/ip_name_lookup.rs @@ -0,0 +1,105 @@ +use crate::p2::SocketError; +use crate::p2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream}; +use crate::p2::bindings::sockets::network::{ErrorCode, IpAddress, Network}; +use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking}; +use crate::sockets::WasiSocketsCtxView; +use anyhow::Result; +use std::mem; +use std::net::ToSocketAddrs; +use std::pin::Pin; +use std::vec; +use wasmtime::component::Resource; +use wasmtime_wasi_io::poll::{DynPollable, Pollable, subscribe}; + +use crate::sockets::util::{from_ipv4_addr, from_ipv6_addr, parse_host}; + +pub enum ResolveAddressStream { + Waiting(AbortOnDropJoinHandle, SocketError>>), + Done(Result, SocketError>), +} + +impl Host for WasiSocketsCtxView<'_> { + fn resolve_addresses( + &mut self, + network: Resource, + name: String, + ) -> Result, SocketError> { + let network = self.table.get(&network)?; + + let host = parse_host(&name)?; + + if !network.allow_ip_name_lookup { + return Err(ErrorCode::PermanentResolverFailure.into()); + } + + let task = spawn_blocking(move || blocking_resolve(&host)); + let resource = self.table.push(ResolveAddressStream::Waiting(task))?; + Ok(resource) + } +} + +impl HostResolveAddressStream for WasiSocketsCtxView<'_> { + fn resolve_next_address( + &mut self, + resource: Resource, + ) -> Result, SocketError> { + let stream: &mut ResolveAddressStream = self.table.get_mut(&resource)?; + loop { + match stream { + ResolveAddressStream::Waiting(future) => { + match crate::runtime::poll_noop(Pin::new(future)) { + Some(result) => { + *stream = ResolveAddressStream::Done(result.map(|v| v.into_iter())); + } + None => return Err(ErrorCode::WouldBlock.into()), + } + } + ResolveAddressStream::Done(slot @ Err(_)) => { + mem::replace(slot, Ok(Vec::new().into_iter()))?; + unreachable!(); + } + ResolveAddressStream::Done(Ok(iter)) => return Ok(iter.next()), + } + } + } + + fn subscribe( + &mut self, + resource: Resource, + ) -> Result> { + subscribe(self.table, resource) + } + + fn drop(&mut self, resource: Resource) -> Result<()> { + self.table.delete(resource)?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Pollable for ResolveAddressStream { + async fn ready(&mut self) { + if let ResolveAddressStream::Waiting(future) = self { + *self = ResolveAddressStream::Done(future.await.map(|v| v.into_iter())); + } + } +} + +fn blocking_resolve(host: &url::Host) -> Result, SocketError> { + match host { + url::Host::Ipv4(v4addr) => Ok(vec![IpAddress::Ipv4(from_ipv4_addr(*v4addr))]), + url::Host::Ipv6(v6addr) => Ok(vec![IpAddress::Ipv6(from_ipv6_addr(*v6addr))]), + url::Host::Domain(domain) => { + // For now use the standard library to perform actual resolution through + // the usage of the `ToSocketAddrs` trait. This is only + // resolving names, not ports, so force the port to be 0. + let addresses = (domain.as_str(), 0) + .to_socket_addrs() + .map_err(|_| ErrorCode::NameUnresolvable)? // If/when we use `getaddrinfo` directly, map the error properly. + .map(|addr| addr.ip().to_canonical().into()) + .collect(); + + Ok(addresses) + } + } +} diff --git a/crates/wasi/src/p2/mod.rs b/crates/wasi/src/p2/mod.rs new file mode 100644 index 00000000..9aa84b69 --- /dev/null +++ b/crates/wasi/src/p2/mod.rs @@ -0,0 +1,507 @@ +//! # Wasmtime's WASIp2 Implementation +//! +//! +//! This module provides a Wasmtime host implementation of WASI 0.2 (aka WASIp2 +//! aka Preview 2) and WASI 0.1 (aka WASIp1 aka Preview 1). WASI is implemented +//! with the Rust crates [`tokio`] and [`cap-std`] primarily, meaning that +//! operations are implemented in terms of their native platform equivalents by +//! default. +//! +//! # WASIp2 interfaces +//! +//! This module contains implementations of the following interfaces: +//! +//! * [`wasi:cli/environment`] +//! * [`wasi:cli/exit`] +//! * [`wasi:cli/stderr`] +//! * [`wasi:cli/stdin`] +//! * [`wasi:cli/stdout`] +//! * [`wasi:cli/terminal-input`] +//! * [`wasi:cli/terminal-output`] +//! * [`wasi:cli/terminal-stderr`] +//! * [`wasi:cli/terminal-stdin`] +//! * [`wasi:cli/terminal-stdout`] +//! * [`wasi:clocks/monotonic-clock`] +//! * [`wasi:clocks/wall-clock`] +//! * [`wasi:filesystem/preopens`] +//! * [`wasi:filesystem/types`] +//! * [`wasi:random/insecure-seed`] +//! * [`wasi:random/insecure`] +//! * [`wasi:random/random`] +//! * [`wasi:sockets/instance-network`] +//! * [`wasi:sockets/ip-name-lookup`] +//! * [`wasi:sockets/network`] +//! * [`wasi:sockets/tcp-create-socket`] +//! * [`wasi:sockets/tcp`] +//! * [`wasi:sockets/udp-create-socket`] +//! * [`wasi:sockets/udp`] +//! +//! Most traits are implemented for [`WasiCtxView`] trait which provides +//! access to [`WasiCtx`] and [`ResourceTable`], which defines the configuration +//! for WASI and handle state. The [`WasiView`] trait is used to acquire and +//! construct a [`WasiCtxView`]. +//! +//! The [`wasmtime-wasi-io`] crate contains implementations of the +//! following interfaces, and this module reuses those implementations: +//! +//! * [`wasi:io/error`] +//! * [`wasi:io/poll`] +//! * [`wasi:io/streams`] +//! +//! These traits are implemented directly for [`ResourceTable`]. All aspects of +//! `wasmtime-wasi-io` that are used by this module are re-exported. Unless you +//! are implementing other host functionality that needs to interact with the +//! WASI scheduler and don't want to use other functionality provided by +//! `wasmtime-wasi`, you don't need to take a direct dependency on +//! `wasmtime-wasi-io`. +//! +//! # Generated Bindings +//! +//! This module uses [`wasmtime::component::bindgen!`] to generate bindings for +//! all WASI interfaces. Raw bindings are available in the [`bindings`] submodule +//! of this module. Downstream users can either implement these traits themselves +//! or you can use the built-in implementations in this module for +//! `WasiImpl`. +//! +//! # The `WasiView` trait +//! +//! This module's implementation of WASI is done in terms of an implementation of +//! [`WasiView`]. This trait provides a "view" into WASI-related state that is +//! contained within a [`Store`](wasmtime::Store). +//! +//! For all of the generated bindings in this module (Host traits), +//! implementations are provided looking like: +//! +//! ``` +//! # use wash_wasi::WasiCtxView; +//! # trait WasiView {} +//! # mod bindings { pub mod wasi { pub trait Host {} } } +//! impl bindings::wasi::Host for WasiCtxView<'_> { +//! // ... +//! } +//! ``` +//! +//! where the [`WasiCtxView`] type comes from [`WasiView::ctx`] for the type +//! contained within the `Store`. The [`add_to_linker_sync`] and +//! [`add_to_linker_async`] function then require that `T: WasiView` with +//! [`Linker`](wasmtime::component::Linker). +//! +//! To implement the [`WasiView`] trait you will first select a +//! `T` to put in `Store` (typically, by defining your own struct). +//! Somewhere within `T` you'll store: +//! +//! * [`ResourceTable`] - created through default constructors. +//! * [`WasiCtx`] - created through [`WasiCtxBuilder`]. +//! +//! You'll then write an implementation of the [`WasiView`] +//! trait to access those items in your `T`. For example: +//! ``` +//! use wasmtime::component::ResourceTable; +//! use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! +//! struct MyCtx { +//! table: ResourceTable, +//! wasi: WasiCtx, +//! } +//! +//! impl WasiView for MyCtx { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } +//! } +//! } +//! ``` +//! +//! # Async and Sync +//! +//! As of WASI0.2, WASI functions are not blocking from WebAssembly's point of +//! view: a WebAssembly call into these functions returns when they are +//! complete. +//! +//! This module provides an implementation of those functions in the host, +//! where for some functions, it is appropriate to implement them using +//! async Rust and the Tokio executor, so that the host implementation can be +//! nonblocking when Wasmtime's [`Config::async_support`][async] is set. +//! Synchronous wrappers are provided for all async implementations, which +//! creates a private Tokio executor. +//! +//! Users can choose between these modes of implementation using variants +//! of the add_to_linker functions: +//! +//! * For non-async users (the default of `Config`), use [`add_to_linker_sync`]. +//! * For async users, use [`add_to_linker_async`]. +//! +//! Note that bindings are generated once for async and once for sync. Most +//! interfaces do not change, however, so only interfaces with blocking +//! functions have bindings generated twice. Bindings are organized as: +//! +//! * [`bindings`] - default location of all bindings, blocking functions are +//! `async` +//! * [`bindings::sync`] - blocking interfaces have synchronous versions here. +//! +//! # Module-specific traits +//! +//! This module's default implementation of WASI bindings to native primitives +//! for the platform that it is compiled for. For example opening a TCP socket +//! uses the native platform to open a TCP socket (so long as [`WasiCtxBuilder`] +//! allows it). There are a few important traits, however, that are specific to +//! this module. +//! +//! * [`InputStream`] and [`OutputStream`] - these are the host traits +//! behind the WASI `input-stream` and `output-stream` types in the +//! `wasi:io/streams` interface. These enable embedders to build their own +//! custom stream and insert them into a [`ResourceTable`] (as a boxed trait +//! object, see [`DynInputStream`] and [`DynOutputStream`]) to be used from +//! wasm. +//! +//! * [`Pollable`] - this trait enables building arbitrary logic to get hooked +//! into a `pollable` resource from `wasi:io/poll`. A pollable resource is +//! created through the [`subscribe`] function. +//! +//! * [`HostWallClock`](crate::HostWallClock) and [`HostMonotonicClock`](crate::HostMonotonicClock) are used in conjunction with +//! [`WasiCtxBuilder::wall_clock`] and [`WasiCtxBuilder::monotonic_clock`] if +//! the defaults host's clock should not be used. +//! +//! * [`StdinStream`] and [`StdoutStream`] are used to provide custom +//! stdin/stdout streams if they're not inherited (or null, which is the +//! default). +//! +//! These traits enable embedders to customize small portions of WASI interfaces +//! provided while still providing all other interfaces. +//! +//! # Examples +//! +//! Usage of this module is done through a few steps to get everything hooked up: +//! +//! 1. First implement [`WasiView`] for your type which is the +//! `T` in `Store`. +//! 2. Add WASI interfaces to a `wasmtime::component::Linker`. This is either +//! done through top-level functions like [`add_to_linker_sync`] or through +//! individual `add_to_linker` functions in generated bindings throughout +//! this module. +//! 3. Create a [`WasiCtx`] for each `Store` through [`WasiCtxBuilder`]. Each +//! WASI context is "null" or "empty" by default, so items must be explicitly +//! added to get accessed by wasm (such as env vars or program arguments). +//! 4. Use the previous `Linker` to instantiate a `Component` within a +//! `Store`. +//! +//! For examples see each of [`WasiView`], [`WasiCtx`], [`WasiCtxBuilder`], +//! [`add_to_linker_sync`], and [`bindings::Command`]. +//! +//! [`wasmtime::component::bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! [`tokio`]: https://crates.io/crates/tokio +//! [`cap-std`]: https://crates.io/crates/cap-std +//! [`wasmtime-wasi-io`]: https://crates.io/crates/wasmtime-wasi-io +//! [`wasi:cli/environment`]: bindings::cli::environment::Host +//! [`wasi:cli/exit`]: bindings::cli::exit::Host +//! [`wasi:cli/stderr`]: bindings::cli::stderr::Host +//! [`wasi:cli/stdin`]: bindings::cli::stdin::Host +//! [`wasi:cli/stdout`]: bindings::cli::stdout::Host +//! [`wasi:cli/terminal-input`]: bindings::cli::terminal_input::Host +//! [`wasi:cli/terminal-output`]: bindings::cli::terminal_output::Host +//! [`wasi:cli/terminal-stdin`]: bindings::cli::terminal_stdin::Host +//! [`wasi:cli/terminal-stdout`]: bindings::cli::terminal_stdout::Host +//! [`wasi:cli/terminal-stderr`]: bindings::cli::terminal_stderr::Host +//! [`wasi:clocks/monotonic-clock`]: bindings::clocks::monotonic_clock::Host +//! [`wasi:clocks/wall-clock`]: bindings::clocks::wall_clock::Host +//! [`wasi:filesystem/preopens`]: bindings::filesystem::preopens::Host +//! [`wasi:filesystem/types`]: bindings::filesystem::types::Host +//! [`wasi:io/error`]: wasmtime_wasi_io::bindings::wasi::io::error::Host +//! [`wasi:io/poll`]: wasmtime_wasi_io::bindings::wasi::io::poll::Host +//! [`wasi:io/streams`]: wasmtime_wasi_io::bindings::wasi::io::streams::Host +//! [`wasi:random/insecure-seed`]: bindings::random::insecure_seed::Host +//! [`wasi:random/insecure`]: bindings::random::insecure::Host +//! [`wasi:random/random`]: bindings::random::random::Host +//! [`wasi:sockets/instance-network`]: bindings::sockets::instance_network::Host +//! [`wasi:sockets/ip-name-lookup`]: bindings::sockets::ip_name_lookup::Host +//! [`wasi:sockets/network`]: bindings::sockets::network::Host +//! [`wasi:sockets/tcp-create-socket`]: bindings::sockets::tcp_create_socket::Host +//! [`wasi:sockets/tcp`]: bindings::sockets::tcp::Host +//! [`wasi:sockets/udp-create-socket`]: bindings::sockets::udp_create_socket::Host +//! [`wasi:sockets/udp`]: bindings::sockets::udp::Host +//! [async]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support +//! [`ResourceTable`]: wasmtime::component::ResourceTable + +use crate::WasiView; +use crate::cli::{WasiCli, WasiCliView as _}; +use crate::clocks::{WasiClocks, WasiClocksView as _}; +use crate::filesystem::{WasiFilesystem, WasiFilesystemView as _}; +use crate::random::WasiRandom; +use crate::sockets::{WasiSockets, WasiSocketsView as _}; +use wasmtime::component::{HasData, Linker, ResourceTable}; + +pub mod bindings; +pub(crate) mod filesystem; +mod host; +mod ip_name_lookup; +mod network; +pub mod pipe; +mod poll; +mod stdio; +mod tcp; +pub(crate) mod udp; +mod write_stream; + +pub use self::filesystem::{FsError, FsResult, ReaddirIterator}; +pub use self::network::{Network, SocketError, SocketResult}; +pub use self::stdio::IsATTY; +pub(crate) use tcp::P2TcpStreamingState; +// These contents of wasmtime-wasi-io are re-exported by this module for compatibility: +// they were originally defined in this module before being factored out, and many +// users of this module depend on them at these names. +pub use wasmtime_wasi_io::poll::{DynFuture, DynPollable, MakeFuture, Pollable, subscribe}; +pub use wasmtime_wasi_io::streams::{ + DynInputStream, DynOutputStream, Error as IoError, InputStream, OutputStream, StreamError, + StreamResult, +}; + +/// Add all WASI interfaces from this crate into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p2::add_to_linker_async(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut builder = WasiCtx::builder(); +/// +/// // ... configure `builder` more to add env vars, args, etc ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_async(linker: &mut Linker) -> anyhow::Result<()> { + let options = bindings::LinkOptions::default(); + add_to_linker_with_options_async(linker, &options) +} + +/// Similar to [`add_to_linker_async`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options_async( + linker: &mut Linker, + options: &bindings::LinkOptions, +) -> anyhow::Result<()> { + add_async_io_to_linker(linker)?; + add_nonblocking_to_linker(linker, options)?; + + let l = linker; + bindings::filesystem::types::add_to_linker::(l, T::filesystem)?; + bindings::sockets::tcp::add_to_linker::(l, T::sockets)?; + bindings::sockets::udp::add_to_linker::(l, T::sockets)?; + Ok(()) +} + +/// Shared functionality for [`add_to_linker_async`] and [`add_to_linker_sync`]. +fn add_nonblocking_to_linker<'a, T: WasiView, O>( + linker: &mut Linker, + options: &'a O, +) -> anyhow::Result<()> +where + bindings::sockets::network::LinkOptions: From<&'a O>, + bindings::cli::exit::LinkOptions: From<&'a O>, +{ + use crate::p2::bindings::{cli, clocks, filesystem, random, sockets}; + + let l = linker; + clocks::wall_clock::add_to_linker::(l, T::clocks)?; + clocks::monotonic_clock::add_to_linker::(l, T::clocks)?; + filesystem::preopens::add_to_linker::(l, T::filesystem)?; + random::random::add_to_linker::(l, |t| &mut t.ctx().ctx.random)?; + random::insecure::add_to_linker::(l, |t| &mut t.ctx().ctx.random)?; + random::insecure_seed::add_to_linker::(l, |t| &mut t.ctx().ctx.random)?; + cli::exit::add_to_linker::(l, &options.into(), T::cli)?; + cli::environment::add_to_linker::(l, T::cli)?; + cli::stdin::add_to_linker::(l, T::cli)?; + cli::stdout::add_to_linker::(l, T::cli)?; + cli::stderr::add_to_linker::(l, T::cli)?; + cli::terminal_input::add_to_linker::(l, T::cli)?; + cli::terminal_output::add_to_linker::(l, T::cli)?; + cli::terminal_stdin::add_to_linker::(l, T::cli)?; + cli::terminal_stdout::add_to_linker::(l, T::cli)?; + cli::terminal_stderr::add_to_linker::(l, T::cli)?; + sockets::tcp_create_socket::add_to_linker::(l, T::sockets)?; + sockets::udp_create_socket::add_to_linker::(l, T::sockets)?; + sockets::instance_network::add_to_linker::(l, T::sockets)?; + sockets::network::add_to_linker::(l, &options.into(), T::sockets)?; + sockets::ip_name_lookup::add_to_linker::(l, T::sockets)?; + Ok(()) +} + +/// Same as [`add_to_linker_async`] except that this only adds interfaces +/// present in the `wasi:http/proxy` world. +pub fn add_to_linker_proxy_interfaces_async( + linker: &mut Linker, +) -> anyhow::Result<()> { + add_async_io_to_linker(linker)?; + add_proxy_interfaces_nonblocking(linker) +} + +/// Same as [`add_to_linker_sync`] except that this only adds interfaces +/// present in the `wasi:http/proxy` world. +#[doc(hidden)] +pub fn add_to_linker_proxy_interfaces_sync( + linker: &mut Linker, +) -> anyhow::Result<()> { + add_sync_wasi_io(linker)?; + add_proxy_interfaces_nonblocking(linker) +} + +fn add_proxy_interfaces_nonblocking(linker: &mut Linker) -> anyhow::Result<()> { + use crate::p2::bindings::{cli, clocks, random}; + + let l = linker; + clocks::wall_clock::add_to_linker::(l, T::clocks)?; + clocks::monotonic_clock::add_to_linker::(l, T::clocks)?; + random::random::add_to_linker::(l, |t| &mut t.ctx().ctx.random)?; + cli::stdin::add_to_linker::(l, T::cli)?; + cli::stdout::add_to_linker::(l, T::cli)?; + cli::stderr::add_to_linker::(l, T::cli)?; + Ok(()) +} + +/// Add all WASI interfaces from this crate into the `linker` provided. +/// +/// This function will add the synchronous variant of all interfaces into the +/// [`Linker`] provided. By synchronous this means that this function is only +/// compatible with [`Config::async_support(false)`][async]. For embeddings +/// with async support enabled see [`add_to_linker_async`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// +/// fn main() -> Result<()> { +/// let engine = Engine::default(); +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p2::add_to_linker_sync(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut builder = WasiCtx::builder(); +/// +/// // ... configure `builder` more to add env vars, args, etc ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::new(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_sync( + linker: &mut wasmtime::component::Linker, +) -> anyhow::Result<()> { + let options = bindings::sync::LinkOptions::default(); + add_to_linker_with_options_sync(linker, &options) +} + +/// Similar to [`add_to_linker_sync`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options_sync( + linker: &mut wasmtime::component::Linker, + options: &bindings::sync::LinkOptions, +) -> anyhow::Result<()> { + add_nonblocking_to_linker(linker, options)?; + add_sync_wasi_io(linker)?; + + let l = linker; + bindings::sync::filesystem::types::add_to_linker::(l, T::filesystem)?; + bindings::sync::sockets::tcp::add_to_linker::(l, T::sockets)?; + bindings::sync::sockets::udp::add_to_linker::(l, T::sockets)?; + Ok(()) +} + +/// Shared functionality of [`add_to_linker_sync`]` and +/// [`add_to_linker_proxy_interfaces_sync`]. +fn add_sync_wasi_io( + linker: &mut wasmtime::component::Linker, +) -> anyhow::Result<()> { + let l = linker; + wasmtime_wasi_io::bindings::wasi::io::error::add_to_linker::(l, |t| t.ctx().table)?; + bindings::sync::io::poll::add_to_linker::(l, |t| t.ctx().table)?; + bindings::sync::io::streams::add_to_linker::(l, |t| t.ctx().table)?; + Ok(()) +} + +struct HasIo; + +impl HasData for HasIo { + type Data<'a> = &'a mut ResourceTable; +} + +// FIXME: it's a bit unfortunate that this can't use +// `wasmtime_wasi_io::add_to_linker` and that's because `T: WasiView`, here, +// not `T: IoView`. Ideally we'd have `impl IoView for T` but +// that's not possible with these two traits in separate crates. For now this +// is some small duplication but if this gets worse over time then we'll want +// to massage this. +fn add_async_io_to_linker(l: &mut Linker) -> anyhow::Result<()> { + wasmtime_wasi_io::bindings::wasi::io::error::add_to_linker::(l, |t| t.ctx().table)?; + wasmtime_wasi_io::bindings::wasi::io::poll::add_to_linker::(l, |t| t.ctx().table)?; + wasmtime_wasi_io::bindings::wasi::io::streams::add_to_linker::(l, |t| t.ctx().table)?; + Ok(()) +} diff --git a/crates/wasi/src/p2/network.rs b/crates/wasi/src/p2/network.rs new file mode 100644 index 00000000..691e8c1e --- /dev/null +++ b/crates/wasi/src/p2/network.rs @@ -0,0 +1,70 @@ +use crate::TrappableError; +use crate::p2::bindings::sockets::network::ErrorCode; +use crate::sockets::{SocketAddrCheck, SocketAddrUse}; +use std::net::SocketAddr; + +pub type SocketResult = Result; + +pub type SocketError = TrappableError; + +impl From for SocketError { + fn from(error: wasmtime::component::ResourceTableError) -> Self { + Self::trap(error) + } +} + +impl From for SocketError { + fn from(error: std::io::Error) -> Self { + ErrorCode::from(error).into() + } +} + +impl From for SocketError { + fn from(error: rustix::io::Errno) -> Self { + ErrorCode::from(error).into() + } +} + +impl From for SocketError { + fn from(error: crate::sockets::util::ErrorCode) -> Self { + ErrorCode::from(error).into() + } +} + +impl From for ErrorCode { + fn from(error: crate::sockets::util::ErrorCode) -> Self { + match error { + crate::sockets::util::ErrorCode::Unknown => Self::Unknown, + crate::sockets::util::ErrorCode::AccessDenied => Self::AccessDenied, + crate::sockets::util::ErrorCode::NotSupported => Self::NotSupported, + crate::sockets::util::ErrorCode::InvalidArgument => Self::InvalidArgument, + crate::sockets::util::ErrorCode::OutOfMemory => Self::OutOfMemory, + crate::sockets::util::ErrorCode::Timeout => Self::Timeout, + crate::sockets::util::ErrorCode::InvalidState => Self::InvalidState, + crate::sockets::util::ErrorCode::AddressNotBindable => Self::AddressNotBindable, + crate::sockets::util::ErrorCode::AddressInUse => Self::AddressInUse, + crate::sockets::util::ErrorCode::RemoteUnreachable => Self::RemoteUnreachable, + crate::sockets::util::ErrorCode::ConnectionRefused => Self::ConnectionRefused, + crate::sockets::util::ErrorCode::ConnectionReset => Self::ConnectionReset, + crate::sockets::util::ErrorCode::ConnectionAborted => Self::ConnectionAborted, + crate::sockets::util::ErrorCode::DatagramTooLarge => Self::DatagramTooLarge, + crate::sockets::util::ErrorCode::NotInProgress => Self::NotInProgress, + crate::sockets::util::ErrorCode::ConcurrencyConflict => Self::ConcurrencyConflict, + } + } +} + +pub struct Network { + pub(crate) socket_addr_check: SocketAddrCheck, + pub(crate) allow_ip_name_lookup: bool, +} + +impl Network { + pub async fn check_socket_addr( + &self, + addr: SocketAddr, + reason: SocketAddrUse, + ) -> std::io::Result<()> { + self.socket_addr_check.check(addr, reason).await + } +} diff --git a/crates/wasi/src/p2/pipe.rs b/crates/wasi/src/p2/pipe.rs new file mode 100644 index 00000000..96b94f40 --- /dev/null +++ b/crates/wasi/src/p2/pipe.rs @@ -0,0 +1,885 @@ +//! Virtual pipes. +//! +//! These types provide easy implementations of `WasiFile` that mimic much of the behavior of Unix +//! pipes. These are particularly helpful for redirecting WASI stdio handles to destinations other +//! than OS files. +//! +//! Some convenience constructors are included for common backing types like `Vec` and `String`, +//! but the virtual pipes can be instantiated with any `Read` or `Write` type. +//! +use anyhow::anyhow; +use bytes::Bytes; +use std::pin::{Pin, pin}; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use tokio::io::{self, AsyncRead, AsyncWrite}; +use tokio::sync::mpsc; +use wasmtime_wasi_io::{ + poll::Pollable, + streams::{InputStream, OutputStream, StreamError}, +}; + +pub use crate::p2::write_stream::AsyncWriteStream; + +#[derive(Debug, Clone)] +pub struct MemoryInputPipe { + buffer: Arc>, +} + +impl MemoryInputPipe { + pub fn new(bytes: impl Into) -> Self { + Self { + buffer: Arc::new(Mutex::new(bytes.into())), + } + } + + pub fn is_empty(&self) -> bool { + self.buffer.lock().unwrap().is_empty() + } +} + +#[async_trait::async_trait] +impl InputStream for MemoryInputPipe { + fn read(&mut self, size: usize) -> Result { + let mut buffer = self.buffer.lock().unwrap(); + if buffer.is_empty() { + return Err(StreamError::Closed); + } + + let size = size.min(buffer.len()); + let read = buffer.split_to(size); + Ok(read) + } +} + +#[async_trait::async_trait] +impl Pollable for MemoryInputPipe { + async fn ready(&mut self) {} +} + +impl AsyncRead for MemoryInputPipe { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut io::ReadBuf<'_>, + ) -> Poll> { + let mut buffer = self.buffer.lock().unwrap(); + let size = buf.remaining().min(buffer.len()); + let read = buffer.split_to(size); + buf.put_slice(&read); + Poll::Ready(Ok(())) + } +} + +#[derive(Debug, Clone)] +pub struct MemoryOutputPipe { + capacity: usize, + buffer: Arc>, +} + +impl MemoryOutputPipe { + pub fn new(capacity: usize) -> Self { + MemoryOutputPipe { + capacity, + buffer: std::sync::Arc::new(std::sync::Mutex::new(bytes::BytesMut::new())), + } + } + + pub fn contents(&self) -> bytes::Bytes { + self.buffer.lock().unwrap().clone().freeze() + } + + pub fn try_into_inner(self) -> Option { + std::sync::Arc::into_inner(self.buffer).map(|m| m.into_inner().unwrap()) + } +} + +#[async_trait::async_trait] +impl OutputStream for MemoryOutputPipe { + fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> { + let mut buf = self.buffer.lock().unwrap(); + if bytes.len() > self.capacity - buf.len() { + return Err(StreamError::Trap(anyhow!( + "write beyond capacity of MemoryOutputPipe" + ))); + } + buf.extend_from_slice(bytes.as_ref()); + // Always ready for writing + Ok(()) + } + fn flush(&mut self) -> Result<(), StreamError> { + // This stream is always flushed + Ok(()) + } + fn check_write(&mut self) -> Result { + let consumed = self.buffer.lock().unwrap().len(); + if consumed < self.capacity { + Ok(self.capacity - consumed) + } else { + // Since the buffer is full, no more bytes will ever be written + Err(StreamError::Closed) + } + } +} + +#[async_trait::async_trait] +impl Pollable for MemoryOutputPipe { + async fn ready(&mut self) {} +} + +impl AsyncWrite for MemoryOutputPipe { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut buffer = self.buffer.lock().unwrap(); + let amt = buf.len().min(self.capacity - buffer.len()); + buffer.extend_from_slice(&buf[..amt]); + Poll::Ready(Ok(amt)) + } + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +/// Provides a [`InputStream`] impl from a [`tokio::io::AsyncRead`] impl +pub struct AsyncReadStream { + closed: bool, + buffer: Option>, + receiver: mpsc::Receiver>, + join_handle: Option>, +} + +impl AsyncReadStream { + /// Create a [`AsyncReadStream`]. In order to use the [`InputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. + pub fn new(reader: T) -> Self { + let (sender, receiver) = mpsc::channel(1); + let join_handle = crate::runtime::spawn(async move { + let mut reader = pin!(reader); + loop { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = match reader.read_buf(&mut buf).await { + Ok(nbytes) if nbytes == 0 => sender.send(Err(StreamError::Closed)).await, + Ok(_) => sender.send(Ok(buf.freeze())).await, + Err(e) => { + sender + .send(Err(StreamError::LastOperationFailed(e.into()))) + .await + } + }; + if sent.is_err() { + // no more receiver - stop trying to read + break; + } + } + }); + AsyncReadStream { + closed: false, + buffer: None, + receiver, + join_handle: Some(join_handle), + } + } + pub(crate) fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { + if self.buffer.is_some() || self.closed { + return Poll::Ready(()); + } + match self.receiver.poll_recv(cx) { + Poll::Ready(Some(res)) => { + self.buffer = Some(res); + Poll::Ready(()) + } + Poll::Ready(None) => { + panic!("no more sender for an open AsyncReadStream - should be impossible") + } + Poll::Pending => Poll::Pending, + } + } +} + +#[async_trait::async_trait] +impl InputStream for AsyncReadStream { + fn read(&mut self, size: usize) -> Result { + use mpsc::error::TryRecvError; + + match self.buffer.take() { + Some(Ok(mut bytes)) => { + // TODO: de-duplicate the buffer management with the case below + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + } + return Ok(bytes); + } + Some(Err(e)) => { + self.closed = true; + return Err(e); + } + None => {} + } + + match self.receiver.try_recv() { + Ok(Ok(mut bytes)) => { + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + } + + Ok(bytes) + } + Ok(Err(e)) => { + self.closed = true; + Err(e) + } + Err(TryRecvError::Empty) => Ok(Bytes::new()), + Err(TryRecvError::Disconnected) => Err(StreamError::Trap(anyhow!( + "AsyncReadStream sender died - should be impossible" + ))), + } + } + + async fn cancel(&mut self) { + match self.join_handle.take() { + Some(task) => _ = task.cancel().await, + None => {} + } + } +} + +#[async_trait::async_trait] +impl Pollable for AsyncReadStream { + async fn ready(&mut self) { + std::future::poll_fn(|cx| self.poll_ready(cx)).await + } +} + +/// An output stream that consumes all input written to it, and is always ready. +#[derive(Copy, Clone)] +pub struct SinkOutputStream; + +#[async_trait::async_trait] +impl OutputStream for SinkOutputStream { + fn write(&mut self, _buf: Bytes) -> Result<(), StreamError> { + Ok(()) + } + fn flush(&mut self) -> Result<(), StreamError> { + // This stream is always flushed + Ok(()) + } + + fn check_write(&mut self) -> Result { + // This stream is always ready for writing. + Ok(usize::MAX) + } +} + +#[async_trait::async_trait] +impl Pollable for SinkOutputStream { + async fn ready(&mut self) {} +} + +/// A stream that is ready immediately, but will always report that it's closed. +#[derive(Copy, Clone)] +pub struct ClosedInputStream; + +#[async_trait::async_trait] +impl InputStream for ClosedInputStream { + fn read(&mut self, _size: usize) -> Result { + Err(StreamError::Closed) + } +} + +#[async_trait::async_trait] +impl Pollable for ClosedInputStream { + async fn ready(&mut self) {} +} + +/// An output stream that is always closed. +#[derive(Copy, Clone)] +pub struct ClosedOutputStream; + +#[async_trait::async_trait] +impl OutputStream for ClosedOutputStream { + fn write(&mut self, _: Bytes) -> Result<(), StreamError> { + Err(StreamError::Closed) + } + fn flush(&mut self) -> Result<(), StreamError> { + Err(StreamError::Closed) + } + + fn check_write(&mut self) -> Result { + Err(StreamError::Closed) + } +} + +#[async_trait::async_trait] +impl Pollable for ClosedOutputStream { + async fn ready(&mut self) {} +} + +#[cfg(test)] +mod test { + use super::*; + use std::time::Duration; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + + // This is a gross way to handle CI running under qemu for non-x86 architectures. + #[cfg(not(target_arch = "x86_64"))] + const TEST_ITERATIONS: usize = 10; + + #[cfg(target_arch = "x86_64")] + const TEST_ITERATIONS: usize = 100; + + async fn resolves_immediately(fut: F) -> O + where + F: futures::Future, + { + // The input `fut` should resolve immediately, but in case it + // accidentally doesn't don't hang the test indefinitely. Provide a + // generous timeout to account for CI sensitivity and various systems. + tokio::time::timeout(Duration::from_secs(2), fut) + .await + .expect("operation timed out") + } + + async fn never_resolves(fut: F) { + // The input `fut` should never resolve, so only give it a small window + // of budget before we time out. If `fut` is actually resolved this + // should show up as a flaky test. + tokio::time::timeout(Duration::from_millis(10), fut) + .await + .err() + .expect("operation should time out"); + } + + pub fn simplex(size: usize) -> (impl AsyncRead, impl AsyncWrite) { + let (a, b) = tokio::io::duplex(size); + let (_read_half, write_half) = tokio::io::split(a); + let (read_half, _write_half) = tokio::io::split(b); + (read_half, write_half) + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn empty_read_stream() { + let mut reader = AsyncReadStream::new(tokio::io::empty()); + + // In a multi-threaded context, the value of state is not deterministic -- the spawned + // reader task may run on a different thread. + match reader.read(10) { + // The reader task ran before we tried to read, and noticed that the input was empty. + Err(StreamError::Closed) => {} + + // The reader task hasn't run yet. Call `ready` to await and fill the buffer. + Ok(bs) => { + assert!(bs.is_empty()); + resolves_immediately(reader.ready()).await; + assert!(matches!(reader.read(0), Err(StreamError::Closed))); + } + res => panic!("unexpected: {res:?}"), + } + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn infinite_read_stream() { + let mut reader = AsyncReadStream::new(tokio::io::repeat(0)); + + let bs = reader.read(10).unwrap(); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + resolves_immediately(reader.ready()).await; + // Now a read should succeed + let bs = reader.read(10).unwrap(); + assert_eq!(bs.len(), 10); + } else { + assert_eq!(bs.len(), 10); + } + + // Subsequent reads should succeed + let bs = reader.read(10).unwrap(); + assert_eq!(bs.len(), 10); + + // Even 0-length reads should succeed and show its open + let bs = reader.read(0).unwrap(); + assert_eq!(bs.len(), 0); + } + + async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + 'static + use<> { + let (r, mut w) = simplex(contents.len()); + w.write_all(contents).await.unwrap(); + r + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn finite_read_stream() { + let mut reader = AsyncReadStream::new(finite_async_reader(&[1; 123]).await); + + let bs = reader.read(123).unwrap(); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + resolves_immediately(reader.ready()).await; + // Now a read should succeed + let bs = reader.read(123).unwrap(); + assert_eq!(bs.len(), 123); + } else { + assert_eq!(bs.len(), 123); + } + + // The AsyncRead's should be empty now, but we have a race where the reader task hasn't + // yet send that to the AsyncReadStream. + match reader.read(0) { + Err(StreamError::Closed) => {} // Correct! + Ok(bs) => { + assert!(bs.is_empty()); + // Need to await to give this side time to catch up + resolves_immediately(reader.ready()).await; + // Now a read should show closed + assert!(matches!(reader.read(0), Err(StreamError::Closed))); + } + res => panic!("unexpected: {res:?}"), + } + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + // Test that you can write items into the stream, and they get read out in the order they were + // written, with the proper indications of readiness for reading: + async fn multiple_chunks_read_stream() { + let (r, mut w) = simplex(1024); + let mut reader = AsyncReadStream::new(r); + + w.write_all(&[123]).await.unwrap(); + + let bs = reader.read(1).unwrap(); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + resolves_immediately(reader.ready()).await; + // Now a read should succeed + let bs = reader.read(1).unwrap(); + assert_eq!(*bs, [123u8]); + } else { + assert_eq!(*bs, [123u8]); + } + + // The stream should be empty and open now: + let bs = reader.read(1).unwrap(); + assert!(bs.is_empty()); + + // We can wait on readiness and it will time out: + never_resolves(reader.ready()).await; + + // Still open and empty: + let bs = reader.read(1).unwrap(); + assert!(bs.is_empty()); + + // Put something else in the stream: + w.write_all(&[45]).await.unwrap(); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + resolves_immediately(reader.ready()).await; + + // read the something else back out: + let bs = reader.read(1).unwrap(); + assert_eq!(*bs, [45u8]); + + // nothing else in there: + let bs = reader.read(1).unwrap(); + assert!(bs.is_empty()); + + // We can wait on readiness and it will time out: + never_resolves(reader.ready()).await; + + // nothing else in there: + let bs = reader.read(1).unwrap(); + assert!(bs.is_empty()); + + // Now close the pipe: + drop(w); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + resolves_immediately(reader.ready()).await; + + // empty and now closed: + assert!(matches!(reader.read(1), Err(StreamError::Closed))); + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + // At the moment we are restricting AsyncReadStream from buffering more than 4k. This isn't a + // suitable design for all applications, and we will probably make a knob or change the + // behavior at some point, but this test shows the behavior as it is implemented: + async fn backpressure_read_stream() { + let (r, mut w) = simplex(16 * 1024); // Make sure this buffer isn't a bottleneck + let mut reader = AsyncReadStream::new(r); + + let writer_task = tokio::task::spawn(async move { + // Write twice as much as we can buffer up in an AsyncReadStream: + w.write_all(&[123; 8192]).await.unwrap(); + w + }); + + resolves_immediately(reader.ready()).await; + + // Now we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let bs = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + + // Allow the crank to turn more: + resolves_immediately(reader.ready()).await; + + // Again we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let bs = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + + // The writer task is now finished - join with it: + let w = resolves_immediately(writer_task).await; + + // And close the pipe: + drop(w); + + // Allow the crank to turn more: + resolves_immediately(reader.ready()).await; + + // Now we expect the reader to be empty, and the stream.dropd: + assert!(matches!(reader.read(4097), Err(StreamError::Closed))); + } + + #[test_log::test(test_log::test(tokio::test(flavor = "multi_thread")))] + async fn sink_write_stream() { + let mut writer = AsyncWriteStream::new(2048, tokio::io::sink()); + let chunk = Bytes::from_static(&[0; 1024]); + + let readiness = resolves_immediately(writer.write_ready()) + .await + .expect("write_ready does not trap"); + assert_eq!(readiness, 2048); + // I can write whatever: + writer.write(chunk.clone()).expect("write does not error"); + + // This may consume 1k of the buffer: + let readiness = resolves_immediately(writer.write_ready()) + .await + .expect("write_ready does not trap"); + assert!( + readiness == 1024 || readiness == 2048, + "readiness should be 1024 or 2048, got {readiness}" + ); + + if readiness == 1024 { + writer.write(chunk.clone()).expect("write does not error"); + + let readiness = resolves_immediately(writer.write_ready()) + .await + .expect("write_ready does not trap"); + assert!( + readiness == 1024 || readiness == 2048, + "readiness should be 1024 or 2048, got {readiness}" + ); + } + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn closed_write_stream() { + // Run many times because the test is nondeterministic: + for n in 0..TEST_ITERATIONS { + closed_write_stream_(n).await + } + } + #[tracing::instrument] + async fn closed_write_stream_(n: usize) { + let (reader, writer) = simplex(1); + let mut writer = AsyncWriteStream::new(1024, writer); + + // Drop the reader to allow the worker to transition to the closed state eventually. + drop(reader); + + // First the api is going to report the last operation failed, then subsequently + // it will be reported as closed. We set this flag once we see LastOperationFailed. + let mut should_be_closed = false; + + // Write some data to the stream to ensure we have data that cannot be flushed. + let chunk = Bytes::from_static(&[0; 1]); + writer + .write(chunk.clone()) + .expect("first write should succeed"); + + // The rest of this test should be valid whether or not we check write readiness: + let mut write_ready_res = None; + if n % 2 == 0 { + let r = resolves_immediately(writer.write_ready()).await; + // Check write readiness: + match r { + // worker hasn't processed write yet: + Ok(1023) => {} + // worker reports failure: + Err(StreamError::LastOperationFailed(_)) => { + tracing::debug!("discovered stream failure in first write_ready"); + should_be_closed = true; + } + r => panic!("unexpected write_ready: {r:?}"), + } + write_ready_res = Some(r); + } + + // When we drop the simplex reader, that causes the simplex writer to return BrokenPipe on + // its write. Now that the buffering crank has turned, our next write will give BrokenPipe. + let flush_res = writer.flush(); + match flush_res { + // worker reports failure: + Err(StreamError::LastOperationFailed(_)) => { + tracing::debug!("discovered stream failure trying to flush"); + assert!(!should_be_closed); + should_be_closed = true; + } + // Already reported failure, now closed + Err(StreamError::Closed) => { + assert!( + should_be_closed, + "expected a LastOperationFailed before we see Closed. {write_ready_res:?}" + ); + } + // Also possible the worker hasn't processed write yet: + Ok(()) => {} + Err(e) => panic!("unexpected flush error: {e:?} {write_ready_res:?}"), + } + + // Waiting for the flush to complete should always indicate that the channel has been + // closed. + match resolves_immediately(writer.write_ready()).await { + // worker reports failure: + Err(StreamError::LastOperationFailed(_)) => { + tracing::debug!("discovered stream failure trying to flush"); + assert!(!should_be_closed); + } + // Already reported failure, now closed + Err(StreamError::Closed) => { + assert!(should_be_closed); + } + r => { + panic!( + "stream should be reported closed by the end of write_ready after flush, got {r:?}. {write_ready_res:?} {flush_res:?}" + ) + } + } + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn multiple_chunks_write_stream() { + // Run many times because the test is nondeterministic: + for n in 0..TEST_ITERATIONS { + multiple_chunks_write_stream_aux(n).await + } + } + #[tracing::instrument] + async fn multiple_chunks_write_stream_aux(_: usize) { + use std::ops::Deref; + + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(1024, writer); + + // Write a chunk: + let chunk = Bytes::from_static(&[123; 1]); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + + writer.write(chunk.clone()).expect("write does not trap"); + + // At this point the message will either be waiting for the worker to process the write, or + // it will be buffered in the simplex channel. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert!(matches!(permit, 1023 | 1024)); + + let mut read_buf = vec![0; chunk.len()]; + let read_len = reader.read_exact(&mut read_buf).await.unwrap(); + assert_eq!(read_len, chunk.len()); + assert_eq!(read_buf.as_slice(), chunk.deref()); + + // Write a second, different chunk: + let chunk2 = Bytes::from_static(&[45; 1]); + + // We're only guaranteed to see a consistent write budget if we flush. + writer.flush().expect("channel is still alive"); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + + writer.write(chunk2.clone()).expect("write does not trap"); + + // At this point the message will either be waiting for the worker to process the write, or + // it will be buffered in the simplex channel. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert!(matches!(permit, 1023 | 1024)); + + let mut read2_buf = vec![0; chunk2.len()]; + let read2_len = reader.read_exact(&mut read2_buf).await.unwrap(); + assert_eq!(read2_len, chunk2.len()); + assert_eq!(read2_buf.as_slice(), chunk2.deref()); + + // We're only guaranteed to see a consistent write budget if we flush. + writer.flush().expect("channel is still alive"); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn backpressure_write_stream() { + // Run many times because the test is nondeterministic: + for n in 0..TEST_ITERATIONS { + backpressure_write_stream_aux(n).await + } + } + #[tracing::instrument] + async fn backpressure_write_stream_aux(_: usize) { + use futures::future::poll_immediate; + + // The channel can buffer up to 1k, plus another 1k in the stream, before not + // accepting more input: + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(1024, writer); + + let chunk = Bytes::from_static(&[0; 1024]); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + + writer.write(chunk.clone()).expect("write succeeds"); + + // We might still be waiting for the worker to process the message, or the worker may have + // processed it and released all the budget back to us. + let permit = poll_immediate(writer.write_ready()).await; + assert!(matches!(permit, None | Some(Ok(1024)))); + + // Given a little time, the worker will process the message and release all the budget + // back. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + + // Now fill the buffer between here and the writer task. This should always indicate + // back-pressure because now both buffers (simplex and worker) are full. + writer.write(chunk.clone()).expect("write does not trap"); + + // Try shoving even more down there, and it shouldn't accept more input: + writer + .write(chunk.clone()) + .err() + .expect("unpermitted write does trap"); + + // No amount of waiting will resolve the situation, as nothing is emptying the simplex + // buffer. + never_resolves(writer.write_ready()).await; + + // There is 2k buffered between the simplex and worker buffers. I should be able to read + // all of it out: + let mut buf = [0; 2048]; + reader.read_exact(&mut buf).await.unwrap(); + + // and no more: + never_resolves(reader.read(&mut buf)).await; + + // Now the backpressure should be cleared, and an additional write should be accepted. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("ready is ok"); + assert_eq!(permit, 1024); + + // and the write succeeds: + writer.write(chunk.clone()).expect("write does not trap"); + } + + #[test_log::test(tokio::test(flavor = "multi_thread"))] + async fn backpressure_write_stream_with_flush() { + for n in 0..TEST_ITERATIONS { + backpressure_write_stream_with_flush_aux(n).await; + } + } + + async fn backpressure_write_stream_with_flush_aux(_: usize) { + // The channel can buffer up to 1k, plus another 1k in the stream, before not + // accepting more input: + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(1024, writer); + + let chunk = Bytes::from_static(&[0; 1024]); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write should be ready"); + assert_eq!(permit, 1024); + + writer.write(chunk.clone()).expect("write succeeds"); + + writer.flush().expect("flush succeeds"); + + // Waiting for write_ready to resolve after a flush should always show that we have the + // full budget available, as the message will have flushed to the simplex channel. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("write_ready succeeds"); + assert_eq!(permit, 1024); + + // Write enough to fill the simplex buffer: + writer.write(chunk.clone()).expect("write does not trap"); + + // Writes should be refused until this flush succeeds. + writer.flush().expect("flush succeeds"); + + // Try shoving even more down there, and it shouldn't accept more input: + writer + .write(chunk.clone()) + .err() + .expect("unpermitted write does trap"); + + // No amount of waiting will resolve the situation, as nothing is emptying the simplex + // buffer. + never_resolves(writer.write_ready()).await; + + // There is 2k buffered between the simplex and worker buffers. I should be able to read + // all of it out: + let mut buf = [0; 2048]; + reader.read_exact(&mut buf).await.unwrap(); + + // and no more: + never_resolves(reader.read(&mut buf)).await; + + // Now the backpressure should be cleared, and an additional write should be accepted. + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("ready is ok"); + assert_eq!(permit, 1024); + + // and the write succeeds: + writer.write(chunk.clone()).expect("write does not trap"); + + writer.flush().expect("flush succeeds"); + + let permit = resolves_immediately(writer.write_ready()) + .await + .expect("ready is ok"); + assert_eq!(permit, 1024); + } +} diff --git a/crates/wasi/src/p2/poll.rs b/crates/wasi/src/p2/poll.rs new file mode 100644 index 00000000..9cbee5f9 --- /dev/null +++ b/crates/wasi/src/p2/poll.rs @@ -0,0 +1,23 @@ +use crate::runtime::in_tokio; +use wasmtime_wasi_io::{bindings::wasi::io::poll as async_poll, poll::DynPollable}; + +use anyhow::Result; +use wasmtime::component::{Resource, ResourceTable}; + +impl crate::p2::bindings::sync::io::poll::Host for ResourceTable { + fn poll(&mut self, pollables: Vec>) -> Result> { + in_tokio(async { async_poll::Host::poll(self, pollables).await }) + } +} + +impl crate::p2::bindings::sync::io::poll::HostPollable for ResourceTable { + fn ready(&mut self, pollable: Resource) -> Result { + in_tokio(async { async_poll::HostPollable::ready(self, pollable).await }) + } + fn block(&mut self, pollable: Resource) -> Result<()> { + in_tokio(async { async_poll::HostPollable::block(self, pollable).await }) + } + fn drop(&mut self, pollable: Resource) -> Result<()> { + async_poll::HostPollable::drop(self, pollable) + } +} diff --git a/crates/wasi/src/p2/stdio.rs b/crates/wasi/src/p2/stdio.rs new file mode 100644 index 00000000..3184e006 --- /dev/null +++ b/crates/wasi/src/p2/stdio.rs @@ -0,0 +1,82 @@ +use crate::cli::{IsTerminal, WasiCliCtxView}; +use crate::p2::bindings::cli::{ + stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin, + terminal_stdout, +}; +use wasmtime::component::Resource; +use wasmtime_wasi_io::streams; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsATTY { + Yes, + No, +} + +impl stdin::Host for WasiCliCtxView<'_> { + fn get_stdin(&mut self) -> Result, anyhow::Error> { + let stream = self.ctx.stdin.p2_stream(); + Ok(self.table.push(stream)?) + } +} + +impl stdout::Host for WasiCliCtxView<'_> { + fn get_stdout(&mut self) -> Result, anyhow::Error> { + let stream = self.ctx.stdout.p2_stream(); + Ok(self.table.push(stream)?) + } +} + +impl stderr::Host for WasiCliCtxView<'_> { + fn get_stderr(&mut self) -> Result, anyhow::Error> { + let stream = self.ctx.stderr.p2_stream(); + Ok(self.table.push(stream)?) + } +} + +pub struct TerminalInput; +pub struct TerminalOutput; + +impl terminal_input::Host for WasiCliCtxView<'_> {} +impl terminal_input::HostTerminalInput for WasiCliCtxView<'_> { + fn drop(&mut self, r: Resource) -> anyhow::Result<()> { + self.table.delete(r)?; + Ok(()) + } +} +impl terminal_output::Host for WasiCliCtxView<'_> {} +impl terminal_output::HostTerminalOutput for WasiCliCtxView<'_> { + fn drop(&mut self, r: Resource) -> anyhow::Result<()> { + self.table.delete(r)?; + Ok(()) + } +} +impl terminal_stdin::Host for WasiCliCtxView<'_> { + fn get_terminal_stdin(&mut self) -> anyhow::Result>> { + if self.ctx.stdin.is_terminal() { + let fd = self.table.push(TerminalInput)?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} +impl terminal_stdout::Host for WasiCliCtxView<'_> { + fn get_terminal_stdout(&mut self) -> anyhow::Result>> { + if self.ctx.stdout.is_terminal() { + let fd = self.table.push(TerminalOutput)?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} +impl terminal_stderr::Host for WasiCliCtxView<'_> { + fn get_terminal_stderr(&mut self) -> anyhow::Result>> { + if self.ctx.stderr.is_terminal() { + let fd = self.table.push(TerminalOutput)?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} diff --git a/crates/wasi/src/p2/tcp.rs b/crates/wasi/src/p2/tcp.rs new file mode 100644 index 00000000..8e425a15 --- /dev/null +++ b/crates/wasi/src/p2/tcp.rs @@ -0,0 +1,538 @@ +use crate::p2::{ + DynInputStream, DynOutputStream, InputStream, OutputStream, Pollable, SocketError, + SocketResult, StreamError, +}; +use crate::runtime::AbortOnDropJoinHandle; +use crate::sockets::TcpSocket; +use anyhow::Result; +use io_lifetimes::AsSocketlike; +use rustix::io::Errno; +use std::io; +use std::mem; +use std::net::Shutdown; +use std::sync::Arc; +use tokio::sync::Mutex; + +impl TcpSocket { + pub(crate) fn p2_streams(&mut self) -> SocketResult<(DynInputStream, DynOutputStream)> { + match self { + Self::Network(socket) => { + let client = socket.tcp_stream_arc()?; + let reader = Arc::new(Mutex::new(TcpReader::new(client.clone()))); + let writer = Arc::new(Mutex::new(TcpWriter::new(client.clone()))); + socket.set_p2_streaming_state(P2TcpStreamingState { + stream: client.clone(), + reader: reader.clone(), + writer: writer.clone(), + })?; + let input: DynInputStream = Box::new(TcpReadStream(reader)); + let output: DynOutputStream = Box::new(TcpWriteStream(writer)); + Ok((input, output)) + } + Self::Loopback(socket) => { + use crate::sockets::loopback::{TcpConn, TcpSocket, TcpState}; + let state = mem::replace(&mut socket.state, TcpState::Closed); + let TcpState::Connected { + conn: + TcpConn { + local_address, + remote_address, + rx, + tx, + }, + accepted, + } = state + else { + socket.state = state; + return Err(crate::sockets::util::ErrorCode::InvalidState.into()); + }; + let rx = Arc::new(Mutex::new(Some(rx))); + let tx = Arc::new(std::sync::Mutex::new(Some(tx))); + // Ensure `check-write` allows more than `send_buffer_size` bytes to be written to + // make this assertion succeed: + // https://github.com/bytecodealliance/wasmtime/blob/b1c7887c801b62f7fb39e3bd916d8737b3043135/crates/test-programs/src/bin/p2_tcp_streams.rs#L96 + let permits = socket + .send_buffer_size + .saturating_add(1) + .min(TcpSocket::MAX_SEND_BUFFER_SIZE); + let permits = Arc::new(tokio::sync::Semaphore::new(permits as _)); + socket.state = TcpState::P2Streaming { + local_address, + remote_address, + accepted, + permits: Arc::clone(&permits), + rx: Arc::clone(&rx), + tx: Arc::clone(&tx), + }; + let input: DynInputStream = Box::new(LoopbackInputStream { + rx, + buffer: bytes::Bytes::default(), + }); + let output: DynOutputStream = Box::new(LoopbackOutputStream { tx, permits }); + Ok((input, output)) + } + Self::Unspecified { .. } => Err(crate::sockets::util::ErrorCode::InvalidState.into()), + } + } +} + +pub(crate) struct P2TcpStreamingState { + pub(crate) stream: Arc, + reader: Arc>, + writer: Arc>, +} + +impl P2TcpStreamingState { + pub(crate) fn shutdown(&self, how: Shutdown) -> SocketResult<()> { + if let Shutdown::Both | Shutdown::Read = how { + try_lock_for_socket(&self.reader)?.shutdown(); + } + + if let Shutdown::Both | Shutdown::Write = how { + try_lock_for_socket(&self.writer)?.shutdown(); + } + + Ok(()) + } +} + +struct TcpReader { + stream: Arc, + closed: bool, +} + +impl TcpReader { + fn new(stream: Arc) -> Self { + Self { + stream, + closed: false, + } + } + fn read(&mut self, size: usize) -> Result { + if self.closed { + return Err(StreamError::Closed); + } + if size == 0 { + return Ok(bytes::Bytes::new()); + } + + let mut buf = bytes::BytesMut::with_capacity(size); + let n = match self.stream.try_read_buf(&mut buf) { + // A 0-byte read indicates that the stream has closed. + Ok(0) => { + self.closed = true; + return Err(StreamError::Closed); + } + Ok(n) => n, + + // Failing with `EWOULDBLOCK` is how we differentiate between a closed channel and no + // data to read right now. + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => 0, + + Err(e) => { + self.closed = true; + return Err(StreamError::LastOperationFailed(e.into())); + } + }; + + buf.truncate(n); + Ok(buf.freeze()) + } + + fn shutdown(&mut self) { + native_shutdown(&self.stream, Shutdown::Read); + self.closed = true; + } + + async fn ready(&mut self) { + if self.closed { + return; + } + + self.stream.readable().await.unwrap(); + } +} + +struct TcpReadStream(Arc>); + +#[async_trait::async_trait] +impl InputStream for TcpReadStream { + fn read(&mut self, size: usize) -> Result { + try_lock_for_stream(&self.0)?.read(size) + } +} + +#[async_trait::async_trait] +impl Pollable for TcpReadStream { + async fn ready(&mut self) { + self.0.lock().await.ready().await + } +} + +const SOCKET_READY_SIZE: usize = 1024 * 1024 * 1024; + +struct TcpWriter { + stream: Arc, + state: WriteState, +} + +enum WriteState { + Ready, + Writing(AbortOnDropJoinHandle>), + Closing(AbortOnDropJoinHandle>), + Closed, + Error(io::Error), +} + +impl TcpWriter { + fn new(stream: Arc) -> Self { + Self { + stream, + state: WriteState::Ready, + } + } + + fn try_write_portable(stream: &tokio::net::TcpStream, buf: &[u8]) -> io::Result { + stream.try_write(buf).map_err(|error| { + match Errno::from_io_error(&error) { + // Windows returns `WSAESHUTDOWN` when writing to a shut down socket. + // We normalize this to EPIPE, because that is what the other platforms return. + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send#:~:text=WSAESHUTDOWN + #[cfg(windows)] + Some(Errno::SHUTDOWN) => io::Error::new(io::ErrorKind::BrokenPipe, error), + + _ => error, + } + }) + } + + /// Write `bytes` in a background task, remembering the task handle for use in a future call to + /// `write_ready` + fn background_write(&mut self, mut bytes: bytes::Bytes) { + assert!(matches!(self.state, WriteState::Ready)); + + let stream = self.stream.clone(); + self.state = WriteState::Writing(crate::runtime::spawn(async move { + // Note: we are not using the AsyncWrite impl here, and instead using the TcpStream + // primitive try_write, which goes directly to attempt a write with mio. This has + // two advantages: 1. this operation takes a &TcpStream instead of a &mut TcpStream + // required to AsyncWrite, and 2. it eliminates any buffering in tokio we may need + // to flush. + while !bytes.is_empty() { + stream.writable().await?; + match Self::try_write_portable(&stream, &bytes) { + Ok(n) => { + let _ = bytes.split_to(n); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, + Err(e) => return Err(e), + } + } + + Ok(()) + })); + } + + fn write(&mut self, mut bytes: bytes::Bytes) -> Result<(), StreamError> { + match self.state { + WriteState::Ready => {} + WriteState::Closed => return Err(StreamError::Closed), + WriteState::Writing(_) | WriteState::Closing(_) | WriteState::Error(_) => { + return Err(StreamError::Trap(anyhow::anyhow!( + "unpermitted: must call check_write first" + ))); + } + } + while !bytes.is_empty() { + match Self::try_write_portable(&self.stream, &bytes) { + Ok(n) => { + let _ = bytes.split_to(n); + } + + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // As `try_write` indicated that it would have blocked, we'll perform the write + // in the background to allow us to return immediately. + self.background_write(bytes); + + return Ok(()); + } + + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { + self.state = WriteState::Closed; + return Err(StreamError::Closed); + } + + Err(e) => return Err(StreamError::LastOperationFailed(e.into())), + } + } + + Ok(()) + } + + fn flush(&mut self) -> Result<(), StreamError> { + // `flush` is a no-op here, as we're not managing any internal buffer. Additionally, + // `write_ready` will join the background write task if it's active, so following `flush` + // with `write_ready` will have the desired effect. + match self.state { + WriteState::Ready + | WriteState::Writing(_) + | WriteState::Closing(_) + | WriteState::Error(_) => Ok(()), + WriteState::Closed => Err(StreamError::Closed), + } + } + + fn check_write(&mut self) -> Result { + match mem::replace(&mut self.state, WriteState::Closed) { + WriteState::Writing(task) => { + self.state = WriteState::Writing(task); + return Ok(0); + } + WriteState::Closing(task) => { + self.state = WriteState::Closing(task); + return Ok(0); + } + WriteState::Ready => { + self.state = WriteState::Ready; + } + WriteState::Closed => return Err(StreamError::Closed), + WriteState::Error(e) => return Err(StreamError::LastOperationFailed(e.into())), + } + + let writable = self.stream.writable(); + futures::pin_mut!(writable); + if crate::runtime::poll_noop(writable).is_none() { + return Ok(0); + } + Ok(SOCKET_READY_SIZE) + } + + fn shutdown(&mut self) { + self.state = match mem::replace(&mut self.state, WriteState::Closed) { + // No write in progress, immediately shut down: + WriteState::Ready => { + native_shutdown(&self.stream, Shutdown::Write); + WriteState::Closed + } + + // Schedule the shutdown after the current write has finished: + WriteState::Writing(write) => { + let stream = self.stream.clone(); + WriteState::Closing(crate::runtime::spawn(async move { + let result = write.await; + native_shutdown(&stream, Shutdown::Write); + result + })) + } + + s => s, + }; + } + + async fn cancel(&mut self) { + match mem::replace(&mut self.state, WriteState::Closed) { + WriteState::Writing(task) | WriteState::Closing(task) => _ = task.cancel().await, + _ => {} + } + } + + async fn ready(&mut self) { + match &mut self.state { + WriteState::Writing(task) => { + self.state = match task.await { + Ok(()) => WriteState::Ready, + Err(e) => WriteState::Error(e), + } + } + WriteState::Closing(task) => { + self.state = match task.await { + Ok(()) => WriteState::Closed, + Err(e) => WriteState::Error(e), + } + } + _ => {} + } + + if let WriteState::Ready = self.state { + self.stream.writable().await.unwrap(); + } + } +} + +struct TcpWriteStream(Arc>); + +#[async_trait::async_trait] +impl OutputStream for TcpWriteStream { + fn write(&mut self, bytes: bytes::Bytes) -> Result<(), StreamError> { + try_lock_for_stream(&self.0)?.write(bytes) + } + + fn flush(&mut self) -> Result<(), StreamError> { + try_lock_for_stream(&self.0)?.flush() + } + + fn check_write(&mut self) -> Result { + try_lock_for_stream(&self.0)?.check_write() + } + + async fn cancel(&mut self) { + self.0.lock().await.cancel().await + } +} + +#[async_trait::async_trait] +impl Pollable for TcpWriteStream { + async fn ready(&mut self) { + self.0.lock().await.ready().await + } +} + +fn native_shutdown(stream: &tokio::net::TcpStream, how: Shutdown) { + _ = stream + .as_socketlike_view::() + .shutdown(how); +} + +fn try_lock_for_stream(mutex: &Mutex) -> Result, StreamError> { + mutex + .try_lock() + .map_err(|_| StreamError::trap("concurrent access to resource not supported")) +} + +fn try_lock_for_socket(mutex: &Mutex) -> SocketResult> { + mutex.try_lock().map_err(|_| { + SocketError::trap(anyhow::anyhow!( + "concurrent access to resource not supported" + )) + }) +} + +struct LoopbackInputStream { + rx: Arc< + Mutex< + Option< + tokio::sync::mpsc::UnboundedReceiver<( + bytes::Bytes, + tokio::sync::OwnedSemaphorePermit, + )>, + >, + >, + >, + buffer: bytes::Bytes, +} + +#[async_trait::async_trait] +impl Pollable for LoopbackInputStream { + async fn ready(&mut self) { + if !self.buffer.is_empty() { + return; + } + + let mut rx = self.rx.lock().await; + let Some(rx) = rx.as_mut() else { + return; + }; + if let Some((buf, _permit)) = rx.recv().await { + self.buffer = buf; + }; + } +} + +impl InputStream for LoopbackInputStream { + fn read(&mut self, size: usize) -> Result { + use tokio::sync::mpsc::error::TryRecvError; + + let Ok(mut rx) = self.rx.try_lock() else { + return Err(StreamError::Closed); + }; + let Some(rx) = rx.as_mut() else { + return Err(StreamError::Closed); + }; + if rx.is_closed() && rx.is_empty() && self.buffer.is_empty() { + return Err(StreamError::Closed); + } + + if size == 0 { + return Ok(bytes::Bytes::default()); + } + if !self.buffer.is_empty() { + let n = self.buffer.len(); + return Ok(self.buffer.split_to(size.min(n))); + } + match rx.try_recv() { + Ok((buf, _permit)) => { + self.buffer = buf; + let n = self.buffer.len(); + Ok(self.buffer.split_to(size.min(n))) + } + Err(TryRecvError::Empty) => Ok(bytes::Bytes::default()), + Err(TryRecvError::Disconnected) => Err(StreamError::Closed), + } + } +} + +struct LoopbackOutputStream { + tx: Arc< + std::sync::Mutex< + Option< + tokio::sync::mpsc::UnboundedSender<( + bytes::Bytes, + tokio::sync::OwnedSemaphorePermit, + )>, + >, + >, + >, + permits: Arc, +} + +impl LoopbackOutputStream { + fn is_closed(&self) -> bool { + let tx = self.tx.lock().unwrap(); + tx.as_ref().map(|tx| tx.is_closed()).unwrap_or(true) + } +} + +#[async_trait::async_trait] +impl Pollable for LoopbackOutputStream { + async fn ready(&mut self) { + _ = self.permits.acquire().await + } +} + +impl OutputStream for LoopbackOutputStream { + fn write(&mut self, bytes: bytes::Bytes) -> Result<(), StreamError> { + let mut tx = self.tx.lock().unwrap(); + let Some(tx) = tx.as_mut() else { + return Err(StreamError::Closed); + }; + let Some(permit) = bytes.len().try_into().ok().and_then(|n| { + let permits = Arc::clone(&self.permits); + permits.try_acquire_many_owned(n).ok() + }) else { + return Err(StreamError::Trap(anyhow::anyhow!( + "write beyond capacity of LoopbackOutputStream" + ))); + }; + if let Err(..) = tx.send((bytes, permit)) { + Err(StreamError::Closed) + } else { + Ok(()) + } + } + + fn flush(&mut self) -> Result<(), StreamError> { + if self.is_closed() { + Err(StreamError::Closed) + } else { + Ok(()) + } + } + + fn check_write(&mut self) -> Result { + if self.is_closed() { + Err(StreamError::Closed) + } else { + Ok(self.permits.available_permits()) + } + } +} diff --git a/crates/wasi/src/p2/udp.rs b/crates/wasi/src/p2/udp.rs new file mode 100644 index 00000000..c06ecb47 --- /dev/null +++ b/crates/wasi/src/p2/udp.rs @@ -0,0 +1,124 @@ +use crate::sockets::{SocketAddrCheck, SocketAddressFamily}; +use std::net::SocketAddr; +use std::sync::Arc; + +pub struct NetworkIncomingDatagramStream { + pub(crate) inner: Arc, + + /// If this has a value, the stream is "connected". + pub(crate) remote_address: Option, +} + +pub struct NetworkOutgoingDatagramStream { + pub(crate) inner: Arc, + + /// If this has a value, the stream is "connected". + pub(crate) remote_address: Option, + + /// Socket address family. + pub(crate) family: SocketAddressFamily, + + pub(crate) send_state: SendState, + + /// The check of allowed addresses + pub(crate) socket_addr_check: Option, +} + +pub struct LoopbackIncomingDatagramStream { + pub remote_address: Option, + pub rx: Arc< + tokio::sync::Mutex< + tokio::sync::mpsc::UnboundedReceiver<( + crate::sockets::loopback::UdpDatagram, + tokio::sync::OwnedSemaphorePermit, + )>, + >, + >, + pub received: Option<( + crate::sockets::loopback::UdpDatagram, + tokio::sync::OwnedSemaphorePermit, + )>, +} + +impl LoopbackIncomingDatagramStream { + pub fn recv( + &mut self, + datagrams: &mut Vec, + max_results: usize, + ) -> Result<(), crate::sockets::util::ErrorCode> { + let Ok(mut rx) = self.rx.try_lock() else { + return Err(crate::sockets::util::ErrorCode::Unknown); + }; + + let mut rx = core::iter::chain( + self.received.take(), + core::iter::from_fn(|| rx.try_recv().ok()), + ); + while datagrams.len() < max_results { + let Some((dgram, _permit)) = rx.next() else { + break; + }; + match self.remote_address { + Some(connected_addr) if connected_addr != dgram.source_address => continue, + _ => datagrams.push(crate::p2::bindings::sockets::udp::IncomingDatagram { + data: dgram.data, + remote_address: dgram.source_address.into(), + }), + } + } + Ok(()) + } +} + +pub struct LoopbackOutgoingDatagramStream { + pub local_address: SocketAddr, + pub remote_address: Option, + pub(crate) family: SocketAddressFamily, + pub(crate) socket_addr_check: Option, + pub permits: Arc, + pub(crate) permit: Option, +} + +impl LoopbackOutgoingDatagramStream { + pub fn check_send(&mut self) -> bool { + if self.permit.is_some() { + return true; + }; + let Ok(p) = + Arc::clone(&self.permits).try_acquire_many_owned(self.permits.available_permits() as _) + else { + return false; + }; + self.permit = Some(p); + true + } +} + +pub enum IncomingDatagramStream { + Network(NetworkIncomingDatagramStream), + Loopback(LoopbackIncomingDatagramStream), + Unspecified { + net: NetworkIncomingDatagramStream, + lo: LoopbackIncomingDatagramStream, + }, +} + +pub enum OutgoingDatagramStream { + Network(NetworkOutgoingDatagramStream), + Loopback(LoopbackOutgoingDatagramStream), + Unspecified { + net: NetworkOutgoingDatagramStream, + lo: LoopbackOutgoingDatagramStream, + }, +} + +pub(crate) enum SendState { + /// Waiting for the API consumer to call `check-send`. + Idle, + + /// Ready to send up to x datagrams. + Permitted(usize), + + /// Waiting for the OS. + Waiting, +} diff --git a/crates/wasi/src/p2/wit/deps/cli/command.wit b/crates/wasi/src/p2/wit/deps/cli/command.wit new file mode 100644 index 00000000..6d3cc83f --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.6; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/crates/wasi/src/p2/wit/deps/cli/environment.wit b/crates/wasi/src/p2/wit/deps/cli/environment.wit new file mode 100644 index 00000000..2f449bd7 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.2.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.2.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.2.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.2.0) + initial-cwd: func() -> option; +} diff --git a/crates/wasi/src/p2/wit/deps/cli/exit.wit b/crates/wasi/src/p2/wit/deps/cli/exit.wit new file mode 100644 index 00000000..427935c8 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.2.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.2.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasi/src/p2/wit/deps/cli/imports.wit b/crates/wasi/src/p2/wit/deps/cli/imports.wit new file mode 100644 index 00000000..d9fd0171 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.6; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.6; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.6; + @since(version = 0.2.0) + include wasi:random/imports@0.2.6; + @since(version = 0.2.0) + include wasi:io/imports@0.2.6; + + @since(version = 0.2.0) + import environment; + @since(version = 0.2.0) + import exit; + @since(version = 0.2.0) + import stdin; + @since(version = 0.2.0) + import stdout; + @since(version = 0.2.0) + import stderr; + @since(version = 0.2.0) + import terminal-input; + @since(version = 0.2.0) + import terminal-output; + @since(version = 0.2.0) + import terminal-stdin; + @since(version = 0.2.0) + import terminal-stdout; + @since(version = 0.2.0) + import terminal-stderr; +} diff --git a/crates/wasi/src/p2/wit/deps/cli/run.wit b/crates/wasi/src/p2/wit/deps/cli/run.wit new file mode 100644 index 00000000..655346ef --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.2.0) +interface run { + /// Run the program. + @since(version = 0.2.0) + run: func() -> result; +} diff --git a/crates/wasi/src/p2/wit/deps/cli/stdio.wit b/crates/wasi/src/p2/wit/deps/cli/stdio.wit new file mode 100644 index 00000000..cb8aea2d --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/stdio.wit @@ -0,0 +1,26 @@ +@since(version = 0.2.0) +interface stdin { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream}; + + @since(version = 0.2.0) + get-stdin: func() -> input-stream; +} + +@since(version = 0.2.0) +interface stdout { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + + @since(version = 0.2.0) + get-stdout: func() -> output-stream; +} + +@since(version = 0.2.0) +interface stderr { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + + @since(version = 0.2.0) + get-stderr: func() -> output-stream; +} diff --git a/crates/wasi/src/p2/wit/deps/cli/terminal.wit b/crates/wasi/src/p2/wit/deps/cli/terminal.wit new file mode 100644 index 00000000..d305498c --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/cli/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.2.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.2.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.2.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.2.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdin { + @since(version = 0.2.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdout { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stderr { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasi/src/p2/wit/deps/clocks/monotonic-clock.wit b/crates/wasi/src/p2/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 00000000..f3bc8391 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,50 @@ +package wasi:clocks@0.2.6; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/crates/wasi/src/p2/wit/deps/clocks/timezone.wit b/crates/wasi/src/p2/wit/deps/clocks/timezone.wit new file mode 100644 index 00000000..ca98ad15 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.6; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/src/p2/wit/deps/clocks/wall-clock.wit b/crates/wasi/src/p2/wit/deps/clocks/wall-clock.wit new file mode 100644 index 00000000..76636a0c --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.6; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} diff --git a/crates/wasi/src/p2/wit/deps/clocks/world.wit b/crates/wasi/src/p2/wit/deps/clocks/world.wit new file mode 100644 index 00000000..5c53c51a --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import monotonic-clock; + @since(version = 0.2.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/src/p2/wit/deps/filesystem/preopens.wit b/crates/wasi/src/p2/wit/deps/filesystem/preopens.wit new file mode 100644 index 00000000..f2284794 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.2.6; + +@since(version = 0.2.0) +interface preopens { + @since(version = 0.2.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.2.0) + get-directories: func() -> list>; +} diff --git a/crates/wasi/src/p2/wit/deps/filesystem/types.wit b/crates/wasi/src/p2/wit/deps/filesystem/types.wit new file mode 100644 index 00000000..75c19044 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/filesystem/types.wit @@ -0,0 +1,676 @@ +package wasi:filesystem@0.2.6; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.6.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.2.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.2.0) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.2.0) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.2.0) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.2.0) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.2.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.2.0) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.2.0) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.2.0) + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + @since(version = 0.2.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + @since(version = 0.2.0) + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in POSIX. + @since(version = 0.2.0) + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.2.0) + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.2.0) + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.2.0) + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.2.0) + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + @since(version = 0.2.0) + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.2.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.2.0) + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.2.0) + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.2.0) + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.2.0) + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.2.0) + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.2.0) + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.2.0) + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.2.0) + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.2.0) + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.2.0) + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.2.0) + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + @since(version = 0.2.0) + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + @since(version = 0.2.0) + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/crates/wasi/src/p2/wit/deps/filesystem/world.wit b/crates/wasi/src/p2/wit/deps/filesystem/world.wit new file mode 100644 index 00000000..65597f9f --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import types; + @since(version = 0.2.0) + import preopens; +} diff --git a/crates/wasi/src/p2/wit/deps/io/error.wit b/crates/wasi/src/p2/wit/deps/io/error.wit new file mode 100644 index 00000000..784f74a5 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.6; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/crates/wasi/src/p2/wit/deps/io/poll.wit b/crates/wasi/src/p2/wit/deps/io/poll.wit new file mode 100644 index 00000000..7f711836 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.6; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/crates/wasi/src/p2/wit/deps/io/streams.wit b/crates/wasi/src/p2/wit/deps/io/streams.wit new file mode 100644 index 00000000..c5da38c8 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.6; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/crates/wasi/src/p2/wit/deps/io/world.wit b/crates/wasi/src/p2/wit/deps/io/world.wit new file mode 100644 index 00000000..84c85c08 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/crates/wasi/src/p2/wit/deps/random/insecure-seed.wit b/crates/wasi/src/p2/wit/deps/random/insecure-seed.wit new file mode 100644 index 00000000..d3dc03a6 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.2.6; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.2.0) + insecure-seed: func() -> tuple; +} diff --git a/crates/wasi/src/p2/wit/deps/random/insecure.wit b/crates/wasi/src/p2/wit/deps/random/insecure.wit new file mode 100644 index 00000000..d4d02848 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.6; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.2.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.2.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p2/wit/deps/random/random.wit b/crates/wasi/src/p2/wit/deps/random/random.wit new file mode 100644 index 00000000..a0ff9564 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.6; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.2.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.2.0) + get-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p2/wit/deps/random/world.wit b/crates/wasi/src/p2/wit/deps/random/world.wit new file mode 100644 index 00000000..099f47b3 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import random; + + @since(version = 0.2.0) + import insecure; + + @since(version = 0.2.0) + import insecure-seed; +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/instance-network.wit b/crates/wasi/src/p2/wit/deps/sockets/instance-network.wit new file mode 100644 index 00000000..5f6e6c1c --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/instance-network.wit @@ -0,0 +1,11 @@ + +/// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) +interface instance-network { + @since(version = 0.2.0) + use network.{network}; + + /// Get a handle to the default network. + @since(version = 0.2.0) + instance-network: func() -> network; +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/src/p2/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 00000000..ee6419e7 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,56 @@ +@since(version = 0.2.0) +interface ip-name-lookup { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-address}; + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + resolve-addresses: func(network: borrow, name: string) -> result; + + @since(version = 0.2.0) + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/network.wit b/crates/wasi/src/p2/wit/deps/sockets/network.wit new file mode 100644 index 00000000..6ca98b63 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/network.wit @@ -0,0 +1,169 @@ +@since(version = 0.2.0) +interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.6.{error}; + + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + /// Attempts to extract a network-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.2.0) + type ipv4-address = tuple; + @since(version = 0.2.0) + type ipv6-address = tuple; + + @since(version = 0.2.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.2.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.2.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.2.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi/src/p2/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 00000000..eedbd307 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface tcp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/tcp.wit b/crates/wasi/src/p2/wit/deps/sockets/tcp.wit new file mode 100644 index 00000000..beefd7b4 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/tcp.wit @@ -0,0 +1,387 @@ +@since(version = 0.2.0) +interface tcp { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + @since(version = 0.2.0) + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connected` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) + keep-alive-enabled: func() -> result; + @since(version = 0.2.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-interval: func() -> result; + @since(version = 0.2.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-count: func() -> result; + @since(version = 0.2.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + hop-limit: func() -> result; + @since(version = 0.2.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for more information. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent; shutting down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/udp-create-socket.wit b/crates/wasi/src/p2/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 00000000..e8eeacbf --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface udp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/udp.wit b/crates/wasi/src/p2/wit/deps/sockets/udp.wit new file mode 100644 index 00000000..9dbe6932 --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/udp.wit @@ -0,0 +1,288 @@ +@since(version = 0.2.0) +interface udp { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + @since(version = 0.2.0) + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + @since(version = 0.2.0) + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + /// A UDP socket handle. + @since(version = 0.2.0) + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + unicast-hop-limit: func() -> result; + @since(version = 0.2.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasi/src/p2/wit/deps/sockets/world.wit b/crates/wasi/src/p2/wit/deps/sockets/world.wit new file mode 100644 index 00000000..e86f02ce --- /dev/null +++ b/crates/wasi/src/p2/wit/deps/sockets/world.wit @@ -0,0 +1,19 @@ +package wasi:sockets@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import instance-network; + @since(version = 0.2.0) + import network; + @since(version = 0.2.0) + import udp; + @since(version = 0.2.0) + import udp-create-socket; + @since(version = 0.2.0) + import tcp; + @since(version = 0.2.0) + import tcp-create-socket; + @since(version = 0.2.0) + import ip-name-lookup; +} diff --git a/crates/wasi/src/p2/wit/test.wit b/crates/wasi/src/p2/wit/test.wit new file mode 100644 index 00000000..d0d29618 --- /dev/null +++ b/crates/wasi/src/p2/wit/test.wit @@ -0,0 +1,13 @@ +world test-reactor { + include wasi:cli/imports@0.2.6; + + export add-strings: func(s: list) -> u32; + export get-strings: func() -> list; + + use wasi:io/streams@0.2.6.{output-stream}; + + export write-strings-to: func(o: output-stream) -> result; + + use wasi:filesystem/types@0.2.6.{descriptor-stat}; + export pass-an-imported-record: func(d: descriptor-stat) -> string; +} diff --git a/crates/wasi/src/p2/wit/world.wit b/crates/wasi/src/p2/wit/world.wit new file mode 100644 index 00000000..c322caee --- /dev/null +++ b/crates/wasi/src/p2/wit/world.wit @@ -0,0 +1,6 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi; + +world bindings { + include wasi:cli/imports@0.2.6; +} diff --git a/crates/wasi/src/p2/write_stream.rs b/crates/wasi/src/p2/write_stream.rs new file mode 100644 index 00000000..e29d3435 --- /dev/null +++ b/crates/wasi/src/p2/write_stream.rs @@ -0,0 +1,213 @@ +use crate::p2::{OutputStream, Pollable, StreamError}; +use anyhow::anyhow; +use bytes::Bytes; +use std::pin::pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; + +#[derive(Debug)] +struct WorkerState { + alive: bool, + items: std::collections::VecDeque, + write_budget: usize, + flush_pending: bool, + error: Option, + write_ready_changed: Option, +} + +impl WorkerState { + fn check_error(&mut self) -> Result<(), StreamError> { + if let Some(e) = self.error.take() { + return Err(StreamError::LastOperationFailed(e)); + } + if !self.alive { + return Err(StreamError::Closed); + } + Ok(()) + } +} + +struct Worker { + state: Mutex, + new_work: tokio::sync::Notify, +} + +enum Job { + Flush, + Write(Bytes), +} + +impl Worker { + fn new(write_budget: usize) -> Self { + Self { + state: Mutex::new(WorkerState { + alive: true, + items: std::collections::VecDeque::new(), + write_budget, + flush_pending: false, + error: None, + write_ready_changed: None, + }), + new_work: tokio::sync::Notify::new(), + } + } + fn check_write(&self) -> Result { + let mut state = self.state(); + if let Err(e) = state.check_error() { + return Err(e); + } + + if state.flush_pending || state.write_budget == 0 { + return Ok(0); + } + + Ok(state.write_budget) + } + fn state(&self) -> std::sync::MutexGuard<'_, WorkerState> { + self.state.lock().unwrap() + } + fn pop(&self) -> Option { + let mut state = self.state(); + if state.items.is_empty() { + if state.flush_pending { + return Some(Job::Flush); + } + } else if let Some(bytes) = state.items.pop_front() { + return Some(Job::Write(bytes)); + } + + None + } + fn report_error(&self, e: std::io::Error) { + let waker = { + let mut state = self.state(); + state.alive = false; + state.error = Some(e.into()); + state.flush_pending = false; + state.write_ready_changed.take() + }; + if let Some(waker) = waker { + waker.wake(); + } + } + async fn work(&self, writer: T) { + use tokio::io::AsyncWriteExt; + let mut writer = pin!(writer); + loop { + while let Some(job) = self.pop() { + match job { + Job::Flush => { + if let Err(e) = writer.flush().await { + self.report_error(e); + return; + } + + tracing::debug!("worker marking flush complete"); + self.state().flush_pending = false; + } + + Job::Write(mut bytes) => { + tracing::debug!("worker writing: {bytes:?}"); + let len = bytes.len(); + match writer.write_all_buf(&mut bytes).await { + Err(e) => { + self.report_error(e); + return; + } + Ok(_) => { + self.state().write_budget += len; + } + } + } + } + + let waker = self.state().write_ready_changed.take(); + if let Some(waker) = waker { + waker.wake(); + } + } + self.new_work.notified().await; + } + } +} + +/// Provides a [`OutputStream`] impl from a [`tokio::io::AsyncWrite`] impl +pub struct AsyncWriteStream { + worker: Arc, + join_handle: Option>, +} + +impl AsyncWriteStream { + /// Create a [`AsyncWriteStream`]. In order to use the [`OutputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncWrite`]. + pub fn new(write_budget: usize, writer: T) -> Self { + let worker = Arc::new(Worker::new(write_budget)); + + let w = Arc::clone(&worker); + let join_handle = crate::runtime::spawn(async move { w.work(writer).await }); + + AsyncWriteStream { + worker, + join_handle: Some(join_handle), + } + } + + pub(crate) fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { + let mut state = self.worker.state(); + if state.error.is_some() || !state.alive || (!state.flush_pending && state.write_budget > 0) + { + return Poll::Ready(()); + } + state.write_ready_changed = Some(cx.waker().clone()); + Poll::Pending + } +} + +#[async_trait::async_trait] +impl OutputStream for AsyncWriteStream { + fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> { + let mut state = self.worker.state(); + state.check_error()?; + if state.flush_pending { + return Err(StreamError::Trap(anyhow!( + "write not permitted while flush pending" + ))); + } + match state.write_budget.checked_sub(bytes.len()) { + Some(remaining_budget) => { + state.write_budget = remaining_budget; + state.items.push_back(bytes); + } + None => return Err(StreamError::Trap(anyhow!("write exceeded budget"))), + } + drop(state); + self.worker.new_work.notify_one(); + Ok(()) + } + fn flush(&mut self) -> Result<(), StreamError> { + let mut state = self.worker.state(); + state.check_error()?; + + state.flush_pending = true; + self.worker.new_work.notify_one(); + + Ok(()) + } + + fn check_write(&mut self) -> Result { + self.worker.check_write() + } + + async fn cancel(&mut self) { + match self.join_handle.take() { + Some(task) => _ = task.cancel().await, + None => {} + } + } +} +#[async_trait::async_trait] +impl Pollable for AsyncWriteStream { + async fn ready(&mut self) { + std::future::poll_fn(|cx| self.poll_ready(cx)).await + } +} diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs new file mode 100644 index 00000000..a74520f0 --- /dev/null +++ b/crates/wasi/src/p3/bindings.rs @@ -0,0 +1,267 @@ +//! Auto-generated bindings for WASI interfaces. +//! +//! This module contains the output of the [`bindgen!`] macro when run over +//! the `wasi:cli/imports` world. +//! +//! [`bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! +//! # Examples +//! +//! If you have a WIT world which refers to WASI interfaces you probably want to +//! use this modules's bindings rather than generate fresh bindings. That can be +//! done using the `with` option to [`bindgen!`]: +//! +//! ```rust +//! use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! use wasmtime::{Result, Engine, Config}; +//! use wasmtime::component::{Linker, HasSelf, ResourceTable}; +//! +//! wasmtime::component::bindgen!({ +//! inline: " +//! package example:wasi; +//! +//! // An example of extending the `wasi:cli/command` world with a +//! // custom host interface. +//! world my-world { +//! include wasi:cli/command@0.3.0-rc-2025-09-16; +//! +//! import custom-host; +//! } +//! +//! interface custom-host { +//! my-custom-function: func(); +//! } +//! ", +//! path: "src/p3/wit", +//! with: { +//! "wasi": wash_wasi::p3::bindings, +//! }, +//! require_store_data_send: true, +//! }); +//! +//! struct MyState { +//! ctx: WasiCtx, +//! table: ResourceTable, +//! } +//! +//! impl example::wasi::custom_host::Host for MyState { +//! fn my_custom_function(&mut self) { +//! // .. +//! } +//! } +//! +//! impl WasiView for MyState { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView{ +//! ctx: &mut self.ctx, +//! table: &mut self.table, +//! } +//! } +//! } +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.async_support(true); +//! config.wasm_component_model_async(true); +//! let engine = Engine::new(&config)?; +//! let mut linker: Linker = Linker::new(&engine); +//! wash_wasi::p3::add_to_linker(&mut linker)?; +//! example::wasi::custom_host::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state)?; +//! +//! // .. use `Linker` to instantiate component ... +//! +//! Ok(()) +//! } +//! ``` + +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:cli/command", + imports: { + "wasi:cli/stdin": async | store | tracing | trappable, + "wasi:cli/stdout": async | store | tracing | trappable, + "wasi:cli/stderr": async | store | tracing | trappable, + "wasi:filesystem/types/[method]descriptor.read-via-stream": async | store | tracing | trappable, + "wasi:sockets/types/[method]tcp-socket.bind": async | store | tracing | trappable, + "wasi:sockets/types/[method]tcp-socket.listen": async | store | tracing | trappable, + "wasi:sockets/types/[method]tcp-socket.receive": async | store | tracing | trappable, + "wasi:sockets/types/[method]udp-socket.bind": async | store | tracing | trappable, + "wasi:sockets/types/[method]udp-socket.connect": async | store | tracing | trappable, + default: tracing | trappable, + }, + exports: { default: async | store }, + with: { + "wasi:cli/terminal-input/terminal-input": crate::p3::cli::TerminalInput, + "wasi:cli/terminal-output/terminal-output": crate::p3::cli::TerminalOutput, + "wasi:filesystem/types/descriptor": crate::filesystem::Descriptor, + "wasi:sockets/types/tcp-socket": crate::sockets::TcpSocket, + "wasi:sockets/types/udp-socket": crate::sockets::UdpSocket, + }, + trappable_error_type: { + "wasi:filesystem/types/error-code" => crate::p3::filesystem::FilesystemError, + "wasi:sockets/types/error-code" => crate::p3::sockets::SocketError, + }, + }); +} +pub use self::generated::LinkOptions; +pub use self::generated::exports; +pub use self::generated::wasi::*; + +/// Bindings to execute and run a `wasi:cli/command`. +/// +/// This structure is automatically generated by `bindgen!`. +/// +/// This can be used for a more "typed" view of executing a command component +/// through the [`Command::wasi_cli_run`] method plus +/// [`Guest::call_run`](exports::wasi::cli::run::Guest::call_run). +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Component, Linker, ResourceTable}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wash_wasi::p3::bindings::Command; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::add_to_linker(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtx::builder(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::default(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let instance = linker.instantiate_async(&mut store, &component).await?; +/// let command = Command::new(&mut store, &instance)?; +/// let program_result = instance.run_concurrent(&mut store, async move |store| { +/// command.wasi_cli_run().call_run(store).await +/// }).await??; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +/// +/// --- +pub use self::generated::Command; + +/// Pre-instantiated analog of [`Command`] +/// +/// This can be used to front-load work such as export lookup before +/// instantiation. +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, Component, ResourceTable}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wash_wasi::p3::bindings::CommandPre; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::add_to_linker(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// let pre = CommandPre::new(linker.instantiate_pre(&component)?)?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtx::builder(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// table: ResourceTable::default(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = pre.instantiate_async(&mut store).await?; +/// // TODO: Construct an accessor from `store` to call `run` +/// // https://github.com/bytecodealliance/wasmtime/issues/11249 +/// //let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// let program_result = todo!(); +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +/// +/// --- +// TODO: Make this public, once `CommandPre` can be used for +// calling exports +// https://github.com/bytecodealliance/wasmtime/issues/11249 +#[doc(hidden)] +pub use self::generated::CommandPre; + +pub use self::generated::CommandIndices; diff --git a/crates/wasi/src/p3/cli/host.rs b/crates/wasi/src/p3/cli/host.rs new file mode 100644 index 00000000..f18e3c14 --- /dev/null +++ b/crates/wasi/src/p3/cli/host.rs @@ -0,0 +1,300 @@ +use crate::I32Exit; +use crate::cli::{IsTerminal, WasiCli, WasiCliCtxView}; +use crate::p3::DEFAULT_BUFFER_CAPACITY; +use crate::p3::bindings::cli::types::ErrorCode; +use crate::p3::bindings::cli::{ + environment, exit, stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, + terminal_stdin, terminal_stdout, +}; +use crate::p3::cli::{TerminalInput, TerminalOutput}; +use anyhow::{Context as _, anyhow}; +use bytes::BytesMut; +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::io::{self, Cursor}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::sync::oneshot; +use wasmtime::component::{ + Accessor, Destination, FutureReader, Resource, Source, StreamConsumer, StreamProducer, + StreamReader, StreamResult, +}; +use wasmtime::{AsContextMut as _, StoreContextMut}; + +struct InputStreamProducer { + rx: Pin>, + result_tx: Option>, +} + +fn io_error_to_error_code(err: io::Error) -> ErrorCode { + match err.kind() { + io::ErrorKind::BrokenPipe => ErrorCode::Pipe, + other => { + tracing::warn!("stdio error: {other}"); + ErrorCode::Io + } + } +} + +impl StreamProducer for InputStreamProducer { + type Item = u8; + type Buffer = Cursor; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut store: StoreContextMut<'a, D>, + dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + // If the destination buffer is empty then this is a request on + // behalf of the guest to wait for this input stream to be readable. + // The `AsyncRead` trait abstraction does not provide the ability to + // await this event so we're forced to basically just lie here and + // say we're ready read data later. + // + // See WebAssembly/component-model#561 for some more information. + if dst.remaining(store.as_context_mut()) == Some(0) { + return Poll::Ready(Ok(StreamResult::Completed)); + } + + let mut dst = dst.as_direct(store, DEFAULT_BUFFER_CAPACITY); + let mut buf = ReadBuf::new(dst.remaining()); + match self.rx.as_mut().poll_read(cx, &mut buf) { + Poll::Ready(Ok(())) if buf.filled().is_empty() => { + Poll::Ready(Ok(StreamResult::Dropped)) + } + Poll::Ready(Ok(())) => { + let n = buf.filled().len(); + dst.mark_written(n); + Poll::Ready(Ok(StreamResult::Completed)) + } + Poll::Ready(Err(e)) => { + let _ = self + .result_tx + .take() + .unwrap() + .send(io_error_to_error_code(e)); + Poll::Ready(Ok(StreamResult::Dropped)) + } + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +struct OutputStreamConsumer { + tx: Pin>, + result_tx: Option>, +} + +impl StreamConsumer for OutputStreamConsumer { + type Item = u8; + + fn poll_consume( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + finish: bool, + ) -> Poll> { + let mut src = src.as_direct(store); + let buf = src.remaining(); + + // If the source buffer is empty then this is a request on behalf of + // the guest to wait for this output stream to be writable. The + // `AsyncWrite` trait abstraction does not provide the ability to await + // this event so we're forced to basically just lie here and say we're + // ready write data later. + // + // See WebAssembly/component-model#561 for some more information. + if buf.len() == 0 { + return Poll::Ready(Ok(StreamResult::Completed)); + } + match self.tx.as_mut().poll_write(cx, buf) { + Poll::Ready(Ok(n)) => { + src.mark_read(n); + Poll::Ready(Ok(StreamResult::Completed)) + } + Poll::Ready(Err(e)) => { + let _ = self + .result_tx + .take() + .unwrap() + .send(io_error_to_error_code(e)); + Poll::Ready(Ok(StreamResult::Dropped)) + } + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +impl terminal_input::Host for WasiCliCtxView<'_> {} +impl terminal_output::Host for WasiCliCtxView<'_> {} + +impl terminal_input::HostTerminalInput for WasiCliCtxView<'_> { + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.table + .delete(rep) + .context("failed to delete terminal input resource from table")?; + Ok(()) + } +} + +impl terminal_output::HostTerminalOutput for WasiCliCtxView<'_> { + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.table + .delete(rep) + .context("failed to delete terminal output resource from table")?; + Ok(()) + } +} + +impl terminal_stdin::Host for WasiCliCtxView<'_> { + fn get_terminal_stdin(&mut self) -> wasmtime::Result>> { + if self.ctx.stdin.is_terminal() { + let fd = self + .table + .push(TerminalInput) + .context("failed to push terminal stdin resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl terminal_stdout::Host for WasiCliCtxView<'_> { + fn get_terminal_stdout(&mut self) -> wasmtime::Result>> { + if self.ctx.stdout.is_terminal() { + let fd = self + .table + .push(TerminalOutput) + .context("failed to push terminal stdout resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl terminal_stderr::Host for WasiCliCtxView<'_> { + fn get_terminal_stderr(&mut self) -> wasmtime::Result>> { + if self.ctx.stderr.is_terminal() { + let fd = self + .table + .push(TerminalOutput) + .context("failed to push terminal stderr resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl stdin::HostWithStore for WasiCli { + async fn read_via_stream( + store: &Accessor, + ) -> wasmtime::Result<(StreamReader, FutureReader>)> { + let instance = store.instance(); + store.with(|mut store| { + let rx = store.get().ctx.stdin.async_stream(); + let (result_tx, result_rx) = oneshot::channel(); + let stream = StreamReader::new( + instance, + &mut store, + InputStreamProducer { + rx: Box::into_pin(rx), + result_tx: Some(result_tx), + }, + ); + let future = FutureReader::new(instance, &mut store, async { + anyhow::Ok(match result_rx.await { + Ok(err) => Err(err), + Err(_) => Ok(()), + }) + }); + Ok((stream, future)) + }) + } +} + +impl stdin::Host for WasiCliCtxView<'_> {} + +impl stdout::HostWithStore for WasiCli { + async fn write_via_stream( + store: &Accessor, + data: StreamReader, + ) -> wasmtime::Result> { + let (result_tx, result_rx) = oneshot::channel(); + store.with(|mut store| { + let tx = store.get().ctx.stdout.async_stream(); + data.pipe( + store, + OutputStreamConsumer { + tx: Box::into_pin(tx), + result_tx: Some(result_tx), + }, + ); + }); + Ok(match result_rx.await { + Ok(err) => Err(err), + Err(_) => Ok(()), + }) + } +} + +impl stdout::Host for WasiCliCtxView<'_> {} + +impl stderr::HostWithStore for WasiCli { + async fn write_via_stream( + store: &Accessor, + data: StreamReader, + ) -> wasmtime::Result> { + let (result_tx, result_rx) = oneshot::channel(); + store.with(|mut store| { + let tx = store.get().ctx.stderr.async_stream(); + data.pipe( + store, + OutputStreamConsumer { + tx: Box::into_pin(tx), + result_tx: Some(result_tx), + }, + ); + }); + Ok(match result_rx.await { + Ok(err) => Err(err), + Err(_) => Ok(()), + }) + } +} + +impl stderr::Host for WasiCliCtxView<'_> {} + +impl environment::Host for WasiCliCtxView<'_> { + fn get_environment(&mut self) -> wasmtime::Result> { + Ok(self.ctx.environment.clone()) + } + + fn get_arguments(&mut self) -> wasmtime::Result> { + Ok(self.ctx.arguments.clone()) + } + + fn get_initial_cwd(&mut self) -> wasmtime::Result> { + Ok(self.ctx.initial_cwd.clone()) + } +} + +impl exit::Host for WasiCliCtxView<'_> { + fn exit(&mut self, status: Result<(), ()>) -> wasmtime::Result<()> { + let status = match status { + Ok(()) => 0, + Err(()) => 1, + }; + Err(anyhow!(I32Exit(status))) + } + + fn exit_with_code(&mut self, status_code: u8) -> wasmtime::Result<()> { + Err(anyhow!(I32Exit(status_code.into()))) + } +} diff --git a/crates/wasi/src/p3/cli/mod.rs b/crates/wasi/src/p3/cli/mod.rs new file mode 100644 index 00000000..c646244d --- /dev/null +++ b/crates/wasi/src/p3/cli/mod.rs @@ -0,0 +1,92 @@ +mod host; + +use crate::cli::{WasiCli, WasiCliView}; +use crate::p3::bindings::cli::{ + environment, exit, stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, + terminal_stdin, terminal_stdout, +}; +use wasmtime::component::Linker; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this module. +/// +/// This is low-level API for advanced use cases, +/// [`wash_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wash_wasi::cli::{WasiCliCtx, WasiCliView, WasiCliCtxView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::cli::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// cli: WasiCliCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiCliView for MyState { +/// fn cli(&mut self) -> WasiCliCtxView<'_> { +/// WasiCliCtxView { +/// ctx: &mut self.cli, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiCliView + 'static, +{ + let exit_options = exit::LinkOptions::default(); + add_to_linker_with_options(linker, &exit_options) +} + +/// Similar to [`add_to_linker`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options( + linker: &mut Linker, + exit_options: &exit::LinkOptions, +) -> anyhow::Result<()> +where + T: WasiCliView + 'static, +{ + exit::add_to_linker::<_, WasiCli>(linker, exit_options, T::cli)?; + environment::add_to_linker::<_, WasiCli>(linker, T::cli)?; + stdin::add_to_linker::<_, WasiCli>(linker, T::cli)?; + stdout::add_to_linker::<_, WasiCli>(linker, T::cli)?; + stderr::add_to_linker::<_, WasiCli>(linker, T::cli)?; + terminal_input::add_to_linker::<_, WasiCli>(linker, T::cli)?; + terminal_output::add_to_linker::<_, WasiCli>(linker, T::cli)?; + terminal_stdin::add_to_linker::<_, WasiCli>(linker, T::cli)?; + terminal_stdout::add_to_linker::<_, WasiCli>(linker, T::cli)?; + terminal_stderr::add_to_linker::<_, WasiCli>(linker, T::cli)?; + Ok(()) +} + +pub struct TerminalInput; +pub struct TerminalOutput; diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs new file mode 100644 index 00000000..e374c8a1 --- /dev/null +++ b/crates/wasi/src/p3/clocks/host.rs @@ -0,0 +1,57 @@ +use crate::clocks::WasiClocksCtxView; +use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; +use crate::p3::clocks::WasiClocks; +use core::time::Duration; +use tokio::time::sleep; +use wasmtime::component::Accessor; + +impl wall_clock::Host for WasiClocksCtxView<'_> { + fn now(&mut self) -> wasmtime::Result { + let now = self.ctx.wall_clock.now(); + Ok(wall_clock::Datetime { + seconds: now.as_secs(), + nanoseconds: now.subsec_nanos(), + }) + } + + fn get_resolution(&mut self) -> wasmtime::Result { + let res = self.ctx.wall_clock.resolution(); + Ok(wall_clock::Datetime { + seconds: res.as_secs(), + nanoseconds: res.subsec_nanos(), + }) + } +} + +impl monotonic_clock::HostWithStore for WasiClocks { + async fn wait_until( + store: &Accessor, + when: monotonic_clock::Instant, + ) -> wasmtime::Result<()> { + let clock_now = store.with(|mut view| view.get().ctx.monotonic_clock.now()); + if when > clock_now { + sleep(Duration::from_nanos(when - clock_now)).await; + }; + Ok(()) + } + + async fn wait_for( + _store: &Accessor, + duration: monotonic_clock::Duration, + ) -> wasmtime::Result<()> { + if duration > 0 { + sleep(Duration::from_nanos(duration)).await; + } + Ok(()) + } +} + +impl monotonic_clock::Host for WasiClocksCtxView<'_> { + fn now(&mut self) -> wasmtime::Result { + Ok(self.ctx.monotonic_clock.now()) + } + + fn get_resolution(&mut self) -> wasmtime::Result { + Ok(self.ctx.monotonic_clock.resolution()) + } +} diff --git a/crates/wasi/src/p3/clocks/mod.rs b/crates/wasi/src/p3/clocks/mod.rs new file mode 100644 index 00000000..a773d3d4 --- /dev/null +++ b/crates/wasi/src/p3/clocks/mod.rs @@ -0,0 +1,101 @@ +mod host; + +use crate::clocks::{WasiClocks, WasiClocksView}; +use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; +use cap_std::time::SystemTime; +use wasmtime::component::Linker; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:clocks/imports` world supported by +/// this module. +/// +/// This is low-level API for advanced use cases, +/// [`wash_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wash_wasi::clocks::{WasiClocksView, WasiClocksCtxView, WasiClocksCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::clocks::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// clocks: WasiClocksCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiClocksView for MyState { +/// fn clocks(&mut self) -> WasiClocksCtxView { +/// WasiClocksCtxView { ctx: &mut self.clocks, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiClocksView + 'static, +{ + monotonic_clock::add_to_linker::<_, WasiClocks>(linker, T::clocks)?; + wall_clock::add_to_linker::<_, WasiClocks>(linker, T::clocks)?; + Ok(()) +} + +impl From for wall_clock::Datetime { + fn from( + crate::clocks::Datetime { + seconds, + nanoseconds, + }: crate::clocks::Datetime, + ) -> Self { + Self { + seconds, + nanoseconds, + } + } +} + +impl From for crate::clocks::Datetime { + fn from( + wall_clock::Datetime { + seconds, + nanoseconds, + }: wall_clock::Datetime, + ) -> Self { + Self { + seconds, + nanoseconds, + } + } +} + +impl TryFrom for wall_clock::Datetime { + type Error = wasmtime::Error; + + fn try_from(time: SystemTime) -> Result { + let time = crate::clocks::Datetime::try_from(time)?; + Ok(time.into()) + } +} diff --git a/crates/wasi/src/p3/filesystem/host.rs b/crates/wasi/src/p3/filesystem/host.rs new file mode 100644 index 00000000..df94cdef --- /dev/null +++ b/crates/wasi/src/p3/filesystem/host.rs @@ -0,0 +1,867 @@ +use crate::filesystem::{Descriptor, Dir, File, WasiFilesystem, WasiFilesystemCtxView}; +use crate::p3::bindings::clocks::wall_clock; +use crate::p3::bindings::filesystem::types::{ + self, Advice, DescriptorFlags, DescriptorStat, DescriptorType, DirectoryEntry, ErrorCode, + Filesize, MetadataHashValue, NewTimestamp, OpenFlags, PathFlags, +}; +use crate::p3::filesystem::{FilesystemError, FilesystemResult, preopens}; +use crate::p3::{DEFAULT_BUFFER_CAPACITY, FallibleIteratorProducer}; +use crate::{DirPerms, FilePerms}; +use anyhow::Context as _; +use bytes::BytesMut; +use core::pin::Pin; +use core::task::{Context, Poll, ready}; +use core::{iter, mem}; +use std::io::{self, Cursor}; +use std::sync::Arc; +use system_interface::fs::FileIoExt as _; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::{JoinHandle, spawn_blocking}; +use wasmtime::StoreContextMut; +use wasmtime::component::{ + Accessor, Destination, FutureReader, Resource, ResourceTable, Source, StreamConsumer, + StreamProducer, StreamReader, StreamResult, +}; + +fn get_descriptor<'a>( + table: &'a ResourceTable, + fd: &'a Resource, +) -> FilesystemResult<&'a Descriptor> { + table + .get(fd) + .context("failed to get descriptor resource from table") + .map_err(FilesystemError::trap) +} + +fn get_file<'a>( + table: &'a ResourceTable, + fd: &'a Resource, +) -> FilesystemResult<&'a File> { + let file = get_descriptor(table, fd).map(Descriptor::file)??; + Ok(file) +} + +fn get_dir<'a>( + table: &'a ResourceTable, + fd: &'a Resource, +) -> FilesystemResult<&'a Dir> { + let dir = get_descriptor(table, fd).map(Descriptor::dir)??; + Ok(dir) +} + +trait AccessorExt { + fn get_descriptor(&self, fd: &Resource) -> FilesystemResult; + fn get_file(&self, fd: &Resource) -> FilesystemResult; + fn get_dir(&self, fd: &Resource) -> FilesystemResult; + fn get_dir_pair( + &self, + a: &Resource, + b: &Resource, + ) -> FilesystemResult<(Dir, Dir)>; +} + +impl AccessorExt for Accessor { + fn get_descriptor(&self, fd: &Resource) -> FilesystemResult { + self.with(|mut store| { + let fd = get_descriptor(store.get().table, fd)?; + Ok(fd.clone()) + }) + } + + fn get_file(&self, fd: &Resource) -> FilesystemResult { + self.with(|mut store| { + let file = get_file(store.get().table, fd)?; + Ok(file.clone()) + }) + } + + fn get_dir(&self, fd: &Resource) -> FilesystemResult { + self.with(|mut store| { + let dir = get_dir(store.get().table, fd)?; + Ok(dir.clone()) + }) + } + + fn get_dir_pair( + &self, + a: &Resource, + b: &Resource, + ) -> FilesystemResult<(Dir, Dir)> { + self.with(|mut store| { + let table = store.get().table; + let a = get_dir(table, a)?; + let b = get_dir(table, b)?; + Ok((a.clone(), b.clone())) + }) + } +} + +fn systemtime_from(t: wall_clock::Datetime) -> Result { + std::time::SystemTime::UNIX_EPOCH + .checked_add(core::time::Duration::new(t.seconds, t.nanoseconds)) + .ok_or(ErrorCode::Overflow) +} + +fn systemtimespec_from(t: NewTimestamp) -> Result, ErrorCode> { + use fs_set_times::SystemTimeSpec; + match t { + NewTimestamp::NoChange => Ok(None), + NewTimestamp::Now => Ok(Some(SystemTimeSpec::SymbolicNow)), + NewTimestamp::Timestamp(st) => { + let st = systemtime_from(st)?; + Ok(Some(SystemTimeSpec::Absolute(st))) + } + } +} + +struct ReadStreamProducer { + file: File, + offset: u64, + result: Option>>, + task: Option>>, +} + +impl Drop for ReadStreamProducer { + fn drop(&mut self) { + self.close(Ok(())) + } +} + +impl ReadStreamProducer { + fn close(&mut self, res: Result<(), ErrorCode>) { + if let Some(tx) = self.result.take() { + _ = tx.send(res); + } + } + + /// Update the internal `offset` field after reading `amt` bytes from the file. + fn complete_read(&mut self, amt: usize) -> StreamResult { + let Ok(amt) = amt.try_into() else { + self.close(Err(ErrorCode::Overflow)); + return StreamResult::Dropped; + }; + let Some(amt) = self.offset.checked_add(amt) else { + self.close(Err(ErrorCode::Overflow)); + return StreamResult::Dropped; + }; + self.offset = amt; + StreamResult::Completed + } +} + +impl StreamProducer for ReadStreamProducer { + type Item = u8; + type Buffer = Cursor; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut<'a, D>, + mut dst: Destination<'a, Self::Item, Self::Buffer>, + // Intentionally ignore this as in blocking mode everything is always + // ready and otherwise spawned blocking work can't be cancelled. + _finish: bool, + ) -> Poll> { + if let Some(file) = self.file.as_blocking_file() { + // Once a blocking file, always a blocking file, so assert as such. + assert!(self.task.is_none()); + let mut dst = dst.as_direct(store, DEFAULT_BUFFER_CAPACITY); + let buf = dst.remaining(); + if buf.is_empty() { + return Poll::Ready(Ok(StreamResult::Completed)); + } + return match file.read_at(buf, self.offset) { + Ok(0) => { + self.close(Ok(())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + Ok(n) => { + dst.mark_written(n); + Poll::Ready(Ok(self.complete_read(n))) + } + Err(err) => { + self.close(Err(err.into())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + }; + } + + // Lazily spawn a read task if one hasn't already been spawned yet. + let me = &mut *self; + let task = me.task.get_or_insert_with(|| { + let mut buf = dst.take_buffer().into_inner(); + buf.resize(DEFAULT_BUFFER_CAPACITY, 0); + let file = Arc::clone(me.file.as_file()); + let offset = me.offset; + spawn_blocking(move || { + file.read_at(&mut buf, offset).map(|n| { + buf.truncate(n); + buf + }) + }) + }); + + // Await the completion of the read task. Note that this is not a + // cancellable await point because we can't cancel the other task, so + // the `finish` parameter is ignored. + let res = ready!(Pin::new(task).poll(cx)).expect("I/O task should not panic"); + self.task = None; + match res { + Ok(buf) if buf.is_empty() => { + self.close(Ok(())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + Ok(buf) => { + let n = buf.len(); + dst.set_buffer(Cursor::new(buf)); + Poll::Ready(Ok(self.complete_read(n))) + } + Err(err) => { + self.close(Err(err.into())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + } + } +} + +fn map_dir_entry( + entry: std::io::Result, +) -> Result, ErrorCode> { + match entry { + Ok(entry) => { + let meta = entry.metadata()?; + let Ok(name) = entry.file_name().into_string() else { + return Err(ErrorCode::IllegalByteSequence); + }; + Ok(Some(DirectoryEntry { + type_: meta.file_type().into(), + name, + })) + } + Err(err) => { + // On windows, filter out files like `C:\DumpStack.log.tmp` which we + // can't get full metadata for. + #[cfg(windows)] + { + use windows_sys::Win32::Foundation::{ + ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, + }; + if err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) + || err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) + { + return Ok(None); + } + } + Err(err.into()) + } + } +} + +struct ReadDirStream { + rx: mpsc::Receiver, + task: JoinHandle>, + result: Option>>, +} + +impl ReadDirStream { + fn new( + dir: Arc, + result: oneshot::Sender>, + ) -> ReadDirStream { + let (tx, rx) = mpsc::channel(1); + ReadDirStream { + task: spawn_blocking(move || { + let entries = dir.entries()?; + for entry in entries { + if let Some(entry) = map_dir_entry(entry)? { + if let Err(_) = tx.blocking_send(entry) { + break; + } + } + } + Ok(()) + }), + rx, + result: Some(result), + } + } + + fn close(&mut self, res: Result<(), ErrorCode>) { + self.rx.close(); + self.task.abort(); + let _ = self.result.take().unwrap().send(res); + } +} + +impl StreamProducer for ReadDirStream { + type Item = DirectoryEntry; + type Buffer = Option; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut store: StoreContextMut<'a, D>, + mut dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + // If this is a 0-length read then `mpsc::Receiver` does not expose an + // API to wait for an item to be available without taking it out of the + // channel. In lieu of that just say that we're complete and ready for a + // read. + if dst.remaining(&mut store) == Some(0) { + return Poll::Ready(Ok(StreamResult::Completed)); + } + + match self.rx.poll_recv(cx) { + // If an item is on the channel then send that along and say that + // the read is now complete with one item being yielded. + Poll::Ready(Some(item)) => { + dst.set_buffer(Some(item)); + Poll::Ready(Ok(StreamResult::Completed)) + } + + // If there's nothing left on the channel then that means that an + // error occurred or the iterator is done. In both cases an + // un-cancellable wait for the spawned task is entered and we await + // its completion. Upon completion there our own stream is closed + // with the result (sending an error code on our oneshot) and then + // the stream is reported as dropped. + Poll::Ready(None) => { + let result = ready!(Pin::new(&mut self.task).poll(cx)) + .expect("spawned task should not panic"); + self.close(result); + Poll::Ready(Ok(StreamResult::Dropped)) + } + + // If an item isn't ready yet then cancel this outstanding request + // if `finish` is set, otherwise propagate the `Pending` status. + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +impl Drop for ReadDirStream { + fn drop(&mut self) { + if self.result.is_some() { + self.close(Ok(())); + } + } +} + +struct WriteStreamConsumer { + file: File, + location: WriteLocation, + result: Option>>, + buffer: BytesMut, + task: Option>>, +} + +#[derive(Copy, Clone)] +enum WriteLocation { + End, + Offset(u64), +} + +impl WriteStreamConsumer { + fn new_at(file: File, offset: u64, result: oneshot::Sender>) -> Self { + Self { + file, + location: WriteLocation::Offset(offset), + result: Some(result), + buffer: BytesMut::default(), + task: None, + } + } + + fn new_append(file: File, result: oneshot::Sender>) -> Self { + Self { + file, + location: WriteLocation::End, + result: Some(result), + buffer: BytesMut::default(), + task: None, + } + } + + fn close(&mut self, res: Result<(), ErrorCode>) { + _ = self.result.take().unwrap().send(res); + } + + /// Update the internal `offset` field after writing `amt` bytes from the file. + fn complete_write(&mut self, amt: usize) -> StreamResult { + match &mut self.location { + WriteLocation::End => StreamResult::Completed, + WriteLocation::Offset(offset) => { + let Ok(amt) = amt.try_into() else { + self.close(Err(ErrorCode::Overflow)); + return StreamResult::Dropped; + }; + let Some(amt) = offset.checked_add(amt) else { + self.close(Err(ErrorCode::Overflow)); + return StreamResult::Dropped; + }; + *offset = amt; + StreamResult::Completed + } + } + } +} + +impl WriteLocation { + fn write(&self, file: &cap_std::fs::File, bytes: &[u8]) -> io::Result { + match *self { + WriteLocation::End => file.append(bytes), + WriteLocation::Offset(at) => file.write_at(bytes, at), + } + } +} + +impl StreamConsumer for WriteStreamConsumer { + type Item = u8; + + fn poll_consume( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + // Intentionally ignore this as in blocking mode everything is always + // ready and otherwise spawned blocking work can't be cancelled. + _finish: bool, + ) -> Poll> { + let mut src = src.as_direct(store); + if let Some(file) = self.file.as_blocking_file() { + // Once a blocking file, always a blocking file, so assert as such. + assert!(self.task.is_none()); + return match self.location.write(file, src.remaining()) { + Ok(n) => { + src.mark_read(n); + Poll::Ready(Ok(self.complete_write(n))) + } + Err(err) => { + self.close(Err(err.into())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + }; + } + let me = &mut *self; + let task = me.task.get_or_insert_with(|| { + debug_assert!(me.buffer.is_empty()); + me.buffer.extend_from_slice(src.remaining()); + let buf = mem::take(&mut me.buffer); + let file = Arc::clone(me.file.as_file()); + let location = me.location; + spawn_blocking(move || location.write(&file, &buf).map(|n| (buf, n))) + }); + let res = ready!(Pin::new(task).poll(cx)).expect("I/O task should not panic"); + self.task = None; + match res { + Ok((buf, n)) => { + src.mark_read(n); + self.buffer = buf; + self.buffer.clear(); + Poll::Ready(Ok(self.complete_write(n))) + } + Err(err) => { + self.close(Err(err.into())); + Poll::Ready(Ok(StreamResult::Dropped)) + } + } + } +} + +impl Drop for WriteStreamConsumer { + fn drop(&mut self) { + if self.result.is_some() { + self.close(Ok(())) + } + } +} + +impl types::Host for WasiFilesystemCtxView<'_> { + fn convert_error_code(&mut self, error: FilesystemError) -> wasmtime::Result { + error.downcast() + } +} + +impl types::HostDescriptorWithStore for WasiFilesystem { + async fn read_via_stream( + store: &Accessor, + fd: Resource, + offset: Filesize, + ) -> wasmtime::Result<(StreamReader, FutureReader>)> { + let instance = store.instance(); + store.with(|mut store| { + let file = get_file(store.get().table, &fd)?; + if !file.perms.contains(FilePerms::READ) { + return Ok(( + StreamReader::new(instance, &mut store, iter::empty()), + FutureReader::new(instance, &mut store, async { + anyhow::Ok(Err(ErrorCode::NotPermitted)) + }), + )); + } + + let file = file.clone(); + let (result_tx, result_rx) = oneshot::channel(); + Ok(( + StreamReader::new( + instance, + &mut store, + ReadStreamProducer { + file, + offset, + result: Some(result_tx), + task: None, + }, + ), + FutureReader::new(instance, &mut store, result_rx), + )) + }) + } + + async fn write_via_stream( + store: &Accessor, + fd: Resource, + data: StreamReader, + offset: Filesize, + ) -> FilesystemResult<()> { + let (result_tx, result_rx) = oneshot::channel(); + store.with(|mut store| { + let file = get_file(store.get().table, &fd)?; + if !file.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted.into()); + } + let file = file.clone(); + data.pipe(store, WriteStreamConsumer::new_at(file, offset, result_tx)); + FilesystemResult::Ok(()) + })?; + result_rx + .await + .context("oneshot sender dropped") + .map_err(FilesystemError::trap)??; + Ok(()) + } + + async fn append_via_stream( + store: &Accessor, + fd: Resource, + data: StreamReader, + ) -> FilesystemResult<()> { + let (result_tx, result_rx) = oneshot::channel(); + store.with(|mut store| { + let file = get_file(store.get().table, &fd)?; + if !file.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted.into()); + } + let file = file.clone(); + data.pipe(store, WriteStreamConsumer::new_append(file, result_tx)); + FilesystemResult::Ok(()) + })?; + result_rx + .await + .context("oneshot sender dropped") + .map_err(FilesystemError::trap)??; + Ok(()) + } + + async fn advise( + store: &Accessor, + fd: Resource, + offset: Filesize, + length: Filesize, + advice: Advice, + ) -> FilesystemResult<()> { + let file = store.get_file(&fd)?; + file.advise(offset, length, advice.into()).await?; + Ok(()) + } + + async fn sync_data( + store: &Accessor, + fd: Resource, + ) -> FilesystemResult<()> { + let fd = store.get_descriptor(&fd)?; + fd.sync_data().await?; + Ok(()) + } + + async fn get_flags( + store: &Accessor, + fd: Resource, + ) -> FilesystemResult { + let fd = store.get_descriptor(&fd)?; + let flags = fd.get_flags().await?; + Ok(flags.into()) + } + + async fn get_type( + store: &Accessor, + fd: Resource, + ) -> FilesystemResult { + let fd = store.get_descriptor(&fd)?; + let ty = fd.get_type().await?; + Ok(ty.into()) + } + + async fn set_size( + store: &Accessor, + fd: Resource, + size: Filesize, + ) -> FilesystemResult<()> { + let file = store.get_file(&fd)?; + file.set_size(size).await?; + Ok(()) + } + + async fn set_times( + store: &Accessor, + fd: Resource, + data_access_timestamp: NewTimestamp, + data_modification_timestamp: NewTimestamp, + ) -> FilesystemResult<()> { + let fd = store.get_descriptor(&fd)?; + let atim = systemtimespec_from(data_access_timestamp)?; + let mtim = systemtimespec_from(data_modification_timestamp)?; + fd.set_times(atim, mtim).await?; + Ok(()) + } + + async fn read_directory( + store: &Accessor, + fd: Resource, + ) -> wasmtime::Result<( + StreamReader, + FutureReader>, + )> { + let instance = store.instance(); + store.with(|mut store| { + let dir = get_dir(store.get().table, &fd)?; + if !dir.perms.contains(DirPerms::READ) { + return Ok(( + StreamReader::new(instance, &mut store, iter::empty()), + FutureReader::new(instance, &mut store, async { + anyhow::Ok(Err(ErrorCode::NotPermitted)) + }), + )); + } + let allow_blocking_current_thread = dir.allow_blocking_current_thread; + let dir = Arc::clone(dir.as_dir()); + let (result_tx, result_rx) = oneshot::channel(); + let stream = if allow_blocking_current_thread { + match dir.entries() { + Ok(readdir) => StreamReader::new( + instance, + &mut store, + FallibleIteratorProducer::new( + readdir.filter_map(|e| map_dir_entry(e).transpose()), + result_tx, + ), + ), + Err(e) => { + result_tx.send(Err(e.into())).unwrap(); + StreamReader::new(instance, &mut store, iter::empty()) + } + } + } else { + StreamReader::new(instance, &mut store, ReadDirStream::new(dir, result_tx)) + }; + Ok((stream, FutureReader::new(instance, &mut store, result_rx))) + }) + } + + async fn sync(store: &Accessor, fd: Resource) -> FilesystemResult<()> { + let fd = store.get_descriptor(&fd)?; + fd.sync().await?; + Ok(()) + } + + async fn create_directory_at( + store: &Accessor, + fd: Resource, + path: String, + ) -> FilesystemResult<()> { + let dir = store.get_dir(&fd)?; + dir.create_directory_at(path).await?; + Ok(()) + } + + async fn stat( + store: &Accessor, + fd: Resource, + ) -> FilesystemResult { + let fd = store.get_descriptor(&fd)?; + let stat = fd.stat().await?; + Ok(stat.into()) + } + + async fn stat_at( + store: &Accessor, + fd: Resource, + path_flags: PathFlags, + path: String, + ) -> FilesystemResult { + let dir = store.get_dir(&fd)?; + let stat = dir.stat_at(path_flags.into(), path).await?; + Ok(stat.into()) + } + + async fn set_times_at( + store: &Accessor, + fd: Resource, + path_flags: PathFlags, + path: String, + data_access_timestamp: NewTimestamp, + data_modification_timestamp: NewTimestamp, + ) -> FilesystemResult<()> { + let dir = store.get_dir(&fd)?; + let atim = systemtimespec_from(data_access_timestamp)?; + let mtim = systemtimespec_from(data_modification_timestamp)?; + dir.set_times_at(path_flags.into(), path, atim, mtim) + .await?; + Ok(()) + } + + async fn link_at( + store: &Accessor, + fd: Resource, + old_path_flags: PathFlags, + old_path: String, + new_fd: Resource, + new_path: String, + ) -> FilesystemResult<()> { + let (old_dir, new_dir) = store.get_dir_pair(&fd, &new_fd)?; + old_dir + .link_at(old_path_flags.into(), old_path, &new_dir, new_path) + .await?; + Ok(()) + } + + async fn open_at( + store: &Accessor, + fd: Resource, + path_flags: PathFlags, + path: String, + open_flags: OpenFlags, + flags: DescriptorFlags, + ) -> FilesystemResult> { + let (allow_blocking_current_thread, dir) = store.with(|mut store| { + let store = store.get(); + let dir = get_dir(&store.table, &fd)?; + FilesystemResult::Ok((store.ctx.allow_blocking_current_thread, dir.clone())) + })?; + let fd = dir + .open_at( + path_flags.into(), + path, + open_flags.into(), + flags.into(), + allow_blocking_current_thread, + ) + .await?; + let fd = store.with(|mut store| store.get().table.push(fd))?; + Ok(fd) + } + + async fn readlink_at( + store: &Accessor, + fd: Resource, + path: String, + ) -> FilesystemResult { + let dir = store.get_dir(&fd)?; + let path = dir.readlink_at(path).await?; + Ok(path) + } + + async fn remove_directory_at( + store: &Accessor, + fd: Resource, + path: String, + ) -> FilesystemResult<()> { + let dir = store.get_dir(&fd)?; + dir.remove_directory_at(path).await?; + Ok(()) + } + + async fn rename_at( + store: &Accessor, + fd: Resource, + old_path: String, + new_fd: Resource, + new_path: String, + ) -> FilesystemResult<()> { + let (old_dir, new_dir) = store.get_dir_pair(&fd, &new_fd)?; + old_dir.rename_at(old_path, &new_dir, new_path).await?; + Ok(()) + } + + async fn symlink_at( + store: &Accessor, + fd: Resource, + old_path: String, + new_path: String, + ) -> FilesystemResult<()> { + let dir = store.get_dir(&fd)?; + dir.symlink_at(old_path, new_path).await?; + Ok(()) + } + + async fn unlink_file_at( + store: &Accessor, + fd: Resource, + path: String, + ) -> FilesystemResult<()> { + let dir = store.get_dir(&fd)?; + dir.unlink_file_at(path).await?; + Ok(()) + } + + async fn is_same_object( + store: &Accessor, + fd: Resource, + other: Resource, + ) -> wasmtime::Result { + let (fd, other) = store.with(|mut store| { + let table = store.get().table; + let fd = get_descriptor(table, &fd)?.clone(); + let other = get_descriptor(table, &other)?.clone(); + anyhow::Ok((fd, other)) + })?; + fd.is_same_object(&other).await + } + + async fn metadata_hash( + store: &Accessor, + fd: Resource, + ) -> FilesystemResult { + let fd = store.get_descriptor(&fd)?; + let meta = fd.metadata_hash().await?; + Ok(meta.into()) + } + + async fn metadata_hash_at( + store: &Accessor, + fd: Resource, + path_flags: PathFlags, + path: String, + ) -> FilesystemResult { + let dir = store.get_dir(&fd)?; + let meta = dir.metadata_hash_at(path_flags.into(), path).await?; + Ok(meta.into()) + } +} + +impl types::HostDescriptor for WasiFilesystemCtxView<'_> { + fn drop(&mut self, fd: Resource) -> wasmtime::Result<()> { + self.table + .delete(fd) + .context("failed to delete descriptor resource from table")?; + Ok(()) + } +} + +impl preopens::Host for WasiFilesystemCtxView<'_> { + fn get_directories(&mut self) -> wasmtime::Result, String)>> { + self.get_directories() + } +} diff --git a/crates/wasi/src/p3/filesystem/mod.rs b/crates/wasi/src/p3/filesystem/mod.rs new file mode 100644 index 00000000..abac9f1b --- /dev/null +++ b/crates/wasi/src/p3/filesystem/mod.rs @@ -0,0 +1,286 @@ +mod host; + +use crate::TrappableError; +use crate::filesystem::{WasiFilesystem, WasiFilesystemView}; +use crate::p3::bindings::filesystem::{preopens, types}; +use wasmtime::component::Linker; + +pub type FilesystemResult = Result; +pub type FilesystemError = TrappableError; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:filesystem/imports` world supported by +/// this module. +/// +/// This is low-level API for advanced use cases, +/// [`wash_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wash_wasi::filesystem::{WasiFilesystemCtx, WasiFilesystemCtxView, WasiFilesystemView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::filesystem::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// filesystem: WasiFilesystemCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiFilesystemView for MyState { +/// fn filesystem(&mut self) -> WasiFilesystemCtxView<'_> { +/// WasiFilesystemCtxView { +/// ctx: &mut self.filesystem, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiFilesystemView + 'static, +{ + types::add_to_linker::<_, WasiFilesystem>(linker, T::filesystem)?; + preopens::add_to_linker::<_, WasiFilesystem>(linker, T::filesystem)?; + Ok(()) +} + +impl<'a> From<&'a std::io::Error> for types::ErrorCode { + fn from(err: &'a std::io::Error) -> Self { + crate::filesystem::ErrorCode::from(err).into() + } +} + +impl From for types::ErrorCode { + fn from(err: std::io::Error) -> Self { + Self::from(&err) + } +} + +impl From for FilesystemError { + fn from(error: std::io::Error) -> Self { + types::ErrorCode::from(error).into() + } +} + +impl From for types::ErrorCode { + fn from(error: crate::filesystem::ErrorCode) -> Self { + match error { + crate::filesystem::ErrorCode::Access => Self::Access, + crate::filesystem::ErrorCode::Already => Self::Already, + crate::filesystem::ErrorCode::BadDescriptor => Self::BadDescriptor, + crate::filesystem::ErrorCode::Busy => Self::Busy, + crate::filesystem::ErrorCode::Exist => Self::Exist, + crate::filesystem::ErrorCode::FileTooLarge => Self::FileTooLarge, + crate::filesystem::ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, + crate::filesystem::ErrorCode::InProgress => Self::InProgress, + crate::filesystem::ErrorCode::Interrupted => Self::Interrupted, + crate::filesystem::ErrorCode::Invalid => Self::Invalid, + crate::filesystem::ErrorCode::Io => Self::Io, + crate::filesystem::ErrorCode::IsDirectory => Self::IsDirectory, + crate::filesystem::ErrorCode::Loop => Self::Loop, + crate::filesystem::ErrorCode::TooManyLinks => Self::TooManyLinks, + crate::filesystem::ErrorCode::NameTooLong => Self::NameTooLong, + crate::filesystem::ErrorCode::NoEntry => Self::NoEntry, + crate::filesystem::ErrorCode::InsufficientMemory => Self::InsufficientMemory, + crate::filesystem::ErrorCode::InsufficientSpace => Self::InsufficientSpace, + crate::filesystem::ErrorCode::NotDirectory => Self::NotDirectory, + crate::filesystem::ErrorCode::NotEmpty => Self::NotEmpty, + crate::filesystem::ErrorCode::Unsupported => Self::Unsupported, + crate::filesystem::ErrorCode::Overflow => Self::Overflow, + crate::filesystem::ErrorCode::NotPermitted => Self::NotPermitted, + crate::filesystem::ErrorCode::Pipe => Self::Pipe, + crate::filesystem::ErrorCode::InvalidSeek => Self::InvalidSeek, + } + } +} + +impl From for FilesystemError { + fn from(error: crate::filesystem::ErrorCode) -> Self { + types::ErrorCode::from(error).into() + } +} + +impl From for FilesystemError { + fn from(error: wasmtime::component::ResourceTableError) -> Self { + Self::trap(error) + } +} + +impl From for system_interface::fs::Advice { + fn from(advice: types::Advice) -> Self { + match advice { + types::Advice::Normal => Self::Normal, + types::Advice::Sequential => Self::Sequential, + types::Advice::Random => Self::Random, + types::Advice::WillNeed => Self::WillNeed, + types::Advice::DontNeed => Self::DontNeed, + types::Advice::NoReuse => Self::NoReuse, + } + } +} + +impl From for crate::filesystem::OpenFlags { + fn from(flags: types::OpenFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::OpenFlags::CREATE) { + out |= Self::CREATE; + } + if flags.contains(types::OpenFlags::DIRECTORY) { + out |= Self::DIRECTORY; + } + if flags.contains(types::OpenFlags::EXCLUSIVE) { + out |= Self::EXCLUSIVE; + } + if flags.contains(types::OpenFlags::TRUNCATE) { + out |= Self::TRUNCATE; + } + out + } +} + +impl From for crate::filesystem::PathFlags { + fn from(flags: types::PathFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::PathFlags::SYMLINK_FOLLOW) { + out |= Self::SYMLINK_FOLLOW; + } + out + } +} + +impl From for types::DescriptorFlags { + fn from(flags: crate::filesystem::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(crate::filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if flags.contains(crate::filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if flags.contains(crate::filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if flags.contains(crate::filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} + +impl From for crate::filesystem::DescriptorFlags { + fn from(flags: types::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if flags.contains(types::DescriptorFlags::READ) { + out |= Self::READ; + } + if flags.contains(types::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if flags.contains(types::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if flags.contains(types::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if flags.contains(types::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if flags.contains(types::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out + } +} + +impl From for types::MetadataHashValue { + fn from( + crate::filesystem::MetadataHashValue { lower, upper }: crate::filesystem::MetadataHashValue, + ) -> Self { + Self { lower, upper } + } +} + +impl From for types::DescriptorStat { + fn from( + crate::filesystem::DescriptorStat { + type_, + link_count, + size, + data_access_timestamp, + data_modification_timestamp, + status_change_timestamp, + }: crate::filesystem::DescriptorStat, + ) -> Self { + Self { + type_: type_.into(), + link_count, + size, + data_access_timestamp: data_access_timestamp.map(Into::into), + data_modification_timestamp: data_modification_timestamp.map(Into::into), + status_change_timestamp: status_change_timestamp.map(Into::into), + } + } +} + +impl From for types::DescriptorType { + fn from(ty: crate::filesystem::DescriptorType) -> Self { + match ty { + crate::filesystem::DescriptorType::Unknown => Self::Unknown, + crate::filesystem::DescriptorType::BlockDevice => Self::BlockDevice, + crate::filesystem::DescriptorType::CharacterDevice => Self::CharacterDevice, + crate::filesystem::DescriptorType::Directory => Self::Directory, + crate::filesystem::DescriptorType::SymbolicLink => Self::SymbolicLink, + crate::filesystem::DescriptorType::RegularFile => Self::RegularFile, + } + } +} + +impl From for types::DescriptorType { + fn from(ft: cap_std::fs::FileType) -> Self { + use cap_fs_ext::FileTypeExt as _; + if ft.is_dir() { + Self::Directory + } else if ft.is_symlink() { + Self::SymbolicLink + } else if ft.is_block_device() { + Self::BlockDevice + } else if ft.is_char_device() { + Self::CharacterDevice + } else if ft.is_file() { + Self::RegularFile + } else { + Self::Unknown + } + } +} diff --git a/crates/wasi/src/p3/mod.rs b/crates/wasi/src/p3/mod.rs new file mode 100644 index 00000000..8cac68e7 --- /dev/null +++ b/crates/wasi/src/p3/mod.rs @@ -0,0 +1,193 @@ +//! Experimental, unstable and incomplete implementation of wasip3 version of WASI. +//! +//! This module is under heavy development. +//! It is not compliant with semver and is not ready +//! for production use. +//! +//! Bug and security fixes limited to wasip3 will not be given patch releases. +//! +//! Documentation of this module may be incorrect or out-of-sync with the implementation. + +pub mod bindings; +pub mod cli; +pub mod clocks; +pub mod filesystem; +pub mod random; +pub mod sockets; + +use crate::WasiView; +use crate::p3::bindings::LinkOptions; +use core::pin::Pin; +use core::task::{Context, Poll}; +use tokio::sync::oneshot; +use wasmtime::StoreContextMut; +use wasmtime::component::{Destination, Linker, StreamProducer, StreamResult, VecBuffer}; + +// Default buffer capacity to use for reads of byte-sized values. +const DEFAULT_BUFFER_CAPACITY: usize = 8192; + +/// Helper structure to convert an iterator of `Result` into a `stream` +/// plus a `future>` in WIT. +/// +/// This will drain the iterator on calls to `poll_produce` and place as many +/// items as the input buffer has capacity for into the result. This will avoid +/// doing anything if the async read is cancelled. +/// +/// Note that this does not actually do anything async, it's assuming that the +/// internal `iter` is either fast or intended to block. +struct FallibleIteratorProducer { + iter: I, + result: Option>>, +} + +impl StreamProducer for FallibleIteratorProducer +where + I: Iterator> + Send + Unpin + 'static, + T: Send + Sync + 'static, + E: Send + 'static, +{ + type Item = T; + type Buffer = VecBuffer; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + _: &mut Context<'_>, + mut store: StoreContextMut<'a, D>, + mut dst: Destination<'a, Self::Item, Self::Buffer>, + // Explicitly ignore `_finish` because this implementation never + // returns `Poll::Pending` anyway meaning that it never "blocks" in the + // async sense. + _finish: bool, + ) -> Poll> { + // Take up to `count` items as requested by the guest, or pick some + // reasonable-ish number for the host. + let count = dst.remaining(&mut store).unwrap_or(32); + + // Handle 0-length reads which test for readiness as saying "we're + // always ready" since, in theory, this is. + if count == 0 { + return Poll::Ready(Ok(StreamResult::Completed)); + } + + // Drain `self.iter`. Successful results go into `buf`. Any errors make + // their way to the `oneshot` result inside this structure. Otherwise + // this only gets dropped if `None` is seen or an error. Also this'll + // terminate once `buf` grows too large. + let mut buf = Vec::new(); + let result = loop { + match self.iter.next() { + Some(Ok(item)) => buf.push(item), + Some(Err(e)) => { + self.close(Err(e)); + break StreamResult::Dropped; + } + + None => { + self.close(Ok(())); + break StreamResult::Dropped; + } + } + if buf.len() >= count { + break StreamResult::Completed; + } + }; + + dst.set_buffer(buf.into()); + return Poll::Ready(Ok(result)); + } +} + +impl FallibleIteratorProducer { + fn new(iter: I, result: oneshot::Sender>) -> Self { + Self { + iter, + result: Some(result), + } + } + + fn close(&mut self, result: Result<(), E>) { + // Ignore send failures because it means the other end wasn't interested + // in the final error, if any. + let _ = self.result.take().unwrap().send(result); + } +} + +impl Drop for FallibleIteratorProducer { + fn drop(&mut self) { + if self.result.is_some() { + self.close(Ok(())); + } + } +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this module. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiView + 'static, +{ + let options = LinkOptions::default(); + add_to_linker_with_options(linker, &options) +} + +/// Similar to [`add_to_linker`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options( + linker: &mut Linker, + options: &LinkOptions, +) -> wasmtime::Result<()> +where + T: WasiView + 'static, +{ + cli::add_to_linker_with_options(linker, &options.into())?; + clocks::add_to_linker(linker)?; + filesystem::add_to_linker(linker)?; + random::add_to_linker(linker)?; + sockets::add_to_linker(linker)?; + Ok(()) +} diff --git a/crates/wasi/src/p3/random/host.rs b/crates/wasi/src/p3/random/host.rs new file mode 100644 index 00000000..03f106a0 --- /dev/null +++ b/crates/wasi/src/p3/random/host.rs @@ -0,0 +1,38 @@ +use cap_rand::Rng; +use cap_rand::distributions::Standard; + +use crate::p3::bindings::random::{insecure, insecure_seed, random}; +use crate::random::WasiRandomCtx; + +impl random::Host for WasiRandomCtx { + fn get_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_random_u64(&mut self) -> wasmtime::Result { + Ok(self.random.sample(Standard)) + } +} + +impl insecure::Host for WasiRandomCtx { + fn get_insecure_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.insecure_random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_insecure_random_u64(&mut self) -> wasmtime::Result { + Ok(self.insecure_random.sample(Standard)) + } +} + +impl insecure_seed::Host for WasiRandomCtx { + fn get_insecure_seed(&mut self) -> wasmtime::Result<(u64, u64)> { + let seed: u128 = self.insecure_random_seed; + Ok((seed as u64, (seed >> 64) as u64)) + } +} diff --git a/crates/wasi/src/p3/random/mod.rs b/crates/wasi/src/p3/random/mod.rs new file mode 100644 index 00000000..84490705 --- /dev/null +++ b/crates/wasi/src/p3/random/mod.rs @@ -0,0 +1,62 @@ +mod host; + +use crate::p3::bindings::random::{insecure, insecure_seed, random}; +use crate::random::{WasiRandom, WasiRandomView}; +use wasmtime::component::Linker; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:random/imports` world supported by +/// this crate. +/// +/// This is low-level API for advanced use cases, +/// [`wash_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::Linker; +/// use wash_wasi::random::{WasiRandomView, WasiRandomCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::random::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// random: WasiRandomCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// random: WasiRandomCtx, +/// } +/// +/// impl WasiRandomView for MyState { +/// fn random(&mut self) -> &mut WasiRandomCtx { &mut self.random } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiRandomView + 'static, +{ + random::add_to_linker::<_, WasiRandom>(linker, T::random)?; + insecure::add_to_linker::<_, WasiRandom>(linker, T::random)?; + insecure_seed::add_to_linker::<_, WasiRandom>(linker, T::random)?; + Ok(()) +} diff --git a/crates/wasi/src/p3/sockets/conv.rs b/crates/wasi/src/p3/sockets/conv.rs new file mode 100644 index 00000000..915bd3e9 --- /dev/null +++ b/crates/wasi/src/p3/sockets/conv.rs @@ -0,0 +1,252 @@ +use crate::p3::bindings::sockets::types; +use crate::p3::sockets::SocketError; +use crate::sockets::SocketAddressFamily; +use crate::sockets::util::{from_ipv4_addr, from_ipv6_addr, to_ipv4_addr, to_ipv6_addr}; +use core::net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}; +use rustix::io::Errno; +use std::net::ToSocketAddrs; +use tracing::debug; + +impl From for types::IpAddress { + fn from(addr: IpAddr) -> Self { + match addr { + IpAddr::V4(v4) => Self::Ipv4(from_ipv4_addr(v4)), + IpAddr::V6(v6) => Self::Ipv6(from_ipv6_addr(v6)), + } + } +} + +impl From for IpAddr { + fn from(addr: types::IpAddress) -> Self { + match addr { + types::IpAddress::Ipv4(v4) => Self::V4(to_ipv4_addr(v4)), + types::IpAddress::Ipv6(v6) => Self::V6(to_ipv6_addr(v6)), + } + } +} + +impl From for SocketAddr { + fn from(addr: types::IpSocketAddress) -> Self { + match addr { + types::IpSocketAddress::Ipv4(ipv4) => Self::V4(ipv4.into()), + types::IpSocketAddress::Ipv6(ipv6) => Self::V6(ipv6.into()), + } + } +} + +impl From for types::IpSocketAddress { + fn from(addr: SocketAddr) -> Self { + match addr { + SocketAddr::V4(v4) => Self::Ipv4(v4.into()), + SocketAddr::V6(v6) => Self::Ipv6(v6.into()), + } + } +} + +impl From for SocketAddrV4 { + fn from(addr: types::Ipv4SocketAddress) -> Self { + Self::new(to_ipv4_addr(addr.address), addr.port) + } +} + +impl From for types::Ipv4SocketAddress { + fn from(addr: SocketAddrV4) -> Self { + Self { + address: from_ipv4_addr(*addr.ip()), + port: addr.port(), + } + } +} + +impl From for SocketAddrV6 { + fn from(addr: types::Ipv6SocketAddress) -> Self { + Self::new( + to_ipv6_addr(addr.address), + addr.port, + addr.flow_info, + addr.scope_id, + ) + } +} + +impl From for types::Ipv6SocketAddress { + fn from(addr: SocketAddrV6) -> Self { + Self { + address: from_ipv6_addr(*addr.ip()), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + } + } +} + +impl ToSocketAddrs for types::IpSocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddr::from(*self).to_socket_addrs() + } +} + +impl ToSocketAddrs for types::Ipv4SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddrV4::from(*self).to_socket_addrs() + } +} + +impl ToSocketAddrs for types::Ipv6SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + SocketAddrV6::from(*self).to_socket_addrs() + } +} + +impl From for cap_net_ext::AddressFamily { + fn from(family: types::IpAddressFamily) -> Self { + match family { + types::IpAddressFamily::Ipv4 => Self::Ipv4, + types::IpAddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for types::IpAddressFamily { + fn from(family: cap_net_ext::AddressFamily) -> Self { + match family { + cap_net_ext::AddressFamily::Ipv4 => Self::Ipv4, + cap_net_ext::AddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for types::IpAddressFamily { + fn from(family: SocketAddressFamily) -> Self { + match family { + SocketAddressFamily::Ipv4 => Self::Ipv4, + SocketAddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for SocketAddressFamily { + fn from(family: types::IpAddressFamily) -> Self { + match family { + types::IpAddressFamily::Ipv4 => Self::Ipv4, + types::IpAddressFamily::Ipv6 => Self::Ipv6, + } + } +} + +impl From for types::ErrorCode { + fn from(value: std::io::Error) -> Self { + (&value).into() + } +} + +impl From<&std::io::Error> for types::ErrorCode { + fn from(value: &std::io::Error) -> Self { + // Attempt the more detailed native error code first: + if let Some(errno) = Errno::from_io_error(value) { + return errno.into(); + } + + match value.kind() { + std::io::ErrorKind::AddrInUse => Self::AddressInUse, + std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable, + std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted, + std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused, + std::io::ErrorKind::ConnectionReset => Self::ConnectionReset, + std::io::ErrorKind::InvalidInput => Self::InvalidArgument, + std::io::ErrorKind::NotConnected => Self::InvalidState, + std::io::ErrorKind::OutOfMemory => Self::OutOfMemory, + std::io::ErrorKind::PermissionDenied => Self::AccessDenied, + std::io::ErrorKind::TimedOut => Self::Timeout, + std::io::ErrorKind::Unsupported => Self::NotSupported, + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} + +impl From for types::ErrorCode { + fn from(value: Errno) -> Self { + (&value).into() + } +} + +impl From<&Errno> for types::ErrorCode { + fn from(value: &Errno) -> Self { + match *value { + #[cfg(not(windows))] + Errno::PERM => Self::AccessDenied, + Errno::ACCESS => Self::AccessDenied, + Errno::ADDRINUSE => Self::AddressInUse, + Errno::ADDRNOTAVAIL => Self::AddressNotBindable, + Errno::TIMEDOUT => Self::Timeout, + Errno::CONNREFUSED => Self::ConnectionRefused, + Errno::CONNRESET => Self::ConnectionReset, + Errno::CONNABORTED => Self::ConnectionAborted, + Errno::INVAL => Self::InvalidArgument, + Errno::HOSTUNREACH => Self::RemoteUnreachable, + Errno::HOSTDOWN => Self::RemoteUnreachable, + Errno::NETDOWN => Self::RemoteUnreachable, + Errno::NETUNREACH => Self::RemoteUnreachable, + #[cfg(target_os = "linux")] + Errno::NONET => Self::RemoteUnreachable, + Errno::ISCONN => Self::InvalidState, + Errno::NOTCONN => Self::InvalidState, + Errno::DESTADDRREQ => Self::InvalidState, + Errno::MSGSIZE => Self::DatagramTooLarge, + #[cfg(not(windows))] + Errno::NOMEM => Self::OutOfMemory, + Errno::NOBUFS => Self::OutOfMemory, + Errno::OPNOTSUPP => Self::NotSupported, + Errno::NOPROTOOPT => Self::NotSupported, + Errno::PFNOSUPPORT => Self::NotSupported, + Errno::PROTONOSUPPORT => Self::NotSupported, + Errno::PROTOTYPE => Self::NotSupported, + Errno::SOCKTNOSUPPORT => Self::NotSupported, + Errno::AFNOSUPPORT => Self::NotSupported, + + // FYI, EINPROGRESS should have already been handled by connect. + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} + +impl From for types::ErrorCode { + fn from(code: crate::sockets::util::ErrorCode) -> Self { + match code { + crate::sockets::util::ErrorCode::Unknown => Self::Unknown, + crate::sockets::util::ErrorCode::AccessDenied => Self::AccessDenied, + crate::sockets::util::ErrorCode::NotSupported => Self::NotSupported, + crate::sockets::util::ErrorCode::InvalidArgument => Self::InvalidArgument, + crate::sockets::util::ErrorCode::OutOfMemory => Self::OutOfMemory, + crate::sockets::util::ErrorCode::Timeout => Self::Timeout, + crate::sockets::util::ErrorCode::InvalidState => Self::InvalidState, + crate::sockets::util::ErrorCode::AddressNotBindable => Self::AddressNotBindable, + crate::sockets::util::ErrorCode::AddressInUse => Self::AddressInUse, + crate::sockets::util::ErrorCode::RemoteUnreachable => Self::RemoteUnreachable, + crate::sockets::util::ErrorCode::ConnectionRefused => Self::ConnectionRefused, + crate::sockets::util::ErrorCode::ConnectionReset => Self::ConnectionReset, + crate::sockets::util::ErrorCode::ConnectionAborted => Self::ConnectionAborted, + crate::sockets::util::ErrorCode::DatagramTooLarge => Self::DatagramTooLarge, + crate::sockets::util::ErrorCode::NotInProgress => Self::InvalidState, + crate::sockets::util::ErrorCode::ConcurrencyConflict => Self::InvalidState, + } + } +} + +impl From for SocketError { + fn from(code: crate::sockets::util::ErrorCode) -> Self { + SocketError::from(types::ErrorCode::from(code)) + } +} diff --git a/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs b/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs new file mode 100644 index 00000000..d4d3dbbc --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/ip_name_lookup.rs @@ -0,0 +1,39 @@ +use tokio::net::lookup_host; +use wasmtime::component::Accessor; + +use crate::p3::bindings::sockets::ip_name_lookup::{ErrorCode, Host, HostWithStore}; +use crate::p3::bindings::sockets::types; +use crate::p3::sockets::WasiSockets; +use crate::sockets::WasiSocketsCtxView; +use crate::sockets::util::{from_ipv4_addr, from_ipv6_addr, parse_host}; + +impl HostWithStore for WasiSockets { + async fn resolve_addresses( + store: &Accessor, + name: String, + ) -> wasmtime::Result, ErrorCode>> { + let Ok(host) = parse_host(&name) else { + return Ok(Err(ErrorCode::InvalidArgument)); + }; + if !store.with(|mut view| view.get().ctx.allowed_network_uses.ip_name_lookup) { + return Ok(Err(ErrorCode::PermanentResolverFailure)); + } + match host { + url::Host::Ipv4(addr) => Ok(Ok(vec![types::IpAddress::Ipv4(from_ipv4_addr(addr))])), + url::Host::Ipv6(addr) => Ok(Ok(vec![types::IpAddress::Ipv6(from_ipv6_addr(addr))])), + url::Host::Domain(domain) => { + // This is only resolving names, not ports, so force the port to be 0. + if let Ok(addrs) = lookup_host((domain.as_str(), 0)).await { + Ok(Ok(addrs + .map(|addr| addr.ip().to_canonical().into()) + .collect())) + } else { + // If/when we use `getaddrinfo` directly, map the error properly. + Ok(Err(ErrorCode::NameUnresolvable)) + } + } + } + } +} + +impl Host for WasiSocketsCtxView<'_> {} diff --git a/crates/wasi/src/p3/sockets/host/mod.rs b/crates/wasi/src/p3/sockets/host/mod.rs new file mode 100644 index 00000000..aa4d333f --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/mod.rs @@ -0,0 +1,2 @@ +mod ip_name_lookup; +mod types; diff --git a/crates/wasi/src/p3/sockets/host/types/mod.rs b/crates/wasi/src/p3/sockets/host/types/mod.rs new file mode 100644 index 00000000..60c152da --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/mod.rs @@ -0,0 +1,26 @@ +use crate::p3::bindings::sockets::types::{ErrorCode, Host}; +use crate::p3::sockets::{SocketError, WasiSockets}; +use crate::sockets::{SocketAddrCheck, SocketAddrUse, WasiSocketsCtxView}; +use core::net::SocketAddr; +use wasmtime::component::Accessor; + +mod tcp; +mod udp; + +impl Host for WasiSocketsCtxView<'_> { + fn convert_error_code(&mut self, error: SocketError) -> anyhow::Result { + error.downcast() + } +} + +fn get_socket_addr_check(store: &Accessor) -> SocketAddrCheck { + store.with(|mut view| view.get().ctx.socket_addr_check.clone()) +} + +async fn is_addr_allowed( + store: &Accessor, + addr: SocketAddr, + reason: SocketAddrUse, +) -> bool { + get_socket_addr_check(store)(addr, reason).await +} diff --git a/crates/wasi/src/p3/sockets/host/types/tcp.rs b/crates/wasi/src/p3/sockets/host/types/tcp.rs new file mode 100644 index 00000000..13bea56c --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/tcp.rs @@ -0,0 +1,535 @@ +use super::is_addr_allowed; +use crate::p3::DEFAULT_BUFFER_CAPACITY; +use crate::p3::bindings::sockets::types::{ + Duration, ErrorCode, HostTcpSocket, HostTcpSocketWithStore, IpAddressFamily, IpSocketAddress, + TcpSocket, +}; +use crate::p3::sockets::{SocketError, SocketResult, WasiSockets}; +use crate::sockets::{NonInheritedOptions, SocketAddrUse, SocketAddressFamily, WasiSocketsCtxView}; +use anyhow::Context as _; +use bytes::BytesMut; +use core::iter; +use core::pin::Pin; +use core::task::{Context, Poll}; +use io_lifetimes::AsSocketlike as _; +use std::io::Cursor; +use std::net::{Shutdown, SocketAddr}; +use std::sync::Arc; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::oneshot; +use wasmtime::component::{ + Accessor, Destination, FutureReader, Resource, ResourceTable, Source, StreamConsumer, + StreamProducer, StreamReader, StreamResult, +}; +use wasmtime::{AsContextMut as _, StoreContextMut}; + +fn get_socket<'a>( + table: &'a ResourceTable, + socket: &'a Resource, +) -> SocketResult<&'a TcpSocket> { + table + .get(socket) + .context("failed to get socket resource from table") + .map_err(SocketError::trap) +} + +fn get_socket_mut<'a>( + table: &'a mut ResourceTable, + socket: &'a Resource, +) -> SocketResult<&'a mut TcpSocket> { + table + .get_mut(socket) + .context("failed to get socket resource from table") + .map_err(SocketError::trap) +} + +struct ListenStreamProducer { + listener: Arc, + family: SocketAddressFamily, + options: NonInheritedOptions, + getter: for<'a> fn(&'a mut T) -> WasiSocketsCtxView<'a>, +} + +impl StreamProducer for ListenStreamProducer +where + D: 'static, +{ + type Item = Resource; + type Buffer = Option; + + fn poll_produce<'a>( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut store: StoreContextMut<'a, D>, + mut dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + // If the destination buffer is empty then this is a request on + // behalf of the guest to wait for this socket to be ready to accept + // without actually accepting something. The `TcpListener` in Tokio does + // not have this capability so we're forced to lie here and say instead + // "yes we're ready to accept" as a fallback. + // + // See WebAssembly/component-model#561 for some more information. + if dst.remaining(&mut store) == Some(0) { + return Poll::Ready(Ok(StreamResult::Completed)); + } + let res = match self.listener.poll_accept(cx) { + Poll::Ready(res) => res.map(|(stream, _)| stream), + Poll::Pending if finish => return Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => return Poll::Pending, + }; + let socket = TcpSocket::new_accept(res, &self.options, self.family) + .unwrap_or_else(|err| TcpSocket::new_error(err, self.family)); + let WasiSocketsCtxView { table, .. } = (self.getter)(store.data_mut()); + let socket = table + .push(socket) + .context("failed to push socket resource to table")?; + dst.set_buffer(Some(socket)); + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +struct ReceiveStreamProducer { + stream: Arc, + result: Option>>, +} + +impl Drop for ReceiveStreamProducer { + fn drop(&mut self) { + self.close(Ok(())) + } +} + +impl ReceiveStreamProducer { + fn close(&mut self, res: Result<(), ErrorCode>) { + if let Some(tx) = self.result.take() { + _ = self + .stream + .as_socketlike_view::() + .shutdown(Shutdown::Read); + _ = tx.send(res); + } + } +} + +impl StreamProducer for ReceiveStreamProducer { + type Item = u8; + type Buffer = Cursor; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut store: StoreContextMut<'a, D>, + dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + let res = 'result: { + // 0-length reads are an indication that we should wait for + // readiness here, so use `poll_read_ready`. + if dst.remaining(store.as_context_mut()) == Some(0) { + return match self.stream.poll_read_ready(cx) { + Poll::Ready(Ok(())) => Poll::Ready(Ok(StreamResult::Completed)), + Poll::Ready(Err(err)) => break 'result Err(err.into()), + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + }; + } + + let mut dst = dst.as_direct(store, DEFAULT_BUFFER_CAPACITY); + let buf = dst.remaining(); + loop { + match self.stream.try_read(buf) { + Ok(0) => break 'result Ok(()), + Ok(n) => { + dst.mark_written(n); + return Poll::Ready(Ok(StreamResult::Completed)); + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + match self.stream.poll_read_ready(cx) { + Poll::Ready(Ok(())) => continue, + Poll::Ready(Err(err)) => break 'result Err(err.into()), + Poll::Pending if finish => { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + Poll::Pending => return Poll::Pending, + } + } + Err(err) => break 'result Err(err.into()), + } + } + }; + self.close(res); + Poll::Ready(Ok(StreamResult::Dropped)) + } +} + +struct SendStreamConsumer { + stream: Arc, + result: Option>>, +} + +impl Drop for SendStreamConsumer { + fn drop(&mut self) { + self.close(Ok(())) + } +} + +impl SendStreamConsumer { + fn close(&mut self, res: Result<(), ErrorCode>) { + if let Some(tx) = self.result.take() { + _ = self + .stream + .as_socketlike_view::() + .shutdown(Shutdown::Write); + _ = tx.send(res); + } + } +} + +impl StreamConsumer for SendStreamConsumer { + type Item = u8; + + fn poll_consume( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + finish: bool, + ) -> Poll> { + let mut src = src.as_direct(store); + let res = 'result: { + // A 0-length write is a request to wait for readiness so use + // `poll_write_ready` to wait for the underlying object to be ready. + if src.remaining().is_empty() { + return match self.stream.poll_write_ready(cx) { + Poll::Ready(Ok(())) => Poll::Ready(Ok(StreamResult::Completed)), + Poll::Ready(Err(err)) => break 'result Err(err.into()), + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + }; + } + loop { + match self.stream.try_write(src.remaining()) { + Ok(n) => { + debug_assert!(n > 0); + src.mark_read(n); + return Poll::Ready(Ok(StreamResult::Completed)); + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + match self.stream.poll_write_ready(cx) { + Poll::Ready(Ok(())) => continue, + Poll::Ready(Err(err)) => break 'result Err(err.into()), + Poll::Pending if finish => { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + Poll::Pending => return Poll::Pending, + } + } + Err(err) => break 'result Err(err.into()), + } + } + }; + self.close(res); + Poll::Ready(Ok(StreamResult::Dropped)) + } +} + +impl HostTcpSocketWithStore for WasiSockets { + async fn bind( + store: &Accessor, + socket: Resource, + local_address: IpSocketAddress, + ) -> SocketResult<()> { + let local_address = SocketAddr::from(local_address); + if !is_addr_allowed(store, local_address, SocketAddrUse::TcpBind).await { + return Err(ErrorCode::AccessDenied.into()); + } + store.with(|mut store| { + let socket = get_socket_mut(store.get().table, &socket)?; + let TcpSocket::Network(socket) = socket else { + todo!() + }; + socket.start_bind(local_address)?; + socket.finish_bind()?; + Ok(()) + }) + } + + async fn connect( + store: &Accessor, + socket: Resource, + remote_address: IpSocketAddress, + ) -> SocketResult<()> { + let remote_address = SocketAddr::from(remote_address); + if !is_addr_allowed(store, remote_address, SocketAddrUse::TcpConnect).await { + return Err(ErrorCode::AccessDenied.into()); + } + let sock = store.with(|mut store| { + let ctx = store.get(); + let socket = get_socket_mut(ctx.table, &socket)?; + let mut loopback = ctx.ctx.loopback.lock().unwrap(); + let socket = socket.start_connect(&remote_address, &mut loopback)?; + SocketResult::Ok(socket) + })?; + + // FIXME: handle possible cancellation of the outer `connect` + // https://github.com/bytecodealliance/wasmtime/pull/11291#discussion_r2223917986 + let res = sock.connect(remote_address).await; + store.with(|mut store| { + let ctx = store.get(); + let socket = get_socket_mut(ctx.table, &socket)?; + let mut loopback = ctx.ctx.loopback.lock().unwrap(); + socket.finish_connect(res, &mut loopback)?; + Ok(()) + }) + } + + async fn listen( + store: &Accessor, + socket: Resource, + ) -> SocketResult>> { + let instance = store.instance(); + let getter = store.getter(); + store.with(|mut store| { + let socket = get_socket_mut(store.get().table, &socket)?; + let TcpSocket::Network(socket) = socket else { + todo!() + }; + socket.start_listen()?; + socket.finish_listen()?; + let listener = socket.tcp_listener_arc().unwrap().clone(); + let family = socket.address_family(); + let options = socket.non_inherited_options().clone(); + Ok(StreamReader::new( + instance, + &mut store, + ListenStreamProducer { + listener, + family, + options, + getter, + }, + )) + }) + } + + async fn send( + store: &Accessor, + socket: Resource, + data: StreamReader, + ) -> SocketResult<()> { + let (result_tx, result_rx) = oneshot::channel(); + store.with(|mut store| match get_socket(store.get().table, &socket)? { + TcpSocket::Network(sock) => { + let stream = sock.tcp_stream_arc()?; + let stream = Arc::clone(stream); + data.pipe( + store, + SendStreamConsumer { + stream, + result: Some(result_tx), + }, + ); + SocketResult::Ok(()) + } + TcpSocket::Loopback(..) => todo!(), + TcpSocket::Unspecified { .. } => todo!(), + })?; + result_rx + .await + .context("oneshot sender dropped") + .map_err(SocketError::trap)??; + Ok(()) + } + + async fn receive( + store: &Accessor, + socket: Resource, + ) -> wasmtime::Result<(StreamReader, FutureReader>)> { + let instance = store.instance(); + store.with(|mut store| { + let socket = get_socket_mut(store.get().table, &socket)?; + let TcpSocket::Network(socket) = socket else { + todo!() + }; + match socket.start_receive() { + Some(stream) => { + let stream = Arc::clone(stream); + let (result_tx, result_rx) = oneshot::channel(); + Ok(( + StreamReader::new( + instance, + &mut store, + ReceiveStreamProducer { + stream, + result: Some(result_tx), + }, + ), + FutureReader::new(instance, &mut store, result_rx), + )) + } + None => Ok(( + StreamReader::new(instance, &mut store, iter::empty()), + FutureReader::new(instance, &mut store, async { + anyhow::Ok(Err(ErrorCode::InvalidState)) + }), + )), + } + }) + } +} + +impl HostTcpSocket for WasiSocketsCtxView<'_> { + fn create(&mut self, address_family: IpAddressFamily) -> SocketResult> { + let family = address_family.into(); + let socket = TcpSocket::new(self.ctx, family)?; + let resource = self + .table + .push(socket) + .context("failed to push socket resource to table") + .map_err(SocketError::trap)?; + Ok(resource) + } + + fn get_local_address(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.local_address()?.into()) + } + + fn get_remote_address(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.remote_address()?.into()) + } + + fn get_is_listening(&mut self, socket: Resource) -> wasmtime::Result { + let sock = get_socket(self.table, &socket)?; + Ok(sock.is_listening()) + } + + fn get_address_family( + &mut self, + socket: Resource, + ) -> wasmtime::Result { + let sock = get_socket(self.table, &socket)?; + Ok(sock.address_family().into()) + } + + fn set_listen_backlog_size( + &mut self, + socket: Resource, + value: u64, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_listen_backlog_size(value)?; + Ok(()) + } + + fn get_keep_alive_enabled(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.keep_alive_enabled()?) + } + + fn set_keep_alive_enabled( + &mut self, + socket: Resource, + value: bool, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_keep_alive_enabled(value)?; + Ok(()) + } + + fn get_keep_alive_idle_time(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.keep_alive_idle_time()?) + } + + fn set_keep_alive_idle_time( + &mut self, + socket: Resource, + value: Duration, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_keep_alive_idle_time(value)?; + Ok(()) + } + + fn get_keep_alive_interval(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.keep_alive_interval()?) + } + + fn set_keep_alive_interval( + &mut self, + socket: Resource, + value: Duration, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_keep_alive_interval(value)?; + Ok(()) + } + + fn get_keep_alive_count(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.keep_alive_count()?) + } + + fn set_keep_alive_count( + &mut self, + socket: Resource, + value: u32, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_keep_alive_count(value)?; + Ok(()) + } + + fn get_hop_limit(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.hop_limit()?) + } + + fn set_hop_limit(&mut self, socket: Resource, value: u8) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_hop_limit(value)?; + Ok(()) + } + + fn get_receive_buffer_size(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.receive_buffer_size()?) + } + + fn set_receive_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_receive_buffer_size(value)?; + Ok(()) + } + + fn get_send_buffer_size(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.send_buffer_size()?) + } + + fn set_send_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_send_buffer_size(value)?; + Ok(()) + } + + fn drop(&mut self, sock: Resource) -> wasmtime::Result<()> { + self.table + .delete(sock) + .context("failed to delete socket resource from table")?; + Ok(()) + } +} diff --git a/crates/wasi/src/p3/sockets/host/types/udp.rs b/crates/wasi/src/p3/sockets/host/types/udp.rs new file mode 100644 index 00000000..d01a3ad5 --- /dev/null +++ b/crates/wasi/src/p3/sockets/host/types/udp.rs @@ -0,0 +1,194 @@ +use super::is_addr_allowed; +use crate::TrappableError; +use crate::p3::bindings::sockets::types::{ + ErrorCode, HostUdpSocket, HostUdpSocketWithStore, IpAddressFamily, IpSocketAddress, +}; +use crate::p3::sockets::{SocketResult, WasiSockets}; +use crate::sockets::{MAX_UDP_DATAGRAM_SIZE, SocketAddrUse, UdpSocket, WasiSocketsCtxView}; +use anyhow::Context; +use std::net::SocketAddr; +use wasmtime::component::{Accessor, Resource, ResourceTable}; + +fn get_socket<'a>( + table: &'a ResourceTable, + socket: &'a Resource, +) -> SocketResult<&'a UdpSocket> { + table + .get(socket) + .context("failed to get socket resource from table") + .map_err(TrappableError::trap) +} + +fn get_socket_mut<'a>( + table: &'a mut ResourceTable, + socket: &'a Resource, +) -> SocketResult<&'a mut UdpSocket> { + table + .get_mut(socket) + .context("failed to get socket resource from table") + .map_err(TrappableError::trap) +} + +impl HostUdpSocketWithStore for WasiSockets { + async fn bind( + store: &Accessor, + socket: Resource, + local_address: IpSocketAddress, + ) -> SocketResult<()> { + let local_address = SocketAddr::from(local_address); + if !is_addr_allowed(store, local_address, SocketAddrUse::UdpBind).await { + return Err(ErrorCode::AccessDenied.into()); + } + store.with(|mut view| { + let ctx = view.get(); + let socket = get_socket_mut(ctx.table, &socket)?; + let mut loopback = ctx.ctx.loopback.lock().unwrap(); + socket.bind(local_address, &mut loopback)?; + socket.finish_bind()?; + Ok(()) + }) + } + + async fn connect( + store: &Accessor, + socket: Resource, + remote_address: IpSocketAddress, + ) -> SocketResult<()> { + let remote_address = SocketAddr::from(remote_address); + if !is_addr_allowed(store, remote_address, SocketAddrUse::UdpConnect).await { + return Err(ErrorCode::AccessDenied.into()); + } + store.with(|mut view| { + let ctx = view.get(); + let socket = get_socket_mut(ctx.table, &socket)?; + let mut loopback = ctx.ctx.loopback.lock().unwrap(); + socket.connect(remote_address, &mut loopback)?; + Ok(()) + }) + } + + async fn send( + store: &Accessor, + socket: Resource, + data: Vec, + remote_address: Option, + ) -> SocketResult<()> { + if data.len() > MAX_UDP_DATAGRAM_SIZE { + return Err(ErrorCode::DatagramTooLarge.into()); + } + if let Some(addr) = remote_address { + let addr = SocketAddr::from(addr); + if !is_addr_allowed(store, addr, SocketAddrUse::UdpOutgoingDatagram).await { + return Err(ErrorCode::AccessDenied.into()); + } + let fut = store.with(|mut view| { + get_socket(view.get().table, &socket).map(|sock| sock.send_to(data, addr)) + })?; + fut.await?; + Ok(()) + } else { + let fut = store.with(|mut view| { + get_socket(view.get().table, &socket).map(|sock| sock.send(data)) + })?; + fut.await?; + Ok(()) + } + } + + async fn receive( + store: &Accessor, + socket: Resource, + ) -> SocketResult<(Vec, IpSocketAddress)> { + let fut = store + .with(|mut view| get_socket(view.get().table, &socket).map(|sock| sock.receive()))?; + let (result, addr) = fut.await?; + Ok((result, addr.into())) + } +} + +impl HostUdpSocket for WasiSocketsCtxView<'_> { + fn create(&mut self, address_family: IpAddressFamily) -> SocketResult> { + let socket = UdpSocket::new(self.ctx, address_family.into())?; + self.table + .push(socket) + .context("failed to push socket resource to table") + .map_err(TrappableError::trap) + } + + fn disconnect(&mut self, socket: Resource) -> SocketResult<()> { + let socket = get_socket_mut(self.table, &socket)?; + let mut loopback = self.ctx.loopback.lock().unwrap(); + socket.disconnect(&mut loopback)?; + Ok(()) + } + + fn get_local_address(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.local_address()?.into()) + } + + fn get_remote_address(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.remote_address()?.into()) + } + + fn get_address_family( + &mut self, + socket: Resource, + ) -> wasmtime::Result { + let sock = get_socket(self.table, &socket)?; + Ok(sock.address_family().into()) + } + + fn get_unicast_hop_limit(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.unicast_hop_limit()?) + } + + fn set_unicast_hop_limit( + &mut self, + socket: Resource, + value: u8, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_unicast_hop_limit(value)?; + Ok(()) + } + + fn get_receive_buffer_size(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.receive_buffer_size()?) + } + + fn set_receive_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_receive_buffer_size(value)?; + Ok(()) + } + + fn get_send_buffer_size(&mut self, socket: Resource) -> SocketResult { + let sock = get_socket(self.table, &socket)?; + Ok(sock.send_buffer_size()?) + } + + fn set_send_buffer_size( + &mut self, + socket: Resource, + value: u64, + ) -> SocketResult<()> { + let sock = get_socket_mut(self.table, &socket)?; + sock.set_send_buffer_size(value)?; + Ok(()) + } + + fn drop(&mut self, sock: Resource) -> wasmtime::Result<()> { + self.table + .delete(sock) + .context("failed to delete socket resource from table")?; + Ok(()) + } +} diff --git a/crates/wasi/src/p3/sockets/mod.rs b/crates/wasi/src/p3/sockets/mod.rs new file mode 100644 index 00000000..e0dee02e --- /dev/null +++ b/crates/wasi/src/p3/sockets/mod.rs @@ -0,0 +1,71 @@ +use crate::TrappableError; +use crate::p3::bindings::sockets::{ip_name_lookup, types}; +use crate::sockets::{WasiSockets, WasiSocketsView}; +use wasmtime::component::Linker; + +mod conv; +mod host; + +pub type SocketResult = Result; +pub type SocketError = TrappableError; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:sockets/imports` world supported by +/// this module. +/// +/// This is low-level API for advanced use cases, +/// [`wash_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wash_wasi::sockets::{WasiSocketsCtx, WasiSocketsCtxView, WasiSocketsView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wash_wasi::p3::sockets::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// sockets: WasiSocketsCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiSocketsView for MyState { +/// fn sockets(&mut self) -> WasiSocketsCtxView<'_> { +/// WasiSocketsCtxView { +/// ctx: &mut self.sockets, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiSocketsView + 'static, +{ + ip_name_lookup::add_to_linker::<_, WasiSockets>(linker, T::sockets)?; + types::add_to_linker::<_, WasiSockets>(linker, T::sockets)?; + Ok(()) +} diff --git a/crates/wasi/src/p3/wit/deps/cli/command.wit b/crates/wasi/src/p3/wit/deps/cli/command.wit new file mode 100644 index 00000000..f2f613e5 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world command { + @since(version = 0.3.0-rc-2025-09-16) + include imports; + + @since(version = 0.3.0-rc-2025-09-16) + export run; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/environment.wit b/crates/wasi/src/p3/wit/deps/cli/environment.wit new file mode 100644 index 00000000..3763f2f6 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.3.0-rc-2025-09-16) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0-rc-2025-09-16) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.3.0-rc-2025-09-16) + get-initial-cwd: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/exit.wit b/crates/wasi/src/p3/wit/deps/cli/exit.wit new file mode 100644 index 00000000..1efba7d6 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0-rc-2025-09-16) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasi/src/p3/wit/deps/cli/imports.wit b/crates/wasi/src/p3/wit/deps/cli/imports.wit new file mode 100644 index 00000000..660a2dd9 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/imports.wit @@ -0,0 +1,34 @@ +package wasi:cli@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world imports { + @since(version = 0.3.0-rc-2025-09-16) + include wasi:clocks/imports@0.3.0-rc-2025-09-16; + @since(version = 0.3.0-rc-2025-09-16) + include wasi:filesystem/imports@0.3.0-rc-2025-09-16; + @since(version = 0.3.0-rc-2025-09-16) + include wasi:sockets/imports@0.3.0-rc-2025-09-16; + @since(version = 0.3.0-rc-2025-09-16) + include wasi:random/imports@0.3.0-rc-2025-09-16; + + @since(version = 0.3.0-rc-2025-09-16) + import environment; + @since(version = 0.3.0-rc-2025-09-16) + import exit; + @since(version = 0.3.0-rc-2025-09-16) + import stdin; + @since(version = 0.3.0-rc-2025-09-16) + import stdout; + @since(version = 0.3.0-rc-2025-09-16) + import stderr; + @since(version = 0.3.0-rc-2025-09-16) + import terminal-input; + @since(version = 0.3.0-rc-2025-09-16) + import terminal-output; + @since(version = 0.3.0-rc-2025-09-16) + import terminal-stdin; + @since(version = 0.3.0-rc-2025-09-16) + import terminal-stdout; + @since(version = 0.3.0-rc-2025-09-16) + import terminal-stderr; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/run.wit b/crates/wasi/src/p3/wit/deps/cli/run.wit new file mode 100644 index 00000000..631441a3 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface run { + /// Run the program. + @since(version = 0.3.0-rc-2025-09-16) + run: async func() -> result; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/stdio.wit b/crates/wasi/src/p3/wit/deps/cli/stdio.wit new file mode 100644 index 00000000..51e5ae4b --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/stdio.wit @@ -0,0 +1,65 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface types { + @since(version = 0.3.0-rc-2025-09-16) + enum error-code { + /// Input/output error + io, + /// Invalid or incomplete multibyte or wide character + illegal-byte-sequence, + /// Broken pipe + pipe, + } +} + +@since(version = 0.3.0-rc-2025-09-16) +interface stdin { + use types.{error-code}; + + /// Return a stream for reading from stdin. + /// + /// This function returns a stream which provides data read from stdin, + /// and a future to signal read results. + /// + /// If the stream's readable end is dropped the future will resolve to success. + /// + /// If the stream's writable end is dropped the future will either resolve to + /// success if stdin was closed by the writer or to an error-code if reading + /// failed for some other reason. + /// + /// Multiple streams may be active at the same time. The behavior of concurrent + /// reads is implementation-specific. + @since(version = 0.3.0-rc-2025-09-16) + read-via-stream: func() -> tuple, future>>; +} + +@since(version = 0.3.0-rc-2025-09-16) +interface stdout { + use types.{error-code}; + + /// Write the given stream to stdout. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2025-09-16) + write-via-stream: async func(data: stream) -> result<_, error-code>; +} + +@since(version = 0.3.0-rc-2025-09-16) +interface stderr { + use types.{error-code}; + + /// Write the given stream to stderr. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2025-09-16) + write-via-stream: async func(data: stream) -> result<_, error-code>; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/terminal.wit b/crates/wasi/src/p3/wit/deps/cli/terminal.wit new file mode 100644 index 00000000..74c17694 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.3.0-rc-2025-09-16) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0-rc-2025-09-16) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.3.0-rc-2025-09-16) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0-rc-2025-09-16) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0-rc-2025-09-16) +interface terminal-stdin { + @since(version = 0.3.0-rc-2025-09-16) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2025-09-16) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0-rc-2025-09-16) +interface terminal-stdout { + @since(version = 0.3.0-rc-2025-09-16) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2025-09-16) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0-rc-2025-09-16) +interface terminal-stderr { + @since(version = 0.3.0-rc-2025-09-16) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2025-09-16) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit b/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 00000000..a91d495c --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,48 @@ +package wasi:clocks@0.3.0-rc-2025-09-16; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.3.0-rc-2025-09-16) +interface monotonic-clock { + use types.{duration}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.3.0-rc-2025-09-16) + 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. + /// + /// For completeness, this function traps if it's not possible to represent + /// the value of the clock in an `instant`. Consequently, implementations + /// should ensure that the starting time is low enough to avoid the + /// possibility of overflow in practice. + @since(version = 0.3.0-rc-2025-09-16) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0-rc-2025-09-16) + get-resolution: func() -> duration; + + /// Wait until the specified instant has occurred. + @since(version = 0.3.0-rc-2025-09-16) + wait-until: async func( + when: instant, + ); + + /// Wait for the specified duration to elapse. + @since(version = 0.3.0-rc-2025-09-16) + wait-for: async func( + how-long: duration, + ); +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/timezone.wit b/crates/wasi/src/p3/wit/deps/clocks/timezone.wit new file mode 100644 index 00000000..ab8f5c08 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.3.0-rc-2025-09-16; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/types.wit b/crates/wasi/src/p3/wit/deps/clocks/types.wit new file mode 100644 index 00000000..aff7c2a2 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/types.wit @@ -0,0 +1,8 @@ +package wasi:clocks@0.3.0-rc-2025-09-16; +/// This interface common types used throughout wasi:clocks. +@since(version = 0.3.0-rc-2025-09-16) +interface types { + /// A duration of time, in nanoseconds. + @since(version = 0.3.0-rc-2025-09-16) + type duration = u64; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit b/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit new file mode 100644 index 00000000..ea940500 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.3.0-rc-2025-09-16; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.3.0-rc-2025-09-16) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.3.0-rc-2025-09-16) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.3.0-rc-2025-09-16) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0-rc-2025-09-16) + get-resolution: func() -> datetime; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/world.wit b/crates/wasi/src/p3/wit/deps/clocks/world.wit new file mode 100644 index 00000000..a6b885f0 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world imports { + @since(version = 0.3.0-rc-2025-09-16) + import monotonic-clock; + @since(version = 0.3.0-rc-2025-09-16) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit b/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit new file mode 100644 index 00000000..9036e90e --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +interface preopens { + @since(version = 0.3.0-rc-2025-09-16) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0-rc-2025-09-16) + get-directories: func() -> list>; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/types.wit b/crates/wasi/src/p3/wit/deps/filesystem/types.wit new file mode 100644 index 00000000..41d91bee --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/types.wit @@ -0,0 +1,636 @@ +package wasi:filesystem@0.3.0-rc-2025-09-16; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.3.0-rc-2025-09-16) +interface types { + @since(version = 0.3.0-rc-2025-09-16) + use wasi:clocks/wall-clock@0.3.0-rc-2025-09-16.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.3.0-rc-2025-09-16) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.3.0-rc-2025-09-16) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.3.0-rc-2025-09-16) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.3.0-rc-2025-09-16) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0-rc-2025-09-16) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.3.0-rc-2025-09-16) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.3.0-rc-2025-09-16) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.3.0-rc-2025-09-16) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a `stream` which provides the data received from the + /// file, and a `future` providing additional error information in case an + /// error is encountered. + /// + /// If no error is encountered, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If an error is encountered, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> tuple, future>>; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + write-via-stream: async func( + /// Data to write + data: stream, + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result<_, error-code>; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + append-via-stream: async func(data: stream) -> result<_, error-code>; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + advise: async func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + sync-data: async func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + get-flags: async func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + get-type: async func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + set-size: async func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + set-times: async func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0-rc-2025-09-16) + read-directory: async func() -> tuple, future>>; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + sync: async func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + create-directory-at: async func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + stat: async func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2025-09-16) + stat-at: async func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.3.0-rc-2025-09-16) + set-times-at: async func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + link-at: async func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + open-at: async func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + readlink-at: async func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + remove-directory-at: async func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + rename-at: async func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + symlink-at: async func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.3.0-rc-2025-09-16) + unlink-file-at: async func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.3.0-rc-2025-09-16) + is-same-object: async func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.3.0-rc-2025-09-16) + metadata-hash: async func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.3.0-rc-2025-09-16) + metadata-hash-at: async func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/world.wit b/crates/wasi/src/p3/wit/deps/filesystem/world.wit new file mode 100644 index 00000000..87fc7271 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world imports { + @since(version = 0.3.0-rc-2025-09-16) + import types; + @since(version = 0.3.0-rc-2025-09-16) + import preopens; +} diff --git a/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit b/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit new file mode 100644 index 00000000..302151ba --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.3.0-rc-2025-09-16; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2025-09-16) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.3.0-rc-2025-09-16) + get-insecure-seed: func() -> tuple; +} diff --git a/crates/wasi/src/p3/wit/deps/random/insecure.wit b/crates/wasi/src/p3/wit/deps/random/insecure.wit new file mode 100644 index 00000000..39146e39 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.3.0-rc-2025-09-16; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2025-09-16) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.3.0-rc-2025-09-16) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.3.0-rc-2025-09-16) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random/random.wit b/crates/wasi/src/p3/wit/deps/random/random.wit new file mode 100644 index 00000000..fa1f111d --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.3.0-rc-2025-09-16; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2025-09-16) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.3.0-rc-2025-09-16) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.3.0-rc-2025-09-16) + get-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random/world.wit b/crates/wasi/src/p3/wit/deps/random/world.wit new file mode 100644 index 00000000..08c5ed88 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world imports { + @since(version = 0.3.0-rc-2025-09-16) + import random; + + @since(version = 0.3.0-rc-2025-09-16) + import insecure; + + @since(version = 0.3.0-rc-2025-09-16) + import insecure-seed; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 00000000..6a652ff2 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,62 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface ip-name-lookup { + @since(version = 0.3.0-rc-2025-09-16) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0-rc-2025-09-16) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// The returned future will resolve to an error code in case of failure. + /// It will resolve to success once the returned stream is exhausted. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + resolve-addresses: async func(name: string) -> result, error-code>; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/types.wit b/crates/wasi/src/p3/wit/deps/sockets/types.wit new file mode 100644 index 00000000..2ed1912e --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/types.wit @@ -0,0 +1,725 @@ +@since(version = 0.3.0-rc-2025-09-16) +interface types { + @since(version = 0.3.0-rc-2025-09-16) + use wasi:clocks/monotonic-clock@0.3.0-rc-2025-09-16.{duration}; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0-rc-2025-09-16) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + } + + @since(version = 0.3.0-rc-2025-09-16) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0-rc-2025-09-16) + type ipv4-address = tuple; + @since(version = 0.3.0-rc-2025-09-16) + type ipv6-address = tuple; + + @since(version = 0.3.0-rc-2025-09-16) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0-rc-2025-09-16) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0-rc-2025-09-16) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0-rc-2025-09-16) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0-rc-2025-09-16) + resource tcp-socket { + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + create: static func(address-family: ip-address-family) -> result; + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Start listening and return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + listen: func() -> result, error-code>; + + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + send: async func(data: stream) -> result<_, error-code>; + + /// Read data from peer. + /// + /// This function returns a `stream` which provides the data received from the + /// socket, and a `future` providing additional error information in case the + /// socket is closed abnormally. + /// + /// If the socket is closed normally, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If the socket is closed abnormally, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// `receive` is meant to be called only once per socket. If it is called more + /// than once, the subsequent calls return a new `stream` that fails as if it + /// were closed abnormally. + /// + /// If the caller is not expecting to receive any data from the peer, + /// they may drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + receive: func() -> tuple, future>>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + get-local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + get-remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0-rc-2025-09-16) + get-is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2025-09-16) + get-address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0-rc-2025-09-16) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0-rc-2025-09-16) + get-keep-alive-enabled: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2025-09-16) + get-keep-alive-idle-time: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2025-09-16) + get-keep-alive-interval: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2025-09-16) + get-keep-alive-count: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2025-09-16) + get-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2025-09-16) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2025-09-16) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0-rc-2025-09-16) + resource udp-socket { + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + create: static func(address-family: ip-address-family) -> result; + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + disconnect: func() -> result<_, error-code>; + + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + send: async func(data: list, remote-address: option) -> result<_, error-code>; + + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + receive: async func() -> result, ip-socket-address>, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + get-local-address: func() -> result; + + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2025-09-16) + get-remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2025-09-16) + get-address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2025-09-16) + get-unicast-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2025-09-16) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2025-09-16) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2025-09-16) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/world.wit b/crates/wasi/src/p3/wit/deps/sockets/world.wit new file mode 100644 index 00000000..44cc427e --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/world.wit @@ -0,0 +1,9 @@ +package wasi:sockets@0.3.0-rc-2025-09-16; + +@since(version = 0.3.0-rc-2025-09-16) +world imports { + @since(version = 0.3.0-rc-2025-09-16) + import types; + @since(version = 0.3.0-rc-2025-09-16) + import ip-name-lookup; +} diff --git a/crates/wasi/src/p3/wit/package.wit b/crates/wasi/src/p3/wit/package.wit new file mode 100644 index 00000000..74db4ae9 --- /dev/null +++ b/crates/wasi/src/p3/wit/package.wit @@ -0,0 +1,2 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi; diff --git a/crates/wasi/src/random.rs b/crates/wasi/src/random.rs new file mode 100644 index 00000000..b9880a14 --- /dev/null +++ b/crates/wasi/src/random.rs @@ -0,0 +1,136 @@ +use cap_rand::{Rng as _, RngCore, SeedableRng as _}; +use wasmtime::component::HasData; + +/// A helper struct which implements [`HasData`] for the `wasi:random` APIs. +/// +/// This can be useful when directly calling `add_to_linker` functions directly, +/// such as [`wash_wasi::p2::bindings::random::random::add_to_linker`] as +/// the `D` type parameter. See [`HasData`] for more information about the type +/// parameter's purpose. +/// +/// When using this type you can skip the [`WasiRandomView`] trait, for +/// example. +/// +/// # Examples +/// +/// ``` +/// use wasmtime::component::Linker; +/// use wasmtime::{Engine, Result, Config}; +/// use wash_wasi::random::*; +/// +/// struct MyStoreState { +/// random: WasiRandomCtx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// let mut linker = Linker::new(&engine); +/// +/// wash_wasi::p2::bindings::random::random::add_to_linker::( +/// &mut linker, +/// |state| &mut state.random, +/// )?; +/// Ok(()) +/// } +/// ``` +pub struct WasiRandom; + +impl HasData for WasiRandom { + type Data<'a> = &'a mut WasiRandomCtx; +} + +pub struct WasiRandomCtx { + pub(crate) random: Box, + pub(crate) insecure_random: Box, + pub(crate) insecure_random_seed: u128, +} + +impl Default for WasiRandomCtx { + fn default() -> Self { + // For the insecure random API, use `SmallRng`, which is fast. It's + // also insecure, but that's the deal here. + let insecure_random = Box::new( + cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) + .unwrap(), + ); + // For the insecure random seed, use a `u128` generated from + // `thread_rng()`, so that it's not guessable from the insecure_random + // API. + let insecure_random_seed = + cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); + Self { + random: thread_rng(), + insecure_random, + insecure_random_seed, + } + } +} + +pub trait WasiRandomView: Send { + fn random(&mut self) -> &mut WasiRandomCtx; +} + +impl WasiRandomView for WasiRandomCtx { + fn random(&mut self) -> &mut WasiRandomCtx { + self + } +} + +/// Implement `insecure-random` using a deterministic cycle of bytes. +pub struct Deterministic { + cycle: std::iter::Cycle>, +} + +impl Deterministic { + pub fn new(bytes: Vec) -> Self { + Deterministic { + cycle: bytes.into_iter().cycle(), + } + } +} + +impl RngCore for Deterministic { + fn next_u32(&mut self) -> u32 { + let b0 = self.cycle.next().expect("infinite sequence"); + let b1 = self.cycle.next().expect("infinite sequence"); + let b2 = self.cycle.next().expect("infinite sequence"); + let b3 = self.cycle.next().expect("infinite sequence"); + ((b0 as u32) << 24) + ((b1 as u32) << 16) + ((b2 as u32) << 8) + (b3 as u32) + } + fn next_u64(&mut self) -> u64 { + let w0 = self.next_u32(); + let w1 = self.next_u32(); + ((w0 as u64) << 32) + (w1 as u64) + } + fn fill_bytes(&mut self, buf: &mut [u8]) { + for b in buf.iter_mut() { + *b = self.cycle.next().expect("infinite sequence"); + } + } + fn try_fill_bytes(&mut self, buf: &mut [u8]) -> Result<(), cap_rand::Error> { + self.fill_bytes(buf); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn deterministic() { + let mut det = Deterministic::new(vec![1, 2, 3, 4]); + let mut buf = vec![0; 1024]; + det.try_fill_bytes(&mut buf).expect("get randomness"); + for (ix, b) in buf.iter().enumerate() { + assert_eq!(*b, (ix % 4) as u8 + 1) + } + } +} + +pub fn thread_rng() -> Box { + use cap_rand::{Rng, SeedableRng}; + let mut rng = cap_rand::thread_rng(cap_rand::ambient_authority()); + Box::new(cap_rand::rngs::StdRng::from_seed(rng.r#gen())) +} diff --git a/crates/wasi/src/runtime.rs b/crates/wasi/src/runtime.rs new file mode 100644 index 00000000..f5a99a7c --- /dev/null +++ b/crates/wasi/src/runtime.rs @@ -0,0 +1,189 @@ +//! This module provides an "ambient Tokio runtime" +//! [`with_ambient_tokio_runtime`]. Embedders of wasmtime-wasi may do so from +//! synchronous Rust, and not use tokio directly. The implementation of +//! wasmtime-wasi requires a tokio executor in a way that is [deeply tied to +//! its +//! design](https://github.com/bytecodealliance/wasmtime/issues/7973#issuecomment-1960513214). +//! When used from a synchronous wasmtime context, this module provides the +//! wrapper function [`in_tokio`] used throughout the shim implementations of +//! synchronous component binding `Host` traits in terms of the async ones. +//! +//! This module also provides a thin wrapper on tokio's tasks. +//! [`AbortOnDropJoinHandle`], which is exactly like a +//! [`tokio::task::JoinHandle`] except for the obvious behavioral change. This +//! whole crate, and any child crates which spawn tasks as part of their +//! implementations, should please use this crate's [`spawn`] and +//! [`spawn_blocking`] over tokio's. so we wanted the type name to stick out +//! if someone misses it. +//! +//! Each of these facilities should be used by dependencies of wasmtime-wasi +//! which when implementing component bindings. + +use std::future::Future; +use std::pin::Pin; +use std::sync::LazyLock; +use std::task::{Context, Poll, Waker}; + +pub(crate) static RUNTIME: LazyLock = LazyLock::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_time() + .enable_io() + .build() + .unwrap() +}); + +/// Exactly like a [`tokio::task::JoinHandle`], except that it aborts the task when +/// the handle is dropped. +/// +/// This behavior makes it easier to tie a worker task to the lifetime of a Resource +/// by keeping this handle owned by the Resource. +#[derive(Debug)] +pub struct AbortOnDropJoinHandle(tokio::task::JoinHandle); +impl AbortOnDropJoinHandle { + /// Abort the task and wait for it to finish. Optionally returns the result + /// of the task if it ran to completion prior to being aborted. + pub async fn cancel(mut self) -> Option { + self.0.abort(); + + match (&mut self.0).await { + Ok(value) => Some(value), + Err(err) if err.is_cancelled() => None, + Err(err) => std::panic::resume_unwind(err.into_panic()), + } + } +} +impl Drop for AbortOnDropJoinHandle { + fn drop(&mut self) { + self.0.abort() + } +} +impl std::ops::Deref for AbortOnDropJoinHandle { + type Target = tokio::task::JoinHandle; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for AbortOnDropJoinHandle { + fn deref_mut(&mut self) -> &mut tokio::task::JoinHandle { + &mut self.0 + } +} +impl From> for AbortOnDropJoinHandle { + fn from(jh: tokio::task::JoinHandle) -> Self { + AbortOnDropJoinHandle(jh) + } +} +impl Future for AbortOnDropJoinHandle { + type Output = T; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match Pin::new(&mut self.as_mut().0).poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(r) => Poll::Ready(r.expect("child task panicked")), + } + } +} + +pub fn spawn(f: F) -> AbortOnDropJoinHandle +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + let j = with_ambient_tokio_runtime(|| tokio::task::spawn(f)); + AbortOnDropJoinHandle(j) +} + +pub fn spawn_blocking(f: F) -> AbortOnDropJoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + let j = with_ambient_tokio_runtime(|| tokio::task::spawn_blocking(f)); + AbortOnDropJoinHandle(j) +} + +pub fn in_tokio(f: F) -> F::Output { + match tokio::runtime::Handle::try_current() { + Ok(h) => { + let _enter = h.enter(); + h.block_on(f) + } + // The `yield_now` here is non-obvious and if you're reading this + // you're likely curious about why it's here. This is currently required + // to get some features of "sync mode" working correctly, such as with + // the CLI. To illustrate why this is required, consider a program + // organized as: + // + // * A program has a `pollable` that it's waiting on. + // * This `pollable` is always ready . + // * Actually making the corresponding operation ready, however, + // requires some background work on Tokio's part. + // * The program is looping on "wait for readiness" coupled with + // performing the operation. + // + // In this situation this program ends up infinitely looping in waiting + // for pollables. The reason appears to be that when we enter the tokio + // runtime here it doesn't necessary yield to background work because + // the provided future `f` is ready immediately. The future `f` will run + // through the list of pollables and determine one of them is ready. + // + // Historically this happened with UDP sockets. A test send a datagram + // from one socket to another and the other socket infinitely didn't + // receive the data. This appeared to be because the server socket was + // waiting on `READABLE | WRITABLE` (which is itself a bug but ignore + // that) and the socket was currently in the "writable" state but never + // ended up receiving a notification for the "readable" state. Moving + // the socket to "readable" would require Tokio to perform some + // background work via epoll/kqueue/handle events but if the future + // provided here is always ready, then that never happened. + // + // Thus the `yield_now()` is an attempt to force Tokio to go do some + // background work eventually and look at new interest masks for + // example. This is a bit of a kludge but everything's already a bit + // wonky in synchronous mode anyway. Note that this is hypothesized to + // not be an issue in async mode because async mode typically has the + // Tokio runtime in a separate thread or otherwise participating in a + // larger application, it's only here in synchronous mode where we + // effectively own the runtime that we need some special care. + Err(_) => { + let _enter = RUNTIME.enter(); + RUNTIME.block_on(async move { + tokio::task::yield_now().await; + f.await + }) + } + } +} + +/// Executes the closure `f` with an "ambient Tokio runtime" which basically +/// means that if code in `f` tries to get a runtime `Handle` it'll succeed. +/// +/// If a `Handle` is already available, e.g. in async contexts, then `f` is run +/// immediately. Otherwise for synchronous contexts this crate's fallback +/// runtime is configured and then `f` is executed. +pub fn with_ambient_tokio_runtime(f: impl FnOnce() -> R) -> R { + match tokio::runtime::Handle::try_current() { + Ok(_) => f(), + Err(_) => { + let _enter = RUNTIME.enter(); + f() + } + } +} + +/// Attempts to get the result of a `future`. +/// +/// This function does not block and will poll the provided future once. If the +/// result is here then `Some` is returned, otherwise `None` is returned. +/// +/// Note that by polling `future` this means that `future` must be re-polled +/// later if it's to wake up a task. +pub fn poll_noop(future: Pin<&mut F>) -> Option +where + F: Future, +{ + let mut task = Context::from_waker(Waker::noop()); + match future.poll(&mut task) { + Poll::Ready(result) => Some(result), + Poll::Pending => None, + } +} diff --git a/crates/wasi/src/sockets/loopback/mod.rs b/crates/wasi/src/sockets/loopback/mod.rs new file mode 100644 index 00000000..704123a9 --- /dev/null +++ b/crates/wasi/src/sockets/loopback/mod.rs @@ -0,0 +1,132 @@ +use crate::sockets::util::ErrorCode; +use core::net::{IpAddr, SocketAddr}; +use core::num::NonZeroU16; +use std::collections::{HashMap, hash_map}; +use tokio::sync::{OwnedSemaphorePermit, mpsc}; + +mod tcp; +mod udp; + +pub use tcp::*; +pub use udp::*; + +#[derive(Default)] +pub struct Network { + pub tcp_ipv4: HashMap, + pub tcp_ipv6: HashMap, + pub udp_ipv4: HashMap, + pub udp_ipv6: HashMap, +} + +fn bind(net: &mut HashMap, port: u16, ep: T) -> Result { + if let Some(port) = NonZeroU16::new(port) { + let hash_map::Entry::Vacant(entry) = net.entry(port) else { + return Err(ErrorCode::AddressInUse); + }; + entry.insert(ep); + Ok(port) + } else { + for port in (1..=u16::MAX).rev() { + let port = NonZeroU16::new(port).unwrap(); + if let hash_map::Entry::Vacant(entry) = net.entry(port) { + entry.insert(ep); + return Ok(port); + }; + } + Err(ErrorCode::AddressInUse) + } +} + +impl Network { + fn get_tcp_net(&self, ip: IpAddr) -> &HashMap { + match ip { + IpAddr::V4(..) => &self.tcp_ipv4, + IpAddr::V6(..) => &self.tcp_ipv6, + } + } + + fn get_tcp_net_mut(&mut self, ip: IpAddr) -> &mut HashMap { + match ip { + IpAddr::V4(..) => &mut self.tcp_ipv4, + IpAddr::V6(..) => &mut self.tcp_ipv6, + } + } + + fn get_udp_net(&self, ip: IpAddr) -> &HashMap { + match ip { + IpAddr::V4(..) => &self.udp_ipv4, + IpAddr::V6(..) => &self.udp_ipv6, + } + } + + fn get_udp_net_mut(&mut self, ip: IpAddr) -> &mut HashMap { + match ip { + IpAddr::V4(..) => &mut self.udp_ipv4, + IpAddr::V6(..) => &mut self.udp_ipv6, + } + } + + pub fn bind_tcp(&mut self, mut addr: SocketAddr) -> Result { + let net = self.get_tcp_net_mut(addr.ip()); + let port = bind(net, addr.port(), TcpEndpoint::Bound)?; + addr.set_port(port.into()); + Ok(addr) + } + + pub fn bind_udp( + &mut self, + mut addr: SocketAddr, + ) -> Result< + ( + SocketAddr, + mpsc::UnboundedReceiver<(UdpDatagram, OwnedSemaphorePermit)>, + ), + ErrorCode, + > { + let net = self.get_udp_net_mut(addr.ip()); + let (tx, rx) = mpsc::unbounded_channel(); + let ep = UdpEndpoint { + tx, + connected_address: None, + }; + let port = bind(net, addr.port(), ep)?; + addr.set_port(port.into()); + Ok((addr, rx)) + } + + pub fn connect_tcp(&mut self, addr: &SocketAddr) -> Result<&mpsc::Sender, ErrorCode> { + let net = self.get_tcp_net(addr.ip()); + let Some(port) = NonZeroU16::new(addr.port()) else { + return Err(ErrorCode::InvalidArgument); + }; + let Some(TcpEndpoint::Listening(tx)) = net.get(&port) else { + return Err(ErrorCode::ConnectionRefused); + }; + Ok(tx) + } + + pub fn connect_udp( + &mut self, + local_address: &SocketAddr, + remote_address: &SocketAddr, + ) -> Result>, ErrorCode> + { + let net = self.get_udp_net(remote_address.ip()); + let Some(port) = NonZeroU16::new(remote_address.port()) else { + return Err(ErrorCode::InvalidArgument); + }; + let Some(UdpEndpoint { + tx, + connected_address, + }) = net.get(&port) + else { + return Ok(None); + }; + if let Some(addr) = connected_address { + if local_address != addr { + return Ok(None); + } + } + Ok(Some(tx)) + } +} diff --git a/crates/wasi/src/sockets/loopback/tcp.rs b/crates/wasi/src/sockets/loopback/tcp.rs new file mode 100644 index 00000000..04b6ef78 --- /dev/null +++ b/crates/wasi/src/sockets/loopback/tcp.rs @@ -0,0 +1,529 @@ +use crate::runtime::with_ambient_tokio_runtime; +use crate::sockets::SocketAddressFamily; +use crate::sockets::loopback::Network; +use crate::sockets::tcp::ConnectingTcpStream; +use crate::sockets::util::ErrorCode; +use anyhow::Context as _; +use bytes::Bytes; +use core::mem; +use core::net::SocketAddr; +use core::num::NonZeroU16; +use core::pin::Pin; +use core::task::{Context, Poll, Waker}; +use std::collections::hash_map; +use std::sync::Arc; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc}; + +#[derive(Debug)] +pub enum TcpEndpoint { + Bound, + Listening(mpsc::Sender), +} + +pub struct TcpConn { + pub local_address: SocketAddr, + pub remote_address: SocketAddr, + pub rx: mpsc::UnboundedReceiver<(Bytes, OwnedSemaphorePermit)>, + pub tx: mpsc::UnboundedSender<(Bytes, OwnedSemaphorePermit)>, +} + +impl TcpConn { + // Returns a tuple of local side of the connection and the remote side of the connection + fn pair(local_address: SocketAddr, remote_address: SocketAddr) -> (Self, Self) { + let (local_tx, remote_rx) = mpsc::unbounded_channel(); + let (remote_tx, local_rx) = mpsc::unbounded_channel(); + ( + Self { + local_address, + remote_address, + rx: local_rx, + tx: local_tx, + }, + Self { + local_address: remote_address, + remote_address: local_address, + rx: remote_rx, + tx: remote_tx, + }, + ) + } +} + +pub enum TcpState { + BindStarted(SocketAddr), + Bound(SocketAddr), + ListenStarted { + local_address: SocketAddr, + rx: Option>, + }, + Listening { + local_address: SocketAddr, + rx: mpsc::Receiver, + pending: Option, + }, + Connecting { + local_address: SocketAddr, + remote_address: SocketAddr, + future: Option> + Send>>>, + }, + ConnectReady { + local_address: SocketAddr, + remote_address: SocketAddr, + result: std::io::Result, + }, + Connected { + conn: TcpConn, + accepted: bool, + }, + P2Streaming { + local_address: SocketAddr, + remote_address: SocketAddr, + accepted: bool, + permits: Arc, + rx: Arc>>>, + tx: Arc>>>, + }, + Closed, +} + +pub struct TcpSocket { + pub state: TcpState, + pub listen_backlog_size: u32, + pub keep_alive_enabled: bool, + pub keep_alive_idle_time: u64, + pub keep_alive_interval: u64, + pub keep_alive_count: u32, + pub hop_limit: u8, + pub receive_buffer_size: u64, + pub send_buffer_size: u32, + pub(crate) family: SocketAddressFamily, +} + +impl TcpSocket { + pub const MAX_SEND_BUFFER_SIZE: u32 = 0x10_0000; + pub const MAX_LISTEN_BACKLOG_SIZE: u32 = 4096; + + pub fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match self.state { + TcpState::BindStarted(addr) => { + self.state = TcpState::Bound(addr); + Ok(()) + } + _ => Err(ErrorCode::NotInProgress), + } + } + + pub fn start_connect<'a>( + &mut self, + addr: &SocketAddr, + loopback: &'a mut Network, + ) -> Result<&'a mpsc::Sender, ErrorCode> { + let TcpState::Bound(local_address) = self.state else { + return Err(ErrorCode::InvalidState); + }; + let tx = loopback.connect_tcp(addr)?; + self.state = TcpState::Connecting { + local_address, + remote_address: *addr, + future: None, + }; + Ok(tx) + } + + pub fn set_pending_connect( + &mut self, + future: impl Future> + Send + 'static, + ) -> Result<(), ErrorCode> { + match &mut self.state { + TcpState::Connecting { + future: slot @ None, + .. + } => { + *slot = Some(Box::pin(future)); + Ok(()) + } + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn take_pending_connect( + &mut self, + ) -> Result>, ErrorCode> { + match mem::replace(&mut self.state, TcpState::Closed) { + TcpState::ConnectReady { + local_address, + remote_address, + result, + } => { + self.state = TcpState::Connecting { + local_address, + remote_address, + future: None, + }; + Ok(Some(result)) + } + TcpState::Connecting { + local_address, + remote_address, + future: Some(mut future), + } => { + let mut cx = Context::from_waker(Waker::noop()); + match with_ambient_tokio_runtime(|| future.as_mut().poll(&mut cx)) { + Poll::Ready(result) => { + self.state = TcpState::Connecting { + local_address, + remote_address, + future: None, + }; + Ok(Some(result)) + } + Poll::Pending => { + self.state = TcpState::Connecting { + local_address, + remote_address, + future: Some(future), + }; + Ok(None) + } + } + } + state => { + self.state = state; + Err(ErrorCode::NotInProgress) + } + } + } + + pub fn finish_connect( + &mut self, + result: std::io::Result, + loopback: &mut Network, + ) -> Result<(), ErrorCode> { + let TcpState::Connecting { + local_address, + remote_address, + future: None, + } = self.state + else { + return Err(ErrorCode::InvalidState); + }; + match result { + Ok(ConnectingTcpStream::Network(..)) => Err(ErrorCode::InvalidState), + Ok(ConnectingTcpStream::Loopback(tx)) => { + let (clt, srv) = TcpConn::pair(local_address, remote_address); + tx.send(srv); + self.state = TcpState::Connected { + conn: clt, + accepted: false, + }; + Ok(()) + } + Err(err) => { + self.state = TcpState::Closed; + let net = loopback.get_tcp_net_mut(local_address.ip()); + let Some(port) = NonZeroU16::new(local_address.port()) else { + return Err(ErrorCode::InvalidState); + }; + let Some(TcpEndpoint::Bound) = net.remove(&port) else { + return Err(ErrorCode::InvalidState); + }; + Err(ErrorCode::from(err)) + } + } + } + + pub fn start_listen(&mut self, loopback: &mut Network) -> Result<(), ErrorCode> { + let TcpState::Bound(addr) = self.state else { + return Err(ErrorCode::InvalidState); + }; + let net = loopback.get_tcp_net_mut(addr.ip()); + let Some(port) = NonZeroU16::new(addr.port()) else { + return Err(ErrorCode::InvalidArgument); + }; + let hash_map::Entry::Occupied(mut entry) = net.entry(port) else { + return Err(ErrorCode::InvalidState); + }; + let TcpEndpoint::Bound = entry.get() else { + return Err(ErrorCode::InvalidState); + }; + + let cap = self.listen_backlog_size.min(Self::MAX_LISTEN_BACKLOG_SIZE); + let cap = cap.try_into().unwrap_or(Semaphore::MAX_PERMITS); + let (tx, rx) = mpsc::channel(cap); + entry.insert(TcpEndpoint::Listening(tx)); + self.state = TcpState::ListenStarted { + local_address: addr, + rx: Some(rx), + }; + Ok(()) + } + + pub fn finish_listen(&mut self) -> Result<(), ErrorCode> { + let TcpState::ListenStarted { + local_address, + ref mut rx, + } = self.state + else { + return Err(ErrorCode::NotInProgress); + }; + let Some(rx) = rx.take() else { + return Err(ErrorCode::InvalidState); + }; + self.state = TcpState::Listening { + local_address, + rx, + pending: None, + }; + Ok(()) + } + + pub fn accept(&mut self) -> Result, ErrorCode> { + let TcpState::Listening { + rx, + pending, + local_address, + } = &mut self.state + else { + return Err(ErrorCode::InvalidState); + }; + + let conn = if let Some(conn) = pending.take() { + conn + } else { + match rx.try_recv() { + Ok(conn) => conn, + Err(TryRecvError::Empty) => return Ok(None), + Err(TryRecvError::Disconnected) => return Err(ErrorCode::ConnectionReset), + } + }; + if conn.local_address != *local_address { + return Err(ErrorCode::Unknown); + } + Ok(Some(Self { + state: TcpState::Connected { + conn, + accepted: true, + }, + listen_backlog_size: self.listen_backlog_size, + keep_alive_enabled: self.keep_alive_enabled, + keep_alive_idle_time: self.keep_alive_idle_time, + keep_alive_interval: self.keep_alive_interval, + keep_alive_count: self.keep_alive_count, + hop_limit: self.hop_limit, + receive_buffer_size: self.receive_buffer_size, + send_buffer_size: self.send_buffer_size, + family: self.family, + })) + } + + pub fn local_address(&self) -> Result { + match &self.state { + TcpState::Bound(local_address) + | TcpState::Connected { + conn: TcpConn { local_address, .. }, + .. + } + | TcpState::Listening { local_address, .. } + | TcpState::P2Streaming { local_address, .. } => Ok(*local_address), + //#[cfg(feature = "p3")] + //TcpState::Receiving(stream) => Ok(stream.local_addr()?), + //#[cfg(feature = "p3")] + //TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn remote_address(&self) -> Result { + match &self.state { + TcpState::Connected { + conn: TcpConn { remote_address, .. }, + .. + } + | TcpState::P2Streaming { remote_address, .. } => Ok(*remote_address), + //#[cfg(feature = "p3")] + //TcpState::Receiving(socket) => Ok(socket), + //#[cfg(feature = "p3")] + //TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn is_listening(&self) -> bool { + matches!(self.state, TcpState::Listening { .. }) + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + self.family + } + + pub fn set_listen_backlog_size(&mut self, value: u64) -> Result<(), ErrorCode> { + let value = value.try_into().unwrap_or(u32::MAX); + match &self.state { + TcpState::Bound(..) => { + self.listen_backlog_size = value; + Ok(()) + } + TcpState::Listening { .. } => Err(ErrorCode::NotSupported), + //#[cfg(feature = "p3")] + //TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn keep_alive_enabled(&self) -> Result { + Ok(self.keep_alive_enabled) + } + + pub fn set_keep_alive_enabled(&mut self, value: bool) -> Result<(), ErrorCode> { + self.keep_alive_enabled = value; + Ok(()) + } + + pub fn keep_alive_idle_time(&self) -> Result { + Ok(self.keep_alive_idle_time) + } + + pub fn set_keep_alive_idle_time(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.keep_alive_idle_time = value; + Ok(()) + } + + pub fn keep_alive_interval(&self) -> Result { + Ok(self.keep_alive_interval) + } + + pub fn set_keep_alive_interval(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.keep_alive_interval = value; + Ok(()) + } + + pub fn keep_alive_count(&self) -> Result { + Ok(self.keep_alive_count) + } + + pub fn set_keep_alive_count(&mut self, value: u32) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.keep_alive_count = value; + Ok(()) + } + + pub fn hop_limit(&self) -> Result { + Ok(self.hop_limit) + } + + pub fn set_hop_limit(&mut self, value: u8) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.hop_limit = value; + Ok(()) + } + + pub fn receive_buffer_size(&self) -> Result { + Ok(self.receive_buffer_size) + } + + pub fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.receive_buffer_size = value; + Ok(()) + } + + pub fn send_buffer_size(&self) -> Result { + Ok(self.send_buffer_size.into()) + } + + pub fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + let mut value = value + .try_into() + .unwrap_or(Self::MAX_SEND_BUFFER_SIZE) + .min(Self::MAX_SEND_BUFFER_SIZE); + if let TcpState::P2Streaming { permits, .. } = &self.state { + let surplus = self.send_buffer_size.saturating_sub(value); + if surplus > 0 { + let reduced = permits.forget_permits(surplus as _) as _; + let leaked = surplus.saturating_sub(reduced); + value = value.checked_add(leaked).ok_or(ErrorCode::Unknown)?; + } else { + permits.add_permits(value.saturating_sub(self.send_buffer_size) as _); + } + } + self.send_buffer_size = value; + Ok(()) + } + + pub async fn ready(&mut self) { + match &mut self.state { + TcpState::BindStarted(..) + | TcpState::Bound(..) + | TcpState::ListenStarted { .. } + | TcpState::Listening { + pending: Some(..), .. + } + | TcpState::Connecting { future: None, .. } + | TcpState::ConnectReady { .. } + | TcpState::Connected { .. } + | TcpState::P2Streaming { .. } + | TcpState::Closed => {} + TcpState::Connecting { + local_address, + remote_address, + future: Some(future), + } => { + let result = future.as_mut().await; + self.state = TcpState::ConnectReady { + local_address: *local_address, + remote_address: *remote_address, + result, + }; + } + TcpState::Listening { + rx, + pending: pending @ None, + .. + } => *pending = rx.recv().await, + } + } + + pub fn drop(self, loopback: &mut Network) -> wasmtime::Result<()> { + let addr = match self.state { + TcpState::BindStarted(local_address) + | TcpState::Bound(local_address) + | TcpState::ListenStarted { local_address, .. } + | TcpState::Listening { local_address, .. } + | TcpState::Connecting { local_address, .. } + | TcpState::ConnectReady { local_address, .. } + | TcpState::Connected { + accepted: false, + conn: TcpConn { local_address, .. }, + .. + } + | TcpState::P2Streaming { + accepted: false, + local_address, + .. + } => local_address, + TcpState::Connected { accepted: true, .. } + | TcpState::P2Streaming { accepted: true, .. } + | TcpState::Closed => return Ok(()), + }; + let net = loopback.get_tcp_net_mut(addr.ip()); + let port = NonZeroU16::new(addr.port()).context("local address port cannot be 0")?; + net.remove(&port); + Ok(()) + } +} diff --git a/crates/wasi/src/sockets/loopback/udp.rs b/crates/wasi/src/sockets/loopback/udp.rs new file mode 100644 index 00000000..198bc9f2 --- /dev/null +++ b/crates/wasi/src/sockets/loopback/udp.rs @@ -0,0 +1,319 @@ +use crate::p2; +use crate::sockets::loopback::Network; +use crate::sockets::util::{ErrorCode, is_valid_address_family, is_valid_remote_address}; +use crate::sockets::{SocketAddrCheck, SocketAddressFamily}; +use anyhow::Context as _; +use core::mem; +use core::net::SocketAddr; +use core::num::NonZeroU16; +use std::sync::Arc; +use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc}; + +pub struct UdpEndpoint { + pub tx: mpsc::UnboundedSender<(UdpDatagram, OwnedSemaphorePermit)>, + pub connected_address: Option, +} + +pub struct UdpDatagram { + pub source_address: SocketAddr, + pub data: Vec, +} + +pub enum UdpState { + BindStarted { + local_address: SocketAddr, + rx: mpsc::UnboundedReceiver<(UdpDatagram, OwnedSemaphorePermit)>, + }, + Bound { + local_address: SocketAddr, + rx: Arc>>, + permits: Arc, + }, + Connected { + local_address: SocketAddr, + remote_address: SocketAddr, + rx: Arc>>, + permits: Arc, + }, + Closed, +} + +pub struct UdpSocket { + pub state: UdpState, + pub hop_limit: u8, + pub receive_buffer_size: u64, + pub send_buffer_size: u32, + pub(crate) family: SocketAddressFamily, + pub(crate) socket_addr_check: Option, +} + +impl UdpSocket { + pub const MAX_SEND_BUFFER_SIZE: u32 = 0x1_0000; + + pub fn p2_udp_streams( + &self, + remote_address: Option, + ) -> Result< + ( + p2::udp::LoopbackIncomingDatagramStream, + p2::udp::LoopbackOutgoingDatagramStream, + ), + ErrorCode, + > { + let Self { + state: + UdpState::Bound { + local_address, + rx, + permits, + } + | UdpState::Connected { + local_address, + rx, + permits, + .. + }, + .. + } = self + else { + return Err(ErrorCode::InvalidState.into()); + }; + Ok(( + p2::udp::LoopbackIncomingDatagramStream { + remote_address, + rx: Arc::clone(rx), + received: None, + }, + p2::udp::LoopbackOutgoingDatagramStream { + local_address: *local_address, + remote_address, + permits: Arc::clone(permits), + permit: None, + family: self.address_family(), + socket_addr_check: self.socket_addr_check().cloned(), + }, + )) + } + + pub fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match mem::replace(&mut self.state, UdpState::Closed) { + UdpState::BindStarted { local_address, rx } => { + let permits = Arc::new(Semaphore::new(self.send_buffer_size as _)); + let rx = Arc::new(tokio::sync::Mutex::new(rx)); + self.state = UdpState::Bound { + local_address, + rx, + permits, + }; + Ok(()) + } + state => { + self.state = state; + Err(ErrorCode::NotInProgress) + } + } + } + + pub fn is_connected(&self) -> bool { + matches!(self.state, UdpState::Connected { .. }) + } + + pub fn is_bound(&self) -> bool { + matches!( + self.state, + UdpState::Connected { .. } | UdpState::Bound { .. } + ) + } + + pub fn disconnect(&mut self, loopback: &mut Network) -> Result<(), ErrorCode> { + match mem::replace(&mut self.state, UdpState::Closed) { + UdpState::Connected { + local_address, + rx, + permits, + .. + } => { + let net = loopback.get_udp_net_mut(local_address.ip()); + let Some(port) = NonZeroU16::new(local_address.port()) else { + return Err(ErrorCode::InvalidState); + }; + let Some(UdpEndpoint { + connected_address: connected_address @ Some(..), + .. + }) = net.get_mut(&port) + else { + return Err(ErrorCode::InvalidState); + }; + *connected_address = None; + + self.state = UdpState::Bound { + local_address, + rx, + permits, + }; + Ok(()) + } + state => { + self.state = state; + Err(ErrorCode::InvalidState) + } + } + } + + pub fn connect(&mut self, addr: SocketAddr, loopback: &mut Network) -> Result<(), ErrorCode> { + if !is_valid_address_family(addr.ip(), self.family) || !is_valid_remote_address(addr) { + return Err(ErrorCode::InvalidArgument); + } + + match mem::replace(&mut self.state, UdpState::Closed) { + UdpState::Bound { + local_address, + rx, + permits, + } + | UdpState::Connected { + local_address, + rx, + permits, + .. + } => { + let net = loopback.get_udp_net_mut(local_address.ip()); + let Some(port) = NonZeroU16::new(local_address.port()) else { + return Err(ErrorCode::InvalidState); + }; + let Some(UdpEndpoint { + connected_address, .. + }) = net.get_mut(&port) + else { + return Err(ErrorCode::InvalidState); + }; + *connected_address = Some(addr); + + self.state = UdpState::Connected { + local_address, + remote_address: addr, + rx, + permits, + }; + Ok(()) + } + state => { + self.state = state; + Err(ErrorCode::InvalidState) + } + } + } + + #[cfg(feature = "p3")] + pub fn send(&self, _buf: Vec) -> impl Future> + use<> { + async { todo!() } + } + + #[cfg(feature = "p3")] + pub fn send_to( + &self, + _buf: Vec, + _addr: SocketAddr, + ) -> impl Future> + use<> { + async { todo!() } + } + + #[cfg(feature = "p3")] + pub fn receive( + &self, + ) -> impl Future, SocketAddr), ErrorCode>> + use<> { + async { todo!() } + } + + pub fn local_address(&self) -> Result { + match &self.state { + UdpState::Bound { local_address, .. } | UdpState::Connected { local_address, .. } => { + Ok(*local_address) + } + _ => Err(ErrorCode::InvalidState), + } + } + + pub fn remote_address(&self) -> Result { + match &self.state { + UdpState::Connected { remote_address, .. } => Ok(*remote_address), + _ => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + self.family + } + + pub fn unicast_hop_limit(&self) -> Result { + Ok(self.hop_limit) + } + + pub fn set_unicast_hop_limit(&mut self, value: u8) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.hop_limit = value; + Ok(()) + } + + pub fn receive_buffer_size(&self) -> Result { + Ok(self.receive_buffer_size) + } + + pub fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + self.receive_buffer_size = value; + Ok(()) + } + + pub fn send_buffer_size(&self) -> Result { + Ok(self.send_buffer_size.into()) + } + + pub fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + let mut value = value + .try_into() + .unwrap_or(Self::MAX_SEND_BUFFER_SIZE) + .min(Self::MAX_SEND_BUFFER_SIZE); + if let UdpState::Bound { permits, .. } | UdpState::Connected { permits, .. } = &self.state { + let surplus = self.send_buffer_size.saturating_sub(value); + if surplus > 0 { + let reduced = permits.forget_permits(surplus as _) as _; + let leaked = surplus.saturating_sub(reduced); + value = value.checked_add(leaked).ok_or(ErrorCode::Unknown)?; + } else { + permits.add_permits(value.saturating_sub(self.send_buffer_size) as _); + } + } + self.send_buffer_size = value; + Ok(()) + } + + pub(crate) fn socket_addr_check(&self) -> Option<&SocketAddrCheck> { + self.socket_addr_check.as_ref() + } + + pub(crate) fn set_socket_addr_check(&mut self, check: Option) { + self.socket_addr_check = check; + } + + pub fn drop(self, loopback: &mut Network) -> wasmtime::Result<()> { + let local_address = match self.state { + UdpState::BindStarted { local_address, .. } + | UdpState::Bound { local_address, .. } + | UdpState::Connected { local_address, .. } => local_address, + UdpState::Closed => return Ok(()), + }; + let net = loopback.get_udp_net_mut(local_address.ip()); + let port = + NonZeroU16::new(local_address.port()).context("local address port cannot be 0")?; + net.remove(&port); + Ok(()) + } +} diff --git a/crates/wasi/src/sockets/mod.rs b/crates/wasi/src/sockets/mod.rs new file mode 100644 index 00000000..59bd72f7 --- /dev/null +++ b/crates/wasi/src/sockets/mod.rs @@ -0,0 +1,202 @@ +use core::future::Future; +use core::ops::Deref; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use wasmtime::component::{HasData, ResourceTable}; + +pub mod loopback; +mod tcp; +mod udp; +pub(crate) mod util; + +#[cfg(feature = "p3")] +pub(crate) use tcp::NonInheritedOptions; +pub use tcp::TcpSocket; +pub use udp::UdpSocket; + +/// A helper struct which implements [`HasData`] for the `wasi:sockets` APIs. +/// +/// This can be useful when directly calling `add_to_linker` functions directly, +/// such as [`wash_wasi::p2::bindings::sockets::tcp::add_to_linker`] as the +/// `D` type parameter. See [`HasData`] for more information about the type +/// parameter's purpose. +/// +/// When using this type you can skip the [`WasiSocketsView`] trait, for +/// example. +/// +/// # Examples +/// +/// ``` +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime::{Engine, Result, Config}; +/// use wash_wasi::sockets::*; +/// +/// struct MyStoreState { +/// table: ResourceTable, +/// sockets: WasiSocketsCtx, +/// } +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// let mut linker = Linker::new(&engine); +/// +/// wash_wasi::p2::bindings::sockets::tcp::add_to_linker::( +/// &mut linker, +/// |state| WasiSocketsCtxView { +/// ctx: &mut state.sockets, +/// table: &mut state.table, +/// }, +/// )?; +/// Ok(()) +/// } +/// ``` +pub struct WasiSockets; + +impl HasData for WasiSockets { + type Data<'a> = WasiSocketsCtxView<'a>; +} + +/// Value taken from rust std library. +pub(crate) const DEFAULT_TCP_BACKLOG: u32 = 128; + +/// Theoretical maximum byte size of a UDP datagram, the real limit is lower, +/// but we do not account for e.g. the transport layer here for simplicity. +/// In practice, datagrams are typically less than 1500 bytes. +pub(crate) const MAX_UDP_DATAGRAM_SIZE: usize = u16::MAX as usize; + +#[derive(Default)] +pub struct WasiSocketsCtx { + pub(crate) socket_addr_check: SocketAddrCheck, + pub(crate) allowed_network_uses: AllowedNetworkUses, + pub(crate) loopback: Arc>, +} + +pub struct WasiSocketsCtxView<'a> { + pub ctx: &'a mut WasiSocketsCtx, + pub table: &'a mut ResourceTable, +} + +pub trait WasiSocketsView: Send { + fn sockets(&mut self) -> WasiSocketsCtxView<'_>; +} + +#[derive(Copy, Clone)] +pub(crate) struct AllowedNetworkUses { + pub(crate) ip_name_lookup: bool, + pub(crate) udp: bool, + pub(crate) tcp: bool, +} + +impl Default for AllowedNetworkUses { + fn default() -> Self { + Self { + ip_name_lookup: false, + udp: true, + tcp: true, + } + } +} + +impl AllowedNetworkUses { + pub(crate) fn check_allowed_udp(&self) -> std::io::Result<()> { + if !self.udp { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "UDP is not allowed", + )); + } + + Ok(()) + } + + pub(crate) fn check_allowed_tcp(&self) -> std::io::Result<()> { + if !self.tcp { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "TCP is not allowed", + )); + } + + Ok(()) + } +} + +/// A check that will be called for each socket address that is used of whether the address is permitted. +#[derive(Clone)] +pub(crate) struct SocketAddrCheck( + Arc< + dyn Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync, + >, +); + +impl SocketAddrCheck { + /// A check that will be called for each socket address that is used. + /// + /// Returning `true` will permit socket connections to the `SocketAddr`, + /// while returning `false` will reject the connection. + pub(crate) fn new( + f: impl Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync + + 'static, + ) -> Self { + Self(Arc::new(f)) + } + + pub(crate) async fn check( + &self, + addr: SocketAddr, + reason: SocketAddrUse, + ) -> std::io::Result<()> { + if (self.0)(addr, reason).await { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "An address was not permitted by the socket address check.", + )) + } + } +} + +impl Deref for SocketAddrCheck { + type Target = dyn Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl Default for SocketAddrCheck { + fn default() -> Self { + Self(Arc::new(|_, _| Box::pin(async { false }))) + } +} + +/// The reason what a socket address is being used for. +#[derive(Clone, Copy, Debug)] +pub enum SocketAddrUse { + /// Binding TCP socket + TcpBind, + /// Connecting TCP socket + TcpConnect, + /// Binding UDP socket + UdpBind, + /// Connecting UDP socket + UdpConnect, + /// Sending datagram on non-connected UDP socket + UdpOutgoingDatagram, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) enum SocketAddressFamily { + Ipv4, + Ipv6, +} diff --git a/crates/wasi/src/sockets/tcp.rs b/crates/wasi/src/sockets/tcp.rs new file mode 100644 index 00000000..a632ebe3 --- /dev/null +++ b/crates/wasi/src/sockets/tcp.rs @@ -0,0 +1,1379 @@ +use crate::p2::P2TcpStreamingState; +use crate::runtime::with_ambient_tokio_runtime; +use crate::sockets::util::{ + ErrorCode, get_unicast_hop_limit, is_valid_address_family, is_valid_remote_address, + is_valid_unicast_address, receive_buffer_size, send_buffer_size, set_keep_alive_count, + set_keep_alive_idle_time, set_keep_alive_interval, set_receive_buffer_size, + set_send_buffer_size, set_unicast_hop_limit, tcp_bind, +}; +use crate::sockets::{DEFAULT_TCP_BACKLOG, SocketAddressFamily, WasiSocketsCtx}; +use io_lifetimes::AsSocketlike as _; +use io_lifetimes::views::SocketlikeView; +use rustix::io::Errno; +use rustix::net::sockopt; +use std::fmt::Debug; +use std::io; +use std::mem; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +/// The state of a TCP socket. +/// +/// This represents the various states a socket can be in during the +/// activities of binding, listening, accepting, and connecting. Note that this +/// state machine encompasses both WASIp2 and WASIp3. +enum TcpState { + /// The initial state for a newly-created socket. + /// + /// From here a socket can transition to `BindStarted`, `ListenStarted`, or + /// `Connecting`. + Default(tokio::net::TcpSocket), + + /// A state indicating that a bind has been started and must be finished + /// subsequently with `finish_bind`. + /// + /// From here a socket can transition to `Bound`. + BindStarted(tokio::net::TcpSocket), + + /// Binding finished. The socket has an address but is not yet listening for + /// connections. + /// + /// From here a socket can transition to `ListenStarted`, or `Connecting`. + Bound(tokio::net::TcpSocket), + + /// Listening on a socket has started and must be completed with + /// `finish_listen`. + /// + /// From here a socket can transition to `Listening`. + ListenStarted(tokio::net::TcpSocket), + + /// The socket is now listening and waiting for an incoming connection. + /// + /// Sockets will not leave this state. + Listening { + /// The raw tokio-basd TCP listener managing the underyling socket. + listener: Arc, + + /// The last-accepted connection, set during the `ready` method and read + /// during the `accept` method. Note that this is only used for WASIp2 + /// at this time. + pending_accept: Option>, + }, + + /// An outgoing connection is started. + /// + /// This is created via the `start_connect` method. The payload here is an + /// optionally-specified owned future for the result of the connect. In + /// WASIp2 the future lives here, but in WASIp3 it lives on the event loop + /// so this is `None`. + /// + /// From here a socket can transition to `ConnectReady` or `Connected`. + Connecting(Option> + Send>>>), + + /// A connection via `Connecting` has completed. + /// + /// This is present for WASIp2 where the `Connecting` state stores `Some` of + /// a future, and the result of that future is recorded here when it + /// finishes as part of the `ready` method. + /// + /// From here a socket can transition to `Connected`. + ConnectReady(io::Result), + + /// A connection has been established. + /// + /// This is created either via `finish_connect` or for freshly accepted + /// sockets from a TCP listener. + /// + /// From here a socket can transition to `Receiving` or `P2Streaming`. + Connected(Arc), + + /// A connection has been established and `receive` has been called. + /// + /// A socket will not transition out of this state. + #[cfg(feature = "p3")] + Receiving(Arc), + + /// This is a WASIp2-bound socket which stores some extra state for + /// read/write streams to handle TCP shutdown. + /// + /// A socket will not transition out of this state. + P2Streaming(Box), + + /// This is not actually a socket but a deferred error. + /// + /// This error came out of `accept` and is deferred until the socket is + /// operated on. + #[cfg(feature = "p3")] + Error(io::Error), + + /// The socket is closed and no more operations can be performed. + Closed, +} + +impl Debug for TcpState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Default(_) => f.debug_tuple("Default").finish(), + Self::BindStarted(_) => f.debug_tuple("BindStarted").finish(), + Self::Bound(_) => f.debug_tuple("Bound").finish(), + Self::ListenStarted { .. } => f.debug_tuple("ListenStarted").finish(), + Self::Listening { .. } => f.debug_tuple("Listening").finish(), + Self::Connecting(..) => f.debug_tuple("Connecting").finish(), + Self::ConnectReady(..) => f.debug_tuple("ConnectReady").finish(), + Self::Connected { .. } => f.debug_tuple("Connected").finish(), + #[cfg(feature = "p3")] + Self::Receiving { .. } => f.debug_tuple("Receiving").finish(), + Self::P2Streaming(_) => f.debug_tuple("P2Streaming").finish(), + #[cfg(feature = "p3")] + Self::Error(..) => f.debug_tuple("Error").finish(), + Self::Closed => write!(f, "Closed"), + } + } +} + +/// A host TCP socket, plus associated bookkeeping. +pub struct NetworkTcpSocket { + /// The current state in the bind/listen/accept/connect progression. + tcp_state: TcpState, + + /// The desired listen queue size. + listen_backlog_size: u32, + + family: SocketAddressFamily, + + options: NonInheritedOptions, +} + +impl NetworkTcpSocket { + /// Create a new socket in the given family. + fn new(ctx: &WasiSocketsCtx, family: SocketAddressFamily) -> Result { + ctx.allowed_network_uses.check_allowed_tcp()?; + + with_ambient_tokio_runtime(|| { + let socket = match family { + SocketAddressFamily::Ipv4 => tokio::net::TcpSocket::new_v4()?, + SocketAddressFamily::Ipv6 => { + let socket = tokio::net::TcpSocket::new_v6()?; + sockopt::set_ipv6_v6only(&socket, true)?; + socket + } + }; + + Ok(Self::from_state(TcpState::Default(socket), family)) + }) + } + + #[cfg(feature = "p3")] + fn new_error(err: io::Error, family: SocketAddressFamily) -> Self { + NetworkTcpSocket::from_state(TcpState::Error(err), family) + } + + /// Creates a new socket with the `result` of an accepted socket from a + /// `TcpListener`. + /// + /// This will handle the `result` internally and `result` should be the raw + /// result from a TCP listen operation. + fn new_accept( + result: io::Result, + options: &NonInheritedOptions, + family: SocketAddressFamily, + ) -> io::Result { + let client = result.map_err(|err| match Errno::from_io_error(&err) { + // From: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept#:~:text=WSAEINPROGRESS + // > WSAEINPROGRESS: A blocking Windows Sockets 1.1 call is in progress, + // > or the service provider is still processing a callback function. + // + // wasi-sockets doesn't have an equivalent to the EINPROGRESS error, + // because in POSIX this error is only returned by a non-blocking + // `connect` and wasi-sockets has a different solution for that. + #[cfg(windows)] + Some(Errno::INPROGRESS) => Errno::INTR.into(), + + // Normalize Linux' non-standard behavior. + // + // From https://man7.org/linux/man-pages/man2/accept.2.html: + // > Linux accept() passes already-pending network errors on the + // > new socket as an error code from accept(). This behavior + // > differs from other BSD socket implementations. (...) + #[cfg(target_os = "linux")] + Some( + Errno::CONNRESET + | Errno::NETRESET + | Errno::HOSTUNREACH + | Errno::HOSTDOWN + | Errno::NETDOWN + | Errno::NETUNREACH + | Errno::PROTO + | Errno::NOPROTOOPT + | Errno::NONET + | Errno::OPNOTSUPP, + ) => Errno::CONNABORTED.into(), + + _ => err, + })?; + options.apply(family, &client); + Ok(Self::from_state( + TcpState::Connected(Arc::new(client)), + family, + )) + } + + /// Create a `TcpSocket` from an existing socket. + fn from_state(state: TcpState, family: SocketAddressFamily) -> Self { + Self { + tcp_state: state, + listen_backlog_size: DEFAULT_TCP_BACKLOG, + family, + options: Default::default(), + } + } + + fn as_std_view(&self) -> Result, ErrorCode> { + match &self.tcp_state { + TcpState::Default(socket) + | TcpState::BindStarted(socket) + | TcpState::Bound(socket) + | TcpState::ListenStarted(socket) => Ok(socket.as_socketlike_view()), + TcpState::Connected(stream) => Ok(stream.as_socketlike_view()), + #[cfg(feature = "p3")] + TcpState::Receiving(stream) => Ok(stream.as_socketlike_view()), + TcpState::Listening { listener, .. } => Ok(listener.as_socketlike_view()), + TcpState::P2Streaming(state) => Ok(state.stream.as_socketlike_view()), + TcpState::Connecting(..) | TcpState::ConnectReady(_) | TcpState::Closed => { + Err(ErrorCode::InvalidState) + } + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + } + } + + pub(crate) fn start_bind(&mut self, addr: SocketAddr) -> Result<(), ErrorCode> { + match mem::replace(&mut self.tcp_state, TcpState::Closed) { + TcpState::Default(sock) => { + if let Err(err) = tcp_bind(&sock, addr) { + self.tcp_state = TcpState::Default(sock); + Err(err) + } else { + self.tcp_state = TcpState::BindStarted(sock); + Ok(()) + } + } + tcp_state => { + self.tcp_state = tcp_state; + Err(ErrorCode::InvalidState) + } + } + } + + pub(crate) fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match mem::replace(&mut self.tcp_state, TcpState::Closed) { + TcpState::BindStarted(socket) => { + self.tcp_state = TcpState::Bound(socket); + Ok(()) + } + current_state => { + // Reset the state so that the outside world doesn't see this socket as closed + self.tcp_state = current_state; + Err(ErrorCode::NotInProgress) + } + } + } + + fn start_connect(&mut self) -> Result { + let (TcpState::Default(tokio_socket) | TcpState::Bound(tokio_socket)) = + mem::replace(&mut self.tcp_state, TcpState::Connecting(None)) + else { + unreachable!(); + }; + + Ok(tokio_socket) + } + + /// For WASIp2 this is used to record the actual connection future as part + /// of `start_connect` within this socket state. + fn set_pending_connect( + &mut self, + future: impl Future> + Send + 'static, + ) -> Result<(), ErrorCode> { + match &mut self.tcp_state { + TcpState::Connecting(slot @ None) => { + *slot = Some(Box::pin(future)); + Ok(()) + } + _ => Err(ErrorCode::InvalidState), + } + } + + /// For WASIp2 this retrieves the result from the future passed to + /// `set_pending_connect`. + /// + /// Return states here are: + /// + /// * `Ok(Some(res))` - where `res` is the result of the connect operation. + /// * `Ok(None)` - the connect operation isn't ready yet. + /// * `Err(e)` - a connect operation is not in progress. + fn take_pending_connect( + &mut self, + ) -> Result>, ErrorCode> { + match mem::replace(&mut self.tcp_state, TcpState::Connecting(None)) { + TcpState::ConnectReady(result) => Ok(Some(result)), + TcpState::Connecting(Some(mut future)) => { + let mut cx = Context::from_waker(Waker::noop()); + match with_ambient_tokio_runtime(|| future.as_mut().poll(&mut cx)) { + Poll::Ready(result) => Ok(Some(result)), + Poll::Pending => { + self.tcp_state = TcpState::Connecting(Some(future)); + Ok(None) + } + } + } + current_state => { + self.tcp_state = current_state; + Err(ErrorCode::NotInProgress) + } + } + } + + fn finish_connect(&mut self, result: io::Result) -> Result<(), ErrorCode> { + if !matches!(self.tcp_state, TcpState::Connecting(None)) { + return Err(ErrorCode::InvalidState); + } + match result { + Ok(ConnectingTcpStream::Network(stream)) => { + self.tcp_state = TcpState::Connected(Arc::new(stream)); + Ok(()) + } + Ok(ConnectingTcpStream::Loopback(..)) => Err(ErrorCode::InvalidState), + Err(err) => { + self.tcp_state = TcpState::Closed; + Err(ErrorCode::from(err)) + } + } + } + + pub(crate) fn start_listen(&mut self) -> Result<(), ErrorCode> { + match mem::replace(&mut self.tcp_state, TcpState::Closed) { + TcpState::Bound(tokio_socket) => { + self.tcp_state = TcpState::ListenStarted(tokio_socket); + Ok(()) + } + previous_state => { + self.tcp_state = previous_state; + Err(ErrorCode::InvalidState) + } + } + } + + pub(crate) fn finish_listen(&mut self) -> Result<(), ErrorCode> { + let tokio_socket = match mem::replace(&mut self.tcp_state, TcpState::Closed) { + TcpState::ListenStarted(tokio_socket) => tokio_socket, + previous_state => { + self.tcp_state = previous_state; + return Err(ErrorCode::NotInProgress); + } + }; + + match with_ambient_tokio_runtime(|| tokio_socket.listen(self.listen_backlog_size)) { + Ok(listener) => { + self.tcp_state = TcpState::Listening { + listener: Arc::new(listener), + pending_accept: None, + }; + Ok(()) + } + Err(err) => { + self.tcp_state = TcpState::Closed; + + Err(match Errno::from_io_error(&err) { + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-listen#:~:text=WSAEMFILE + // According to the docs, `listen` can return EMFILE on Windows. + // This is odd, because we're not trying to create a new socket + // or file descriptor of any kind. So we rewrite it to less + // surprising error code. + // + // At the time of writing, this behavior has never been experimentally + // observed by any of the wasmtime authors, so we're relying fully + // on Microsoft's documentation here. + #[cfg(windows)] + Some(Errno::MFILE) => Errno::NOBUFS.into(), + + _ => err.into(), + }) + } + } + } + + fn accept(&mut self) -> Result, ErrorCode> { + let TcpState::Listening { + listener, + pending_accept, + } = &mut self.tcp_state + else { + return Err(ErrorCode::InvalidState); + }; + + let result = match pending_accept.take() { + Some(result) => result, + None => { + let mut cx = std::task::Context::from_waker(Waker::noop()); + match with_ambient_tokio_runtime(|| listener.poll_accept(&mut cx)) + .map_ok(|(stream, _)| stream) + { + Poll::Ready(result) => result, + Poll::Pending => return Ok(None), + } + } + }; + + Ok(Some(Self::new_accept(result, &self.options, self.family)?)) + } + + #[cfg(feature = "p3")] + pub(crate) fn start_receive(&mut self) -> Option<&Arc> { + match mem::replace(&mut self.tcp_state, TcpState::Closed) { + TcpState::Connected(stream) => { + self.tcp_state = TcpState::Receiving(stream); + Some(self.tcp_stream_arc().unwrap()) + } + prev => { + self.tcp_state = prev; + None + } + } + } + + fn local_address(&self) -> Result { + match &self.tcp_state { + TcpState::Bound(socket) => Ok(socket.local_addr()?), + TcpState::Connected(stream) => Ok(stream.local_addr()?), + #[cfg(feature = "p3")] + TcpState::Receiving(stream) => Ok(stream.local_addr()?), + TcpState::P2Streaming(state) => Ok(state.stream.local_addr()?), + TcpState::Listening { listener, .. } => Ok(listener.local_addr()?), + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + fn remote_address(&self) -> Result { + let stream = self.tcp_stream_arc()?; + let addr = stream.peer_addr()?; + Ok(addr) + } + + fn is_listening(&self) -> bool { + matches!(self.tcp_state, TcpState::Listening { .. }) + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + self.family + } + + fn set_listen_backlog_size(&mut self, value: u64) -> Result<(), ErrorCode> { + const MIN_BACKLOG: u32 = 1; + const MAX_BACKLOG: u32 = i32::MAX as u32; // OS'es will most likely limit it down even further. + + if value == 0 { + return Err(ErrorCode::InvalidArgument); + } + // Silently clamp backlog size. This is OK for us to do, because operating systems do this too. + let value = value + .try_into() + .unwrap_or(MAX_BACKLOG) + .clamp(MIN_BACKLOG, MAX_BACKLOG); + match &self.tcp_state { + TcpState::Default(..) | TcpState::Bound(..) => { + // Socket not listening yet. Stash value for first invocation to `listen`. + self.listen_backlog_size = value; + Ok(()) + } + TcpState::Listening { listener, .. } => { + // Try to update the backlog by calling `listen` again. + // Not all platforms support this. We'll only update our own value if the OS supports changing the backlog size after the fact. + if rustix::net::listen(&listener, value.try_into().unwrap_or(i32::MAX)).is_err() { + return Err(ErrorCode::NotSupported); + } + self.listen_backlog_size = value; + Ok(()) + } + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + fn keep_alive_enabled(&self) -> Result { + let fd = &*self.as_std_view()?; + let v = sockopt::socket_keepalive(fd)?; + Ok(v) + } + + fn set_keep_alive_enabled(&self, value: bool) -> Result<(), ErrorCode> { + let fd = &*self.as_std_view()?; + sockopt::set_socket_keepalive(fd, value)?; + Ok(()) + } + + fn keep_alive_idle_time(&self) -> Result { + let fd = &*self.as_std_view()?; + let v = sockopt::tcp_keepidle(fd)?; + Ok(v.as_nanos().try_into().unwrap_or(u64::MAX)) + } + + fn set_keep_alive_idle_time(&mut self, value: u64) -> Result<(), ErrorCode> { + let value = { + let fd = self.as_std_view()?; + set_keep_alive_idle_time(&*fd, value)? + }; + self.options.set_keep_alive_idle_time(value); + Ok(()) + } + + fn keep_alive_interval(&self) -> Result { + let fd = &*self.as_std_view()?; + let v = sockopt::tcp_keepintvl(fd)?; + Ok(v.as_nanos().try_into().unwrap_or(u64::MAX)) + } + + fn set_keep_alive_interval(&self, value: u64) -> Result<(), ErrorCode> { + let fd = &*self.as_std_view()?; + set_keep_alive_interval(fd, Duration::from_nanos(value))?; + Ok(()) + } + + fn keep_alive_count(&self) -> Result { + let fd = &*self.as_std_view()?; + let v = sockopt::tcp_keepcnt(fd)?; + Ok(v) + } + + fn set_keep_alive_count(&self, value: u32) -> Result<(), ErrorCode> { + let fd = &*self.as_std_view()?; + set_keep_alive_count(fd, value)?; + Ok(()) + } + + fn hop_limit(&self) -> Result { + let fd = &*self.as_std_view()?; + let n = get_unicast_hop_limit(fd, self.family)?; + Ok(n) + } + + fn set_hop_limit(&mut self, value: u8) -> Result<(), ErrorCode> { + { + let fd = &*self.as_std_view()?; + set_unicast_hop_limit(fd, self.family, value)?; + } + self.options.set_hop_limit(value); + Ok(()) + } + + fn receive_buffer_size(&self) -> Result { + let fd = &*self.as_std_view()?; + let n = receive_buffer_size(fd)?; + Ok(n) + } + + fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + let res = { + let fd = &*self.as_std_view()?; + set_receive_buffer_size(fd, value)? + }; + self.options.set_receive_buffer_size(res); + Ok(()) + } + + fn send_buffer_size(&self) -> Result { + let fd = &*self.as_std_view()?; + let n = send_buffer_size(fd)?; + Ok(n) + } + + fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + let res = { + let fd = &*self.as_std_view()?; + set_send_buffer_size(fd, value)? + }; + self.options.set_send_buffer_size(res); + Ok(()) + } + + #[cfg(feature = "p3")] + pub(crate) fn non_inherited_options(&self) -> &NonInheritedOptions { + &self.options + } + + #[cfg(feature = "p3")] + pub(crate) fn tcp_listener_arc(&self) -> Result<&Arc, ErrorCode> { + match &self.tcp_state { + TcpState::Listening { listener, .. } => Ok(listener), + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn tcp_stream_arc(&self) -> Result<&Arc, ErrorCode> { + match &self.tcp_state { + TcpState::Connected(socket) => Ok(socket), + #[cfg(feature = "p3")] + TcpState::Receiving(socket) => Ok(socket), + TcpState::P2Streaming(state) => Ok(&state.stream), + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn p2_streaming_state(&self) -> Result<&P2TcpStreamingState, ErrorCode> { + match &self.tcp_state { + TcpState::P2Streaming(state) => Ok(state), + #[cfg(feature = "p3")] + TcpState::Error(err) => Err(err.into()), + _ => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn set_p2_streaming_state( + &mut self, + state: P2TcpStreamingState, + ) -> Result<(), ErrorCode> { + if !matches!(self.tcp_state, TcpState::Connected(_)) { + return Err(ErrorCode::InvalidState); + } + self.tcp_state = TcpState::P2Streaming(Box::new(state)); + Ok(()) + } + + /// Used for `Pollable` in the WASIp2 implementation this awaits the socket + /// to be connected, if in the connecting state, or for a TCP accept to be + /// ready, if this is in the listening state. + /// + /// For all other states this method immediately returns. + async fn ready(&mut self) { + match &mut self.tcp_state { + TcpState::Default(..) + | TcpState::BindStarted(..) + | TcpState::Bound(..) + | TcpState::ListenStarted(..) + | TcpState::ConnectReady(..) + | TcpState::Closed + | TcpState::Connected { .. } + | TcpState::Connecting(None) + | TcpState::Listening { + pending_accept: Some(_), + .. + } + | TcpState::P2Streaming(_) => {} + + #[cfg(feature = "p3")] + TcpState::Receiving(_) | TcpState::Error(_) => {} + + TcpState::Connecting(Some(future)) => { + self.tcp_state = TcpState::ConnectReady(future.as_mut().await); + } + + TcpState::Listening { + listener, + pending_accept: slot @ None, + } => { + let result = futures::future::poll_fn(|cx| { + listener.poll_accept(cx).map_ok(|(stream, _)| stream) + }) + .await; + *slot = Some(result); + } + } + } +} + +#[cfg(not(target_os = "macos"))] +pub use inherits_option::*; +#[cfg(not(target_os = "macos"))] +mod inherits_option { + use crate::sockets::SocketAddressFamily; + use tokio::net::TcpStream; + + #[derive(Default, Clone)] + pub struct NonInheritedOptions; + + impl NonInheritedOptions { + pub fn set_keep_alive_idle_time(&mut self, _value: u64) {} + + pub fn set_hop_limit(&mut self, _value: u8) {} + + pub fn set_receive_buffer_size(&mut self, _value: usize) {} + + pub fn set_send_buffer_size(&mut self, _value: usize) {} + + pub(crate) fn apply(&self, _family: SocketAddressFamily, _stream: &TcpStream) {} + } +} + +#[cfg(target_os = "macos")] +pub use does_not_inherit_options::*; +#[cfg(target_os = "macos")] +mod does_not_inherit_options { + use crate::sockets::SocketAddressFamily; + use rustix::net::sockopt; + use std::sync::Arc; + use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering::Relaxed}; + use std::time::Duration; + use tokio::net::TcpStream; + + // The socket options below are not automatically inherited from the listener + // on all platforms. So we keep track of which options have been explicitly + // set and manually apply those values to newly accepted clients. + #[derive(Default, Clone)] + pub struct NonInheritedOptions(Arc); + + #[derive(Default)] + struct Inner { + receive_buffer_size: AtomicUsize, + send_buffer_size: AtomicUsize, + hop_limit: AtomicU8, + keep_alive_idle_time: AtomicU64, // nanoseconds + } + + impl NonInheritedOptions { + pub fn set_keep_alive_idle_time(&mut self, value: u64) { + self.0.keep_alive_idle_time.store(value, Relaxed); + } + + pub fn set_hop_limit(&mut self, value: u8) { + self.0.hop_limit.store(value, Relaxed); + } + + pub fn set_receive_buffer_size(&mut self, value: usize) { + self.0.receive_buffer_size.store(value, Relaxed); + } + + pub fn set_send_buffer_size(&mut self, value: usize) { + self.0.send_buffer_size.store(value, Relaxed); + } + + pub(crate) fn apply(&self, family: SocketAddressFamily, stream: &TcpStream) { + // Manually inherit socket options from listener. We only have to + // do this on platforms that don't already do this automatically + // and only if a specific value was explicitly set on the listener. + + let receive_buffer_size = self.0.receive_buffer_size.load(Relaxed); + if receive_buffer_size > 0 { + // Ignore potential error. + _ = sockopt::set_socket_recv_buffer_size(&stream, receive_buffer_size); + } + + let send_buffer_size = self.0.send_buffer_size.load(Relaxed); + if send_buffer_size > 0 { + // Ignore potential error. + _ = sockopt::set_socket_send_buffer_size(&stream, send_buffer_size); + } + + // For some reason, IP_TTL is inherited, but IPV6_UNICAST_HOPS isn't. + if family == SocketAddressFamily::Ipv6 { + let hop_limit = self.0.hop_limit.load(Relaxed); + if hop_limit > 0 { + // Ignore potential error. + _ = sockopt::set_ipv6_unicast_hops(&stream, Some(hop_limit)); + } + } + + let keep_alive_idle_time = self.0.keep_alive_idle_time.load(Relaxed); + if keep_alive_idle_time > 0 { + // Ignore potential error. + _ = sockopt::set_tcp_keepidle(&stream, Duration::from_nanos(keep_alive_idle_time)); + } + } + } +} + +impl super::loopback::TcpSocket { + pub fn new( + socket: &NetworkTcpSocket, + state: super::loopback::TcpState, + ) -> Result { + let fd = &*socket.as_std_view()?; + + let keep_alive_enabled = sockopt::socket_keepalive(fd)?; + + let keep_alive_idle_time = sockopt::tcp_keepidle(fd)?; + let keep_alive_idle_time = keep_alive_idle_time + .as_nanos() + .try_into() + .unwrap_or(u64::MAX); + + let keep_alive_interval = sockopt::tcp_keepintvl(fd)?; + let keep_alive_interval = keep_alive_interval + .as_nanos() + .try_into() + .unwrap_or(u64::MAX); + + let keep_alive_count = sockopt::tcp_keepcnt(fd)?; + + let hop_limit = get_unicast_hop_limit(fd, socket.family)?; + + let receive_buffer_size = receive_buffer_size(fd)?; + + let send_buffer_size = send_buffer_size(fd)?; + let send_buffer_size = send_buffer_size + .try_into() + .unwrap_or(Self::MAX_SEND_BUFFER_SIZE) + .min(Self::MAX_SEND_BUFFER_SIZE); + + let listen_backlog_size = socket + .listen_backlog_size + .min(Self::MAX_LISTEN_BACKLOG_SIZE); + Ok(Self { + state, + send_buffer_size, + receive_buffer_size, + listen_backlog_size, + keep_alive_enabled, + keep_alive_idle_time, + keep_alive_interval, + keep_alive_count, + hop_limit, + family: socket.family, + }) + } +} + +pub enum TcpSocket { + Network(NetworkTcpSocket), + Loopback(super::loopback::TcpSocket), + // A socket bound to unspecified IP, which was not connected yet + Unspecified { + net: NetworkTcpSocket, + lo: super::loopback::TcpSocket, + }, +} + +pub enum ConnectingTcpSocket { + Network(tokio::net::TcpSocket), + Loopback(tokio::sync::mpsc::Sender), +} + +pub enum ConnectingTcpStream { + Network(tokio::net::TcpStream), + Loopback(tokio::sync::mpsc::OwnedPermit), +} + +impl ConnectingTcpSocket { + pub fn connect( + self, + addr: SocketAddr, + ) -> impl Future> { + async move { + match self { + Self::Network(socket) => { + socket.connect(addr).await.map(ConnectingTcpStream::Network) + } + Self::Loopback(tx) => match tx.reserve_owned().await { + Ok(tx) => Ok(ConnectingTcpStream::Loopback(tx)), + Err(..) => Err(std::io::ErrorKind::ConnectionRefused.into()), + }, + } + } + } +} + +impl TcpSocket { + pub(crate) fn new( + ctx: &WasiSocketsCtx, + family: SocketAddressFamily, + ) -> Result { + NetworkTcpSocket::new(ctx, family).map(Self::Network) + } + + #[cfg(feature = "p3")] + pub(crate) fn new_error(err: io::Error, family: SocketAddressFamily) -> Self { + Self::Network(NetworkTcpSocket::new_error(err, family)) + } + + pub(crate) fn new_accept( + result: io::Result, + options: &NonInheritedOptions, + family: SocketAddressFamily, + ) -> io::Result { + NetworkTcpSocket::new_accept(result, options, family).map(Self::Network) + } + + pub(crate) fn start_bind( + &mut self, + mut addr: SocketAddr, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + use core::net::{Ipv4Addr, Ipv6Addr}; + + let Self::Network(socket) = self else { + return Err(ErrorCode::InvalidState); + }; + let ip = addr.ip(); + if !is_valid_unicast_address(ip) || !is_valid_address_family(ip, socket.family) { + return Err(ErrorCode::InvalidArgument); + } + let ip = ip.to_canonical(); + if !ip.is_loopback() { + socket.start_bind(addr)?; + if !ip.is_unspecified() { + return Ok(()); + } + let TcpState::BindStarted(sock) = &socket.tcp_state else { + unreachable!(); + }; + addr = sock.local_addr()?; + match &mut addr { + SocketAddr::V4(addr) => addr.set_ip(Ipv4Addr::LOCALHOST), + SocketAddr::V6(addr) => addr.set_ip(Ipv6Addr::LOCALHOST), + } + } + let addr = loopback.bind_tcp(addr)?; + let lo = + super::loopback::TcpSocket::new(socket, super::loopback::TcpState::BindStarted(addr))?; + if ip.is_unspecified() { + *self = Self::Unspecified { + net: NetworkTcpSocket { + tcp_state: mem::replace(&mut socket.tcp_state, TcpState::Closed), + listen_backlog_size: socket.listen_backlog_size, + family: socket.family, + options: socket.options.clone(), + }, + lo, + } + } else { + *self = Self::Loopback(lo); + } + Ok(()) + } + + pub(crate) fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.finish_bind(), + Self::Loopback(socket) => socket.finish_bind(), + Self::Unspecified { net, lo } => { + net.finish_bind()?; + lo.finish_bind() + } + } + } + + pub(crate) fn start_connect( + &mut self, + addr: &SocketAddr, + loopback: &mut super::loopback::Network, + ) -> Result { + if let Self::Network(socket) | Self::Unspecified { net: socket, .. } = self { + match socket.tcp_state { + TcpState::Default(..) | TcpState::Bound(..) => {} + TcpState::Connecting(..) => { + return Err(ErrorCode::ConcurrencyConflict); + } + _ => return Err(ErrorCode::InvalidState), + }; + + if !is_valid_unicast_address(addr.ip()) + || !is_valid_remote_address(*addr) + || !is_valid_address_family(addr.ip(), socket.family) + { + return Err(ErrorCode::InvalidArgument); + }; + } + if let Self::Loopback(socket) | Self::Unspecified { lo: socket, .. } = self { + match socket.state { + super::loopback::TcpState::Bound(..) => {} + super::loopback::TcpState::Connecting { .. } => { + return Err(ErrorCode::ConcurrencyConflict); + } + _ => return Err(ErrorCode::InvalidState), + }; + + if !is_valid_unicast_address(addr.ip()) + || !is_valid_remote_address(*addr) + || !is_valid_address_family(addr.ip(), socket.family) + { + return Err(ErrorCode::InvalidArgument); + }; + } + + let ip = addr.ip().to_canonical(); + match ( + mem::replace( + self, + Self::Loopback(super::loopback::TcpSocket { + state: super::loopback::TcpState::Closed, + listen_backlog_size: 0, + keep_alive_enabled: false, + keep_alive_idle_time: 0, + keep_alive_interval: 0, + keep_alive_count: 0, + hop_limit: 0, + receive_buffer_size: 0, + send_buffer_size: 0, + family: SocketAddressFamily::Ipv4, + }), + ), + ip.is_loopback(), + ) { + ( + Self::Network(mut socket) + | Self::Unspecified { + net: mut socket, .. + }, + false, + ) => { + let res = socket.start_connect().map(ConnectingTcpSocket::Network); + *self = Self::Network(socket); + res + } + (Self::Network(socket), true) => { + if let TcpState::Bound(..) = socket.tcp_state { + *self = Self::Network(socket); + // socket wasn't bound to loopback + return Err(ErrorCode::InvalidState); + } + + let mut local_address = *addr; + local_address.set_port(0); + let local_address = match loopback.bind_tcp(local_address) { + Ok(addr) => addr, + Err(err) => { + *self = Self::Network(socket); + return Err(err); + } + }; + + let tx = match loopback.connect_tcp(addr) { + Ok(tx) => tx, + Err(err) => { + *self = Self::Network(socket); + return Err(err); + } + }; + + match super::loopback::TcpSocket::new( + &socket, + super::loopback::TcpState::Connecting { + local_address, + remote_address: *addr, + future: None, + }, + ) { + Ok(socket) => { + *self = Self::Loopback(socket); + Ok(ConnectingTcpSocket::Loopback(tx.clone())) + } + Err(err) => { + *self = Self::Network(socket); + Err(err) + } + } + } + (Self::Loopback(mut socket), ..) => { + let tx = socket.start_connect(addr, loopback); + *self = Self::Loopback(socket); + tx.map(|tx| ConnectingTcpSocket::Loopback(tx.clone())) + } + (Self::Unspecified { mut lo, net }, true) => match lo.start_connect(addr, loopback) { + Ok(tx) => { + *self = Self::Loopback(lo); + Ok(ConnectingTcpSocket::Loopback(tx.clone())) + } + Err(err) => { + *self = Self::Unspecified { lo, net }; + Err(err) + } + }, + } + } + + pub(crate) fn set_pending_connect( + &mut self, + future: impl Future> + Send + 'static, + ) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_pending_connect(future), + Self::Loopback(socket) => socket.set_pending_connect(future), + Self::Unspecified { .. } => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn take_pending_connect( + &mut self, + ) -> Result>, ErrorCode> { + match self { + Self::Network(socket) => socket.take_pending_connect(), + Self::Loopback(socket) => socket.take_pending_connect(), + Self::Unspecified { .. } => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn finish_connect( + &mut self, + result: io::Result, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.finish_connect(result), + Self::Loopback(socket) => socket.finish_connect(result, loopback), + Self::Unspecified { .. } => Err(ErrorCode::InvalidState), + } + } + + pub(crate) fn start_listen( + &mut self, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.start_listen(), + Self::Loopback(socket) => socket.start_listen(loopback), + Self::Unspecified { net, lo } => { + net.start_listen()?; + lo.start_listen(loopback) + } + } + } + + pub(crate) fn finish_listen(&mut self) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.finish_listen(), + Self::Loopback(socket) => socket.finish_listen(), + Self::Unspecified { net, lo } => { + net.finish_listen()?; + lo.finish_listen() + } + } + } + + pub(crate) fn accept(&mut self) -> Result, ErrorCode> { + match self { + Self::Network(socket) => socket.accept().map(|sock| sock.map(Self::Network)), + Self::Loopback(socket) => socket.accept().map(|sock| sock.map(Self::Loopback)), + Self::Unspecified { net, lo } => { + if let Some(sock) = net.accept()? { + return Ok(Some(Self::Network(sock))); + } + lo.accept().map(|sock| sock.map(Self::Loopback)) + } + } + } + + pub(crate) fn local_address(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => socket.local_address(), + Self::Loopback(socket) => socket.local_address(), + } + } + + pub(crate) fn remote_address(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.remote_address() + } + Self::Loopback(socket) => socket.remote_address(), + } + } + + pub(crate) fn is_listening(&self) -> bool { + match self { + Self::Network(socket) => socket.is_listening(), + Self::Loopback(socket) => socket.is_listening(), + Self::Unspecified { net, lo } => net.is_listening() && lo.is_listening(), + } + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.address_family() + } + Self::Loopback(socket) => socket.address_family(), + } + } + + pub(crate) fn set_listen_backlog_size(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_listen_backlog_size(value), + Self::Loopback(socket) => socket.set_listen_backlog_size(value), + Self::Unspecified { net, lo } => { + net.set_listen_backlog_size(value)?; + lo.set_listen_backlog_size(value) + } + } + } + + pub(crate) fn keep_alive_enabled(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.keep_alive_enabled() + } + Self::Loopback(socket) => socket.keep_alive_enabled(), + } + } + + pub(crate) fn set_keep_alive_enabled(&mut self, value: bool) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_keep_alive_enabled(value), + Self::Loopback(socket) => socket.set_keep_alive_enabled(value), + Self::Unspecified { net, lo } => { + net.set_keep_alive_enabled(value)?; + lo.set_keep_alive_enabled(value) + } + } + } + + pub(crate) fn keep_alive_idle_time(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.keep_alive_idle_time() + } + Self::Loopback(socket) => socket.keep_alive_idle_time(), + } + } + + pub(crate) fn set_keep_alive_idle_time(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_keep_alive_idle_time(value), + Self::Loopback(socket) => socket.set_keep_alive_idle_time(value), + Self::Unspecified { net, lo } => { + net.set_keep_alive_idle_time(value)?; + lo.set_keep_alive_idle_time(value) + } + } + } + + pub(crate) fn keep_alive_interval(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.keep_alive_interval() + } + Self::Loopback(socket) => socket.keep_alive_interval(), + } + } + + pub(crate) fn set_keep_alive_interval(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_keep_alive_interval(value), + Self::Loopback(socket) => socket.set_keep_alive_interval(value), + Self::Unspecified { net, lo } => { + net.set_keep_alive_interval(value)?; + lo.set_keep_alive_interval(value) + } + } + } + + pub(crate) fn keep_alive_count(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.keep_alive_count() + } + Self::Loopback(socket) => socket.keep_alive_count(), + } + } + + pub(crate) fn set_keep_alive_count(&mut self, value: u32) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_keep_alive_count(value), + Self::Loopback(socket) => socket.set_keep_alive_count(value), + Self::Unspecified { net, lo } => { + net.set_keep_alive_count(value)?; + lo.set_keep_alive_count(value) + } + } + } + + pub(crate) fn hop_limit(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => socket.hop_limit(), + Self::Loopback(socket) => socket.hop_limit(), + } + } + + pub(crate) fn set_hop_limit(&mut self, value: u8) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_hop_limit(value), + Self::Loopback(socket) => socket.set_hop_limit(value), + Self::Unspecified { net, lo } => { + net.set_hop_limit(value)?; + lo.set_hop_limit(value) + } + } + } + + pub(crate) fn receive_buffer_size(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.receive_buffer_size() + } + Self::Loopback(socket) => socket.receive_buffer_size(), + } + } + + pub(crate) fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_receive_buffer_size(value), + Self::Loopback(socket) => socket.set_receive_buffer_size(value), + Self::Unspecified { net, lo } => { + net.set_receive_buffer_size(value)?; + lo.set_receive_buffer_size(value) + } + } + } + + pub(crate) fn send_buffer_size(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.send_buffer_size() + } + Self::Loopback(socket) => socket.send_buffer_size(), + } + } + + pub(crate) fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_send_buffer_size(value), + Self::Loopback(socket) => socket.set_send_buffer_size(value), + Self::Unspecified { net, lo } => { + net.set_send_buffer_size(value)?; + lo.set_send_buffer_size(value) + } + } + } + + pub(crate) async fn ready(&mut self) { + match self { + Self::Network(socket) => socket.ready().await, + Self::Loopback(socket) => socket.ready().await, + Self::Unspecified { net, lo } => { + use core::future::poll_fn; + use core::pin::pin; + use core::task::Poll; + + let mut net = pin!(net.ready()); + let mut lo = pin!(lo.ready()); + poll_fn(|cx| match net.as_mut().poll(cx) { + Poll::Ready(()) => Poll::Ready(()), + Poll::Pending => lo.as_mut().poll(cx), + }) + .await; + } + } + } + + pub(crate) fn drop(self, loopback: &mut super::loopback::Network) -> wasmtime::Result<()> { + match self { + Self::Network(socket) => { + drop(socket); + Ok(()) + } + Self::Loopback(socket) => socket.drop(loopback), + Self::Unspecified { net, lo } => { + drop(net); + lo.drop(loopback) + } + } + } +} diff --git a/crates/wasi/src/sockets/udp.rs b/crates/wasi/src/sockets/udp.rs new file mode 100644 index 00000000..db0fcdd2 --- /dev/null +++ b/crates/wasi/src/sockets/udp.rs @@ -0,0 +1,621 @@ +use crate::runtime::with_ambient_tokio_runtime; +use crate::sockets::util::{ + ErrorCode, get_unicast_hop_limit, is_valid_address_family, is_valid_remote_address, + receive_buffer_size, send_buffer_size, set_receive_buffer_size, set_send_buffer_size, + set_unicast_hop_limit, udp_bind, udp_disconnect, udp_socket, +}; +use crate::sockets::{SocketAddrCheck, SocketAddressFamily, WasiSocketsCtx}; +use cap_net_ext::AddressFamily; +use io_lifetimes::AsSocketlike as _; +use io_lifetimes::raw::{FromRawSocketlike as _, IntoRawSocketlike as _}; +use rustix::io::Errno; +use rustix::net::connect; +use std::net::SocketAddr; +use std::sync::Arc; +use tracing::debug; + +/// The state of a UDP socket. +/// +/// This represents the various states a socket can be in during the +/// activities of binding, and connecting. +#[derive(Clone)] +enum UdpState { + /// The initial state for a newly-created socket. + Default, + + /// A `bind` operation has started but has yet to complete with + /// `finish_bind`. + BindStarted, + + /// Binding finished via `finish_bind`. The socket has an address but + /// is not yet listening for connections. + Bound, + + /// The socket is "connected" to a peer address. + #[cfg_attr( + not(feature = "p3"), + expect(dead_code, reason = "p2 has its own way of managing sending/receiving") + )] + Connected(SocketAddr), +} + +/// A host UDP socket, plus associated bookkeeping. +/// +/// The inner state is wrapped in an Arc because the same underlying socket is +/// used for implementing the stream types. +#[derive(Clone)] +pub struct NetworkUdpSocket { + socket: Arc, + + /// The current state in the bind/connect progression. + udp_state: UdpState, + + /// Socket address family. + family: SocketAddressFamily, + + /// If set, use this custom check for addrs, otherwise use what's in + /// `WasiSocketsCtx`. + socket_addr_check: Option, +} + +impl NetworkUdpSocket { + /// Create a new socket in the given family. + fn new(cx: &WasiSocketsCtx, family: AddressFamily) -> Result { + cx.allowed_network_uses.check_allowed_udp()?; + + // Delegate socket creation to cap_net_ext. They handle a couple of things for us: + // - On Windows: call WSAStartup if not done before. + // - Set the NONBLOCK and CLOEXEC flags. Either immediately during socket creation, + // or afterwards using ioctl or fcntl. Exact method depends on the platform. + + let fd = udp_socket(family)?; + + let socket_address_family = match family { + AddressFamily::Ipv4 => SocketAddressFamily::Ipv4, + AddressFamily::Ipv6 => { + rustix::net::sockopt::set_ipv6_v6only(&fd, true)?; + SocketAddressFamily::Ipv6 + } + }; + + let socket = with_ambient_tokio_runtime(|| { + tokio::net::UdpSocket::try_from(unsafe { + std::net::UdpSocket::from_raw_socketlike(fd.into_raw_socketlike()) + }) + })?; + + Ok(Self { + socket: Arc::new(socket), + udp_state: UdpState::Default, + family: socket_address_family, + socket_addr_check: None, + }) + } + + fn bind(&mut self, addr: SocketAddr) -> Result<(), ErrorCode> { + udp_bind(&self.socket, addr)?; + self.udp_state = UdpState::BindStarted; + Ok(()) + } + + fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match self.udp_state { + UdpState::BindStarted => { + self.udp_state = UdpState::Bound; + Ok(()) + } + _ => Err(ErrorCode::NotInProgress), + } + } + + fn is_connected(&self) -> bool { + matches!(self.udp_state, UdpState::Connected(..)) + } + + fn is_bound(&self) -> bool { + matches!(self.udp_state, UdpState::Connected(..) | UdpState::Bound) + } + + fn disconnect(&mut self) -> Result<(), ErrorCode> { + if !self.is_connected() { + return Err(ErrorCode::InvalidState); + } + udp_disconnect(&self.socket)?; + self.udp_state = UdpState::Bound; + Ok(()) + } + + fn connect(&mut self, addr: SocketAddr) -> Result<(), ErrorCode> { + if !is_valid_address_family(addr.ip(), self.family) || !is_valid_remote_address(addr) { + return Err(ErrorCode::InvalidArgument); + } + + match self.udp_state { + UdpState::Bound | UdpState::Connected(_) => {} + _ => return Err(ErrorCode::InvalidState), + } + + // We disconnect & (re)connect in two distinct steps for two reasons: + // - To leave our socket instance in a consistent state in case the + // connect fails. + // - When reconnecting to a different address, Linux sometimes fails + // if there isn't a disconnect in between. + + // Step #1: Disconnect + if let UdpState::Connected(..) = self.udp_state { + udp_disconnect(&self.socket)?; + self.udp_state = UdpState::Bound; + } + // Step #2: (Re)connect + connect(&self.socket, &addr).map_err(|error| match error { + Errno::AFNOSUPPORT => ErrorCode::InvalidArgument, // See `udp_bind` implementation. + Errno::INPROGRESS => { + debug!("UDP connect returned EINPROGRESS, which should never happen"); + ErrorCode::Unknown + } + err => err.into(), + })?; + self.udp_state = UdpState::Connected(addr); + Ok(()) + } + + #[cfg(feature = "p3")] + fn send(&self, buf: Vec) -> impl Future> + use<> { + let socket = if let UdpState::Connected(..) = self.udp_state { + Ok(Arc::clone(&self.socket)) + } else { + Err(ErrorCode::InvalidArgument) + }; + async move { + let socket = socket?; + send(&socket, &buf).await + } + } + + #[cfg(feature = "p3")] + fn send_to( + &self, + buf: Vec, + addr: SocketAddr, + ) -> impl Future> + use<> { + enum Mode { + Send(Arc), + SendTo(Arc, SocketAddr), + } + let socket = match &self.udp_state { + UdpState::BindStarted => Err(ErrorCode::InvalidState), + UdpState::Default | UdpState::Bound => Ok(Mode::SendTo(Arc::clone(&self.socket), addr)), + UdpState::Connected(caddr) if addr == *caddr => { + Ok(Mode::Send(Arc::clone(&self.socket))) + } + UdpState::Connected(..) => Err(ErrorCode::InvalidArgument), + }; + async move { + match socket? { + Mode::Send(socket) => send(&socket, &buf).await, + Mode::SendTo(socket, addr) => send_to(&socket, &buf, addr).await, + } + } + } + + #[cfg(feature = "p3")] + fn receive(&self) -> impl Future, SocketAddr), ErrorCode>> + use<> { + enum Mode { + Recv(Arc, SocketAddr), + RecvFrom(Arc), + } + let socket = match self.udp_state { + UdpState::Default | UdpState::BindStarted => Err(ErrorCode::InvalidState), + UdpState::Bound => Ok(Mode::RecvFrom(Arc::clone(&self.socket))), + UdpState::Connected(addr) => Ok(Mode::Recv(Arc::clone(&self.socket), addr)), + }; + async move { + let socket = socket?; + let mut buf = vec![0; super::MAX_UDP_DATAGRAM_SIZE]; + let (n, addr) = match socket { + Mode::Recv(socket, addr) => { + let n = socket.recv(&mut buf).await?; + (n, addr) + } + Mode::RecvFrom(socket) => { + let (n, addr) = socket.recv_from(&mut buf).await?; + (n, addr) + } + }; + buf.truncate(n); + Ok((buf, addr)) + } + } + + fn local_address(&self) -> Result { + if matches!(self.udp_state, UdpState::Default | UdpState::BindStarted) { + return Err(ErrorCode::InvalidState); + } + let addr = self + .socket + .as_socketlike_view::() + .local_addr()?; + Ok(addr) + } + + fn remote_address(&self) -> Result { + if !matches!(self.udp_state, UdpState::Connected(..)) { + return Err(ErrorCode::InvalidState); + } + let addr = self + .socket + .as_socketlike_view::() + .peer_addr()?; + Ok(addr) + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + self.family + } + + fn unicast_hop_limit(&self) -> Result { + let n = get_unicast_hop_limit(&self.socket, self.family)?; + Ok(n) + } + + fn set_unicast_hop_limit(&self, value: u8) -> Result<(), ErrorCode> { + set_unicast_hop_limit(&self.socket, self.family, value)?; + Ok(()) + } + + fn receive_buffer_size(&self) -> Result { + let n = receive_buffer_size(&self.socket)?; + Ok(n) + } + + fn set_receive_buffer_size(&self, value: u64) -> Result<(), ErrorCode> { + set_receive_buffer_size(&self.socket, value)?; + Ok(()) + } + + fn send_buffer_size(&self) -> Result { + let n = send_buffer_size(&self.socket)?; + Ok(n) + } + + fn set_send_buffer_size(&self, value: u64) -> Result<(), ErrorCode> { + set_send_buffer_size(&self.socket, value)?; + Ok(()) + } + + pub(crate) fn socket(&self) -> &Arc { + &self.socket + } + + pub(crate) fn socket_addr_check(&self) -> Option<&SocketAddrCheck> { + self.socket_addr_check.as_ref() + } + + fn set_socket_addr_check(&mut self, check: Option) { + self.socket_addr_check = check; + } +} + +#[cfg(feature = "p3")] +async fn send(socket: &tokio::net::UdpSocket, buf: &[u8]) -> Result<(), ErrorCode> { + let n = socket.send(buf).await?; + // From Rust stdlib docs: + // > Note that the operating system may refuse buffers larger than 65507. + // > However, partial writes are not possible until buffer sizes above `i32::MAX`. + // + // For example, on Windows, at most `i32::MAX` bytes will be written + if n != buf.len() { + Err(ErrorCode::Unknown) + } else { + Ok(()) + } +} + +#[cfg(feature = "p3")] +async fn send_to( + socket: &tokio::net::UdpSocket, + buf: &[u8], + addr: SocketAddr, +) -> Result<(), ErrorCode> { + let n = socket.send_to(buf, addr).await?; + // See [`send`] documentation + if n != buf.len() { + Err(ErrorCode::Unknown) + } else { + Ok(()) + } +} + +impl super::loopback::UdpSocket { + pub fn new( + socket: &NetworkUdpSocket, + state: super::loopback::UdpState, + ) -> Result { + let hop_limit = get_unicast_hop_limit(&socket.socket, socket.family)?; + + let receive_buffer_size = receive_buffer_size(&socket.socket)?; + + let send_buffer_size = send_buffer_size(&socket.socket)?; + let send_buffer_size = send_buffer_size + .try_into() + .unwrap_or(Self::MAX_SEND_BUFFER_SIZE); + + Ok(Self { + state, + hop_limit, + receive_buffer_size, + send_buffer_size, + family: socket.family, + socket_addr_check: socket.socket_addr_check.clone(), + }) + } +} + +pub enum UdpSocket { + Network(NetworkUdpSocket), + Loopback(super::loopback::UdpSocket), + Unspecified { + net: NetworkUdpSocket, + lo: super::loopback::UdpSocket, + }, +} + +impl UdpSocket { + pub(crate) fn new(cx: &WasiSocketsCtx, family: AddressFamily) -> Result { + NetworkUdpSocket::new(cx, family).map(Self::Network) + } + + pub(crate) fn bind( + &mut self, + mut addr: SocketAddr, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + use core::net::{Ipv4Addr, Ipv6Addr}; + + let Self::Network(socket) = self else { + return Err(ErrorCode::InvalidState); + }; + if !matches!(socket.udp_state, UdpState::Default) { + return Err(ErrorCode::InvalidState); + } + if !is_valid_address_family(addr.ip(), socket.family) { + return Err(ErrorCode::InvalidArgument); + } + let ip = addr.ip().to_canonical(); + if !ip.is_loopback() { + socket.bind(addr)?; + if !ip.is_unspecified() { + return Ok(()); + } + addr = socket.socket.local_addr()?; + match &mut addr { + SocketAddr::V4(addr) => addr.set_ip(Ipv4Addr::LOCALHOST), + SocketAddr::V6(addr) => addr.set_ip(Ipv6Addr::LOCALHOST), + } + }; + + let (addr, rx) = loopback.bind_udp(addr)?; + let lo = super::loopback::UdpSocket::new( + socket, + super::loopback::UdpState::BindStarted { + local_address: addr, + rx, + }, + )?; + + if ip.is_unspecified() { + *self = Self::Unspecified { + net: socket.clone(), + lo, + } + } else { + *self = Self::Loopback(lo); + } + return Ok(()); + } + + pub(crate) fn finish_bind(&mut self) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.finish_bind(), + Self::Loopback(socket) => socket.finish_bind(), + Self::Unspecified { net, lo } => { + net.finish_bind()?; + lo.finish_bind() + } + } + } + + pub(crate) fn is_connected(&self) -> bool { + match self { + Self::Network(socket) => socket.is_connected(), + Self::Loopback(socket) => socket.is_connected(), + Self::Unspecified { net, lo } => net.is_connected() && lo.is_connected(), + } + } + + pub(crate) fn is_bound(&self) -> bool { + match self { + Self::Network(socket) => socket.is_bound(), + Self::Loopback(socket) => socket.is_bound(), + Self::Unspecified { net, lo } => net.is_bound() && lo.is_bound(), + } + } + + pub(crate) fn disconnect( + &mut self, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.disconnect(), + Self::Loopback(socket) => socket.disconnect(loopback), + Self::Unspecified { net, lo } => { + net.disconnect()?; + lo.disconnect(loopback) + } + } + } + + pub(crate) fn connect( + &mut self, + addr: SocketAddr, + loopback: &mut super::loopback::Network, + ) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.connect(addr), + Self::Loopback(socket) => socket.connect(addr, loopback), + Self::Unspecified { net, lo } => { + net.connect(addr)?; + lo.connect(addr, loopback) + } + } + } + + #[cfg(feature = "p3")] + pub(crate) fn send(&self, buf: Vec) -> impl Future> + use<> { + match self { + Self::Network(socket) => socket.send(buf), + Self::Loopback(..) | Self::Unspecified { .. } => todo!(), + } + } + + #[cfg(feature = "p3")] + pub(crate) fn send_to( + &self, + buf: Vec, + addr: SocketAddr, + ) -> impl Future> + use<> { + match self { + Self::Network(socket) => socket.send_to(buf, addr), + Self::Loopback(..) | Self::Unspecified { .. } => todo!(), + } + } + + #[cfg(feature = "p3")] + pub(crate) fn receive( + &self, + ) -> impl Future, SocketAddr), ErrorCode>> + use<> { + match self { + Self::Network(socket) => socket.receive(), + Self::Loopback(..) | Self::Unspecified { .. } => todo!(), + } + } + + pub(crate) fn local_address(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => socket.local_address(), + Self::Loopback(socket) => socket.local_address(), + } + } + + pub(crate) fn remote_address(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.remote_address() + } + Self::Loopback(socket) => socket.remote_address(), + } + } + + pub(crate) fn address_family(&self) -> SocketAddressFamily { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.address_family() + } + Self::Loopback(socket) => socket.address_family(), + } + } + + pub(crate) fn unicast_hop_limit(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.unicast_hop_limit() + } + Self::Loopback(socket) => socket.unicast_hop_limit(), + } + } + + pub(crate) fn set_unicast_hop_limit(&mut self, value: u8) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_unicast_hop_limit(value), + Self::Loopback(socket) => socket.set_unicast_hop_limit(value), + Self::Unspecified { net, lo } => { + net.set_unicast_hop_limit(value)?; + lo.set_unicast_hop_limit(value) + } + } + } + + pub(crate) fn receive_buffer_size(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.receive_buffer_size() + } + Self::Loopback(socket) => socket.receive_buffer_size(), + } + } + + pub(crate) fn set_receive_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_receive_buffer_size(value), + Self::Loopback(socket) => socket.set_receive_buffer_size(value), + Self::Unspecified { net, lo } => { + net.set_receive_buffer_size(value)?; + lo.set_receive_buffer_size(value) + } + } + } + + pub(crate) fn send_buffer_size(&self) -> Result { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.send_buffer_size() + } + Self::Loopback(socket) => socket.send_buffer_size(), + } + } + + pub(crate) fn set_send_buffer_size(&mut self, value: u64) -> Result<(), ErrorCode> { + match self { + Self::Network(socket) => socket.set_send_buffer_size(value), + Self::Loopback(socket) => socket.set_send_buffer_size(value), + Self::Unspecified { net, lo } => { + net.set_send_buffer_size(value)?; + lo.set_send_buffer_size(value) + } + } + } + + pub(crate) fn socket_addr_check(&self) -> Option<&SocketAddrCheck> { + match self { + Self::Network(socket) | Self::Unspecified { net: socket, .. } => { + socket.socket_addr_check() + } + Self::Loopback(socket) => socket.socket_addr_check(), + } + } + + pub(crate) fn set_socket_addr_check(&mut self, check: Option) { + match self { + Self::Network(socket) => socket.set_socket_addr_check(check), + Self::Loopback(socket) => socket.set_socket_addr_check(check), + Self::Unspecified { net, lo } => { + net.set_socket_addr_check(check.clone()); + lo.set_socket_addr_check(check); + } + } + } + + pub(crate) fn drop(self, loopback: &mut super::loopback::Network) -> wasmtime::Result<()> { + match self { + Self::Network(socket) => { + drop(socket); + Ok(()) + } + Self::Loopback(socket) => socket.drop(loopback), + Self::Unspecified { net, lo } => { + drop(net); + lo.drop(loopback) + } + } + } +} diff --git a/crates/wasi/src/sockets/util.rs b/crates/wasi/src/sockets/util.rs new file mode 100644 index 00000000..a9679a09 --- /dev/null +++ b/crates/wasi/src/sockets/util.rs @@ -0,0 +1,433 @@ +use core::fmt; +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use core::str::FromStr as _; +use core::time::Duration; + +use cap_net_ext::{AddressFamily, Blocking, UdpSocketExt}; +use rustix::fd::AsFd; +use rustix::io::Errno; +use rustix::net::{bind, connect_unspec, sockopt}; +use tracing::debug; + +use crate::sockets::SocketAddressFamily; + +#[derive(Debug)] +pub enum ErrorCode { + Unknown, + AccessDenied, + NotSupported, + InvalidArgument, + OutOfMemory, + Timeout, + InvalidState, + AddressNotBindable, + AddressInUse, + RemoteUnreachable, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + DatagramTooLarge, + NotInProgress, + ConcurrencyConflict, +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl std::error::Error for ErrorCode {} + +fn is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool { + matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _]) + && addr != Ipv6Addr::UNSPECIFIED + && addr != Ipv6Addr::LOCALHOST +} + +pub fn is_valid_address_family(addr: IpAddr, socket_family: SocketAddressFamily) -> bool { + match (socket_family, addr) { + (SocketAddressFamily::Ipv4, IpAddr::V4(..)) => true, + (SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => { + // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated + // since 2006, OS handling of them is inconsistent and our own + // validations don't take them into account either. + // Note that these are not the same as IPv4-*mapped* IPv6 addresses. + !is_deprecated_ipv4_compatible(ipv6) && ipv6.to_ipv4_mapped().is_none() + } + _ => false, + } +} + +pub fn is_valid_remote_address(addr: SocketAddr) -> bool { + !addr.ip().to_canonical().is_unspecified() && addr.port() != 0 +} + +pub fn is_valid_unicast_address(addr: IpAddr) -> bool { + match addr.to_canonical() { + IpAddr::V4(ipv4) => !ipv4.is_multicast() && !ipv4.is_broadcast(), + IpAddr::V6(ipv6) => !ipv6.is_multicast(), + } +} + +pub fn to_ipv4_addr(addr: (u8, u8, u8, u8)) -> Ipv4Addr { + let (x0, x1, x2, x3) = addr; + Ipv4Addr::new(x0, x1, x2, x3) +} + +pub fn from_ipv4_addr(addr: Ipv4Addr) -> (u8, u8, u8, u8) { + let [x0, x1, x2, x3] = addr.octets(); + (x0, x1, x2, x3) +} + +pub fn to_ipv6_addr(addr: (u16, u16, u16, u16, u16, u16, u16, u16)) -> Ipv6Addr { + let (x0, x1, x2, x3, x4, x5, x6, x7) = addr; + Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7) +} + +pub fn from_ipv6_addr(addr: Ipv6Addr) -> (u16, u16, u16, u16, u16, u16, u16, u16) { + let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments(); + (x0, x1, x2, x3, x4, x5, x6, x7) +} + +/* + * Syscalls wrappers with (opinionated) portability fixes. + */ + +pub fn normalize_get_buffer_size(value: usize) -> usize { + if cfg!(target_os = "linux") { + // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead. + // getsockopt returns this internally doubled value. + // We'll half the value to at least get it back into the same ballpark that the application requested it in. + // + // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs + value / 2 + } else { + value + } +} + +pub fn normalize_set_buffer_size(value: usize) -> usize { + value.clamp(1, i32::MAX as usize) +} + +impl From for ErrorCode { + fn from(value: std::io::Error) -> Self { + (&value).into() + } +} + +impl From<&std::io::Error> for ErrorCode { + fn from(value: &std::io::Error) -> Self { + // Attempt the more detailed native error code first: + if let Some(errno) = Errno::from_io_error(value) { + return errno.into(); + } + + match value.kind() { + std::io::ErrorKind::AddrInUse => Self::AddressInUse, + std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable, + std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted, + std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused, + std::io::ErrorKind::ConnectionReset => Self::ConnectionReset, + std::io::ErrorKind::InvalidInput => Self::InvalidArgument, + std::io::ErrorKind::NotConnected => Self::InvalidState, + std::io::ErrorKind::OutOfMemory => Self::OutOfMemory, + std::io::ErrorKind::PermissionDenied => Self::AccessDenied, + std::io::ErrorKind::TimedOut => Self::Timeout, + std::io::ErrorKind::Unsupported => Self::NotSupported, + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} + +impl From for ErrorCode { + fn from(value: Errno) -> Self { + (&value).into() + } +} + +impl From<&Errno> for ErrorCode { + fn from(value: &Errno) -> Self { + match *value { + #[cfg(not(windows))] + Errno::PERM => Self::AccessDenied, + Errno::ACCESS => Self::AccessDenied, + Errno::ADDRINUSE => Self::AddressInUse, + Errno::ADDRNOTAVAIL => Self::AddressNotBindable, + Errno::TIMEDOUT => Self::Timeout, + Errno::CONNREFUSED => Self::ConnectionRefused, + Errno::CONNRESET => Self::ConnectionReset, + Errno::CONNABORTED => Self::ConnectionAborted, + Errno::INVAL => Self::InvalidArgument, + Errno::HOSTUNREACH => Self::RemoteUnreachable, + Errno::HOSTDOWN => Self::RemoteUnreachable, + Errno::NETDOWN => Self::RemoteUnreachable, + Errno::NETUNREACH => Self::RemoteUnreachable, + #[cfg(target_os = "linux")] + Errno::NONET => Self::RemoteUnreachable, + Errno::ISCONN => Self::InvalidState, + Errno::NOTCONN => Self::InvalidState, + Errno::DESTADDRREQ => Self::InvalidState, + Errno::MSGSIZE => Self::DatagramTooLarge, + #[cfg(not(windows))] + Errno::NOMEM => Self::OutOfMemory, + Errno::NOBUFS => Self::OutOfMemory, + Errno::OPNOTSUPP => Self::NotSupported, + Errno::NOPROTOOPT => Self::NotSupported, + Errno::PFNOSUPPORT => Self::NotSupported, + Errno::PROTONOSUPPORT => Self::NotSupported, + Errno::PROTOTYPE => Self::NotSupported, + Errno::SOCKTNOSUPPORT => Self::NotSupported, + Errno::AFNOSUPPORT => Self::NotSupported, + + // FYI, EINPROGRESS should have already been handled by connect. + _ => { + debug!("unknown I/O error: {value}"); + Self::Unknown + } + } + } +} + +pub fn get_ip_ttl(fd: impl AsFd) -> Result { + let v = sockopt::ip_ttl(fd)?; + let Ok(v) = v.try_into() else { + return Err(ErrorCode::NotSupported); + }; + Ok(v) +} + +pub fn get_ipv6_unicast_hops(fd: impl AsFd) -> Result { + let v = sockopt::ipv6_unicast_hops(fd)?; + Ok(v) +} + +pub fn get_unicast_hop_limit(fd: impl AsFd, family: SocketAddressFamily) -> Result { + match family { + SocketAddressFamily::Ipv4 => get_ip_ttl(fd), + SocketAddressFamily::Ipv6 => get_ipv6_unicast_hops(fd), + } +} + +pub fn set_unicast_hop_limit( + fd: impl AsFd, + family: SocketAddressFamily, + value: u8, +) -> Result<(), ErrorCode> { + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + // + // A well-behaved IP application should never send out new packets with TTL 0. + // We validate the value ourselves because OS'es are not consistent in this. + // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation. + return Err(ErrorCode::InvalidArgument); + } + match family { + SocketAddressFamily::Ipv4 => { + sockopt::set_ip_ttl(fd, value.into())?; + } + SocketAddressFamily::Ipv6 => { + sockopt::set_ipv6_unicast_hops(fd, Some(value))?; + } + } + Ok(()) +} + +pub fn receive_buffer_size(fd: impl AsFd) -> Result { + let v = sockopt::socket_recv_buffer_size(fd)?; + Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX)) +} + +pub fn set_receive_buffer_size(fd: impl AsFd, value: u64) -> Result { + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = value.try_into().unwrap_or(usize::MAX); + let value = normalize_set_buffer_size(value); + match sockopt::set_socket_recv_buffer_size(fd, value) { + // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value + // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds + // their capability. + // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the + // value is out of range. We normalize this behavior in favor of the more commonly understood + // "performance hint" semantics. In other words; even ENOBUFS is "Ok". + // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input + // `size` ourselves, to completely close the gap with other platforms. + // + // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs + Err(Errno::NOBUFS) => {} + Err(err) => return Err(err.into()), + _ => {} + }; + Ok(value) +} + +pub fn send_buffer_size(fd: impl AsFd) -> Result { + let v = sockopt::socket_send_buffer_size(fd)?; + Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX)) +} + +pub fn set_send_buffer_size(fd: impl AsFd, value: u64) -> Result { + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = value.try_into().unwrap_or(usize::MAX); + let value = normalize_set_buffer_size(value); + match sockopt::set_socket_send_buffer_size(fd, value) { + Err(Errno::NOBUFS) => {} + Err(err) => return Err(err.into()), + _ => {} + }; + Ok(value) +} + +pub fn set_keep_alive_idle_time(fd: impl AsFd, value: u64) -> Result { + const NANOS_PER_SEC: u64 = 1_000_000_000; + + // Ensure that the value passed to the actual syscall never gets rounded down to 0. + const MIN: u64 = NANOS_PER_SEC; + + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX: u64 = (i16::MAX as u64) * NANOS_PER_SEC; + + if value <= 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + let value = value.clamp(MIN, MAX); + sockopt::set_tcp_keepidle(fd, Duration::from_nanos(value))?; + Ok(value) +} + +pub fn set_keep_alive_interval(fd: impl AsFd, value: Duration) -> Result<(), ErrorCode> { + // Ensure that any fractional value passed to the actual syscall never gets rounded down to 0. + const MIN: Duration = Duration::from_secs(1); + + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX: Duration = Duration::from_secs(i16::MAX as u64); + + if value <= Duration::ZERO { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + sockopt::set_tcp_keepintvl(fd, value.clamp(MIN, MAX))?; + Ok(()) +} + +pub fn set_keep_alive_count(fd: impl AsFd, value: u32) -> Result<(), ErrorCode> { + const MIN_CNT: u32 = 1; + // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms. + const MAX_CNT: u32 = i8::MAX as u32; + + if value == 0 { + // WIT: "If the provided value is 0, an `invalid-argument` error is returned." + return Err(ErrorCode::InvalidArgument); + } + sockopt::set_tcp_keepcnt(fd, value.clamp(MIN_CNT, MAX_CNT))?; + Ok(()) +} + +pub fn tcp_bind( + socket: &tokio::net::TcpSocket, + local_address: SocketAddr, +) -> Result<(), ErrorCode> { + // Automatically bypass the TIME_WAIT state when binding to a specific port + // Unconditionally (re)set SO_REUSEADDR, even when the value is false. + // This ensures we're not accidentally affected by any socket option + // state left behind by a previous failed call to this method. + #[cfg(not(windows))] + if let Err(err) = sockopt::set_socket_reuseaddr(&socket, local_address.port() > 0) { + return Err(err.into()); + } + + // Perform the OS bind call. + socket + .bind(local_address) + .map_err(|err| match Errno::from_io_error(&err) { + // From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html: + // > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket + // + // The most common reasons for this error should have already + // been handled by our own validation slightly higher up in this + // function. This error mapping is here just in case there is + // an edge case we didn't catch. + Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS + // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. + #[cfg(windows)] + Some(Errno::NOBUFS) => ErrorCode::AddressInUse, + _ => err.into(), + }) +} + +pub fn udp_socket(family: AddressFamily) -> std::io::Result { + // Delegate socket creation to cap_net_ext. They handle a couple of things for us: + // - On Windows: call WSAStartup if not done before. + // - Set the NONBLOCK and CLOEXEC flags. Either immediately during socket creation, + // or afterwards using ioctl or fcntl. Exact method depends on the platform. + + let socket = cap_std::net::UdpSocket::new(family, Blocking::No)?; + Ok(socket) +} + +pub fn udp_bind(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), ErrorCode> { + bind(sockfd, &addr).map_err(|err| match err { + // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS + // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. + #[cfg(windows)] + Errno::NOBUFS => ErrorCode::AddressInUse, + // From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html: + // > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket + // + // The most common reasons for this error should have already + // been handled by our own validation slightly higher up in this + // function. This error mapping is here just in case there is + // an edge case we didn't catch. + Errno::AFNOSUPPORT => ErrorCode::InvalidArgument, + _ => err.into(), + }) +} + +pub fn udp_disconnect(sockfd: impl AsFd) -> Result<(), ErrorCode> { + match connect_unspec(sockfd) { + // BSD platforms return an error even if the UDP socket was disconnected successfully. + // + // MacOS was kind enough to document this: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/connect.2.html + // > Datagram sockets may dissolve the association by connecting to an + // > invalid address, such as a null address or an address with the address + // > family set to AF_UNSPEC (the error EAFNOSUPPORT will be harmlessly + // > returned). + // + // ... except that this appears to be incomplete, because experiments + // have shown that MacOS actually returns EINVAL, depending on the + // address family of the socket. + #[cfg(target_os = "macos")] + Err(Errno::INVAL | Errno::AFNOSUPPORT) => Ok(()), + Err(err) => Err(err.into()), + Ok(()) => Ok(()), + } +} + +pub fn parse_host(name: &str) -> Result { + // `url::Host::parse` serves us two functions: + // 1. validate the input is a valid domain name or IP, + // 2. convert unicode domains to punycode. + match url::Host::parse(&name) { + Ok(host) => Ok(host), + + // `url::Host::parse` doesn't understand bare IPv6 addresses without [brackets] + Err(_) => { + if let Ok(addr) = Ipv6Addr::from_str(name) { + Ok(url::Host::Ipv6(addr)) + } else { + Err(ErrorCode::InvalidArgument) + } + } + } +} diff --git a/crates/wasi/src/view.rs b/crates/wasi/src/view.rs new file mode 100644 index 00000000..b4b9d948 --- /dev/null +++ b/crates/wasi/src/view.rs @@ -0,0 +1,95 @@ +use crate::WasiCtx; +use wasmtime::component::ResourceTable; + +/// A trait which provides access to the [`WasiCtx`] inside the embedder's `T` +/// of [`Store`][`Store`]. +/// +/// This crate's WASI Host implementations depend on the contents of +/// [`WasiCtx`]. The `T` type [`Store`][`Store`] is defined in each +/// embedding of Wasmtime. These implementations are connected to the +/// [`Linker`][`Linker`] by [`add_to_linker`](crate::p2::add_to_linker) +/// functions. +/// +/// # Example +/// +/// ``` +/// use wash_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime::component::ResourceTable; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +/// [`Store`]: wasmtime::Store +/// [`Linker`]: wasmtime::component::Linker +/// +pub trait WasiView: Send { + /// Yields mutable access to the [`WasiCtx`] configuration used for this + /// context. + fn ctx(&mut self) -> WasiCtxView<'_>; +} + +/// Structure returned from [`WasiView::ctx`] which provides access to WASI +/// state for host functions to be implemented with. +pub struct WasiCtxView<'a> { + /// The [`WasiCtx`], or configuration, of the guest. + pub ctx: &'a mut WasiCtx, + /// Resources, such as files/streams, that the guest is using. + pub table: &'a mut ResourceTable, +} + +impl crate::cli::WasiCliView for T { + fn cli(&mut self) -> crate::cli::WasiCliCtxView<'_> { + let WasiCtxView { ctx, table } = self.ctx(); + crate::cli::WasiCliCtxView { + ctx: &mut ctx.cli, + table, + } + } +} + +impl crate::clocks::WasiClocksView for T { + fn clocks(&mut self) -> crate::clocks::WasiClocksCtxView<'_> { + let WasiCtxView { ctx, table } = self.ctx(); + crate::clocks::WasiClocksCtxView { + ctx: &mut ctx.clocks, + table, + } + } +} + +impl crate::filesystem::WasiFilesystemView for T { + fn filesystem(&mut self) -> crate::filesystem::WasiFilesystemCtxView<'_> { + let WasiCtxView { ctx, table } = self.ctx(); + crate::filesystem::WasiFilesystemCtxView { + ctx: &mut ctx.filesystem, + table, + } + } +} + +impl crate::random::WasiRandomView for T { + fn random(&mut self) -> &mut crate::random::WasiRandomCtx { + &mut self.ctx().ctx.random + } +} + +impl crate::sockets::WasiSocketsView for T { + fn sockets(&mut self) -> crate::sockets::WasiSocketsCtxView<'_> { + let WasiCtxView { ctx, table } = self.ctx(); + crate::sockets::WasiSocketsCtxView { + ctx: &mut ctx.sockets, + table, + } + } +} diff --git a/crates/wasi/tests/all/main.rs b/crates/wasi/tests/all/main.rs new file mode 100644 index 00000000..9fcf355c --- /dev/null +++ b/crates/wasi/tests/all/main.rs @@ -0,0 +1,15 @@ +macro_rules! assert_test_exists { + ($name:ident) => { + #[expect(unused_imports, reason = "just here to ensure a name exists")] + use self::$name as _; + }; +} + +mod store; + +#[cfg(feature = "p1")] +mod p1; +#[cfg(feature = "p2")] +mod p2; +#[cfg(feature = "p3")] +mod p3; diff --git a/crates/wasi/tests/all/p1.rs b/crates/wasi/tests/all/p1.rs new file mode 100644 index 00000000..36da6fd2 --- /dev/null +++ b/crates/wasi/tests/all/p1.rs @@ -0,0 +1,253 @@ +use crate::store::Ctx; +use anyhow::Result; +use std::path::Path; +use test_programs_artifacts::*; +use wash_wasi::p1::{WasiP1Ctx, add_to_linker_async}; +use wasmtime::{Linker, Module}; + +async fn run(path: &str, inherit_stdio: bool) -> Result<()> { + let path = Path::new(path); + let name = path.file_stem().unwrap().to_str().unwrap(); + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + }); + let mut linker = Linker::>::new(&engine); + add_to_linker_async(&mut linker, |t| &mut t.wasi)?; + + let module = Module::from_file(&engine, path)?; + let (mut store, _td) = Ctx::new(&engine, name, |builder| { + if inherit_stdio { + builder.inherit_stdio(); + } + builder.build_p1() + })?; + let instance = linker.instantiate_async(&mut store, &module).await?; + let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; + start.call_async(&mut store, ()).await?; + Ok(()) +} + +foreach_preview1!(assert_test_exists); + +// Below here is mechanical: there should be one test for every binary in +// wasi-tests. +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_big_random_buf() { + run(PREVIEW1_BIG_RANDOM_BUF, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_clock_time_get() { + run(PREVIEW1_CLOCK_TIME_GET, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_close_preopen() { + run(PREVIEW1_CLOSE_PREOPEN, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dangling_fd() { + run(PREVIEW1_DANGLING_FD, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dangling_symlink() { + run(PREVIEW1_DANGLING_SYMLINK, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_directory_seek() { + run(PREVIEW1_DIRECTORY_SEEK, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dir_fd_op_failures() { + run(PREVIEW1_DIR_FD_OP_FAILURES, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_advise() { + run(PREVIEW1_FD_ADVISE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_filestat_get() { + run(PREVIEW1_FD_FILESTAT_GET, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_filestat_set() { + run(PREVIEW1_FD_FILESTAT_SET, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_flags_set() { + run(PREVIEW1_FD_FLAGS_SET, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_readdir() { + run(PREVIEW1_FD_READDIR, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_allocate() { + run(PREVIEW1_FILE_ALLOCATE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_pread_pwrite() { + run(PREVIEW1_FILE_PREAD_PWRITE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_read_write() { + run(PREVIEW1_FILE_READ_WRITE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_seek_tell() { + run(PREVIEW1_FILE_SEEK_TELL, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_truncation() { + run(PREVIEW1_FILE_TRUNCATION, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_unbuffered_write() { + run(PREVIEW1_FILE_UNBUFFERED_WRITE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_interesting_paths() { + run(PREVIEW1_INTERESTING_PATHS, true).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_regular_file_isatty() { + run(PREVIEW1_REGULAR_FILE_ISATTY, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_nofollow_errors() { + run(PREVIEW1_NOFOLLOW_ERRORS, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_overwrite_preopen() { + run(PREVIEW1_OVERWRITE_PREOPEN, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_exists() { + run(PREVIEW1_PATH_EXISTS, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_filestat() { + run(PREVIEW1_PATH_FILESTAT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_link() { + run(PREVIEW1_PATH_LINK, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_create_existing() { + run(PREVIEW1_PATH_OPEN_CREATE_EXISTING, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_read_write() { + run(PREVIEW1_PATH_OPEN_READ_WRITE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_dirfd_not_dir() { + run(PREVIEW1_PATH_OPEN_DIRFD_NOT_DIR, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_missing() { + run(PREVIEW1_PATH_OPEN_MISSING, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_nonblock() { + run(PREVIEW1_PATH_OPEN_NONBLOCK, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_rename_dir_trailing_slashes() { + run(PREVIEW1_PATH_RENAME_DIR_TRAILING_SLASHES, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_rename() { + run(PREVIEW1_PATH_RENAME, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_symlink_trailing_slashes() { + run(PREVIEW1_PATH_SYMLINK_TRAILING_SLASHES, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_poll_oneoff_files() { + run(PREVIEW1_POLL_ONEOFF_FILES, false).await.unwrap() +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_poll_oneoff_stdio() { + run(PREVIEW1_POLL_ONEOFF_STDIO, true).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_readlink() { + run(PREVIEW1_READLINK, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_remove_directory() { + run(PREVIEW1_REMOVE_DIRECTORY, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_remove_nonempty_directory() { + run(PREVIEW1_REMOVE_NONEMPTY_DIRECTORY, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_renumber() { + run(PREVIEW1_RENUMBER, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_sched_yield() { + run(PREVIEW1_SCHED_YIELD, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio() { + run(PREVIEW1_STDIO, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio_isatty() { + // If the test process is setup such that stdio is a terminal: + if test_programs_artifacts::stdio_is_terminal() { + // Inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_ISATTY, true).await.unwrap() + } +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio_not_isatty() { + // Don't inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_NOT_ISATTY, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_create() { + run(PREVIEW1_SYMLINK_CREATE, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_filestat() { + run(PREVIEW1_SYMLINK_FILESTAT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_loop() { + run(PREVIEW1_SYMLINK_LOOP, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_unlink_file_trailing_slashes() { + run(PREVIEW1_UNLINK_FILE_TRAILING_SLASHES, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_preopen() { + run(PREVIEW1_PATH_OPEN_PREOPEN, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_unicode_output() { + run(PREVIEW1_UNICODE_OUTPUT, true).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_write() { + run(PREVIEW1_FILE_WRITE, true).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_lots() { + run(PREVIEW1_PATH_OPEN_LOTS, true).await.unwrap() +} diff --git a/crates/wasi/tests/all/p2/api.rs b/crates/wasi/tests/all/p2/api.rs new file mode 100644 index 00000000..81fcdedb --- /dev/null +++ b/crates/wasi/tests/all/p2/api.rs @@ -0,0 +1,203 @@ +use anyhow::Result; +use std::io::Write; +use std::sync::Mutex; +use std::time::Duration; +use wash_wasi::p2::add_to_linker_async; +use wash_wasi::p2::bindings::{Command, clocks::wall_clock, filesystem::types as filesystem}; +use wash_wasi::{ + DirPerms, FilePerms, HostMonotonicClock, HostWallClock, WasiCtx, WasiCtxBuilder, WasiCtxView, + WasiView, +}; +use wasmtime::Store; +use wasmtime::component::{Component, Linker, ResourceTable}; + +struct CommandCtx { + table: ResourceTable, + wasi: WasiCtx, +} + +impl WasiView for CommandCtx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +use test_programs_artifacts::*; + +foreach_api!(assert_test_exists); + +async fn instantiate(path: &str, ctx: CommandCtx) -> Result<(Store, Command)> { + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + }); + let mut linker = Linker::new(&engine); + add_to_linker_async(&mut linker)?; + + let mut store = Store::new(&engine, ctx); + let component = Component::from_file(&engine, path)?; + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + Ok((store, command)) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn api_time() -> Result<()> { + struct FakeWallClock; + + impl HostWallClock for FakeWallClock { + fn resolution(&self) -> Duration { + Duration::from_secs(1) + } + + fn now(&self) -> Duration { + Duration::new(1431648000, 100) + } + } + + struct FakeMonotonicClock { + now: Mutex, + } + + impl HostMonotonicClock for FakeMonotonicClock { + fn resolution(&self) -> u64 { + 1_000_000_000 + } + + fn now(&self) -> u64 { + let mut now = self.now.lock().unwrap(); + let then = *now; + *now += 42 * 1_000_000_000; + then + } + } + + let table = ResourceTable::new(); + let wasi = WasiCtxBuilder::new() + .monotonic_clock(FakeMonotonicClock { now: Mutex::new(0) }) + .wall_clock(FakeWallClock) + .build(); + + let (mut store, command) = instantiate(API_TIME_COMPONENT, CommandCtx { table, wasi }).await?; + + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn api_read_only() -> Result<()> { + let dir = tempfile::tempdir()?; + + std::fs::File::create(dir.path().join("bar.txt"))?.write_all(b"And stood awhile in thought")?; + std::fs::create_dir(dir.path().join("sub"))?; + + let table = ResourceTable::new(); + let wasi = WasiCtxBuilder::new() + .preopened_dir(dir.path(), "/", DirPerms::READ, FilePerms::READ)? + .build(); + + let (mut store, command) = + instantiate(API_READ_ONLY_COMPONENT, CommandCtx { table, wasi }).await?; + + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) +} + +#[expect( + dead_code, + reason = "tested in the wasi-http crate, satisfying foreach_api! macro" +)] +fn api_proxy() {} + +#[expect( + dead_code, + reason = "tested in the wasi-http crate, satisfying foreach_api! macro" +)] +fn api_proxy_streaming() {} + +#[expect( + dead_code, + reason = "tested in the wasi-http crate, satisfying foreach_api! macro" +)] +fn api_proxy_forward_request() {} + +wasmtime::component::bindgen!({ + path: "src/p2/wit", + world: "test-reactor", + imports: { default: async }, + exports: { default: async }, + require_store_data_send: true, + with: { "wasi": wash_wasi::p2::bindings }, + ownership: Borrowing { + duplicate_if_necessary: false + } +}); + +#[test_log::test(tokio::test)] +async fn api_reactor() -> Result<()> { + let table = ResourceTable::new(); + let wasi = WasiCtxBuilder::new().env("GOOD_DOG", "gussie").build(); + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + }); + let mut linker = Linker::new(&engine); + add_to_linker_async(&mut linker)?; + + let mut store = Store::new(&engine, CommandCtx { table, wasi }); + let component = Component::from_file(&engine, API_REACTOR_COMPONENT)?; + let reactor = TestReactor::instantiate_async(&mut store, &component, &linker).await?; + + // Show that integration with the WASI context is working - the guest will + // interpolate $GOOD_DOG to gussie here using the environment: + let r = reactor + .call_add_strings(&mut store, &["hello", "$GOOD_DOG"]) + .await?; + assert_eq!(r, 2); + + let contents = reactor.call_get_strings(&mut store).await?; + assert_eq!(contents, &["hello", "gussie"]); + + // Show that we can pass in a resource type whose impls are defined in the + // `host` and `wasi-common` crate. + // Note, this works because of the add_to_linker invocations using the + // `host` crate for `streams`, not because of `with` in the bindgen macro. + let writepipe = wash_wasi::p2::pipe::MemoryOutputPipe::new(4096); + let stream: wash_wasi::p2::DynOutputStream = Box::new(writepipe.clone()); + let table_ix = store.data_mut().table.push(stream)?; + let r = reactor.call_write_strings_to(&mut store, table_ix).await?; + assert_eq!(r, Ok(())); + + assert_eq!(writepipe.contents().as_ref(), b"hellogussie"); + + // Show that the `with` invocation in the macro means we get to re-use the + // type definitions from inside the `host` crate for these structures: + let ds = filesystem::DescriptorStat { + data_access_timestamp: Some(wall_clock::Datetime { + nanoseconds: 123, + seconds: 45, + }), + data_modification_timestamp: Some(wall_clock::Datetime { + nanoseconds: 789, + seconds: 10, + }), + link_count: 0, + size: 0, + status_change_timestamp: Some(wall_clock::Datetime { + nanoseconds: 0, + seconds: 1, + }), + type_: filesystem::DescriptorType::Unknown, + }; + let expected = format!("{ds:?}"); + let got = reactor.call_pass_an_imported_record(&mut store, ds).await?; + assert_eq!(expected, got); + + Ok(()) +} diff --git a/crates/wasi/tests/all/p2/async_.rs b/crates/wasi/tests/all/p2/async_.rs new file mode 100644 index 00000000..e55e7674 --- /dev/null +++ b/crates/wasi/tests/all/p2/async_.rs @@ -0,0 +1,407 @@ +use crate::store::{Ctx, MyWasiCtx}; +use anyhow::Result; +use std::path::Path; +use test_programs_artifacts::*; +use wash_wasi::p2::add_to_linker_async; +use wash_wasi::p2::bindings::Command; +use wasmtime::component::{Component, Linker}; + +async fn run(path: &str, inherit_stdio: bool) -> Result<()> { + let path = Path::new(path); + let name = path.file_stem().unwrap().to_str().unwrap(); + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + }); + let mut linker = Linker::new(&engine); + add_to_linker_async(&mut linker)?; + + let (mut store, _td) = Ctx::new(&engine, name, |builder| { + if inherit_stdio { + builder.inherit_stdio(); + } + MyWasiCtx { + wasi: builder.build(), + table: Default::default(), + } + })?; + let component = Component::from_file(&engine, path)?; + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| anyhow::anyhow!("run returned a failure")) +} + +foreach_preview1!(assert_test_exists); +foreach_preview2!(assert_test_exists); + +// Below here is mechanical: there should be one test for every binary in +// wasi-tests. +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_big_random_buf() { + run(PREVIEW1_BIG_RANDOM_BUF_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_clock_time_get() { + run(PREVIEW1_CLOCK_TIME_GET_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_close_preopen() { + run(PREVIEW1_CLOSE_PREOPEN_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dangling_fd() { + run(PREVIEW1_DANGLING_FD_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dangling_symlink() { + run(PREVIEW1_DANGLING_SYMLINK_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_directory_seek() { + run(PREVIEW1_DIRECTORY_SEEK_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_dir_fd_op_failures() { + run(PREVIEW1_DIR_FD_OP_FAILURES_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_advise() { + run(PREVIEW1_FD_ADVISE_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_filestat_get() { + run(PREVIEW1_FD_FILESTAT_GET_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_filestat_set() { + run(PREVIEW1_FD_FILESTAT_SET_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_flags_set() { + run(PREVIEW1_FD_FLAGS_SET_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_fd_readdir() { + run(PREVIEW1_FD_READDIR_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_allocate() { + run(PREVIEW1_FILE_ALLOCATE_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_pread_pwrite() { + run(PREVIEW1_FILE_PREAD_PWRITE_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_read_write() { + run(PREVIEW1_FILE_READ_WRITE_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_seek_tell() { + run(PREVIEW1_FILE_SEEK_TELL_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_truncation() { + run(PREVIEW1_FILE_TRUNCATION_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_unbuffered_write() { + run(PREVIEW1_FILE_UNBUFFERED_WRITE_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_interesting_paths() { + run(PREVIEW1_INTERESTING_PATHS_COMPONENT, true) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_regular_file_isatty() { + run(PREVIEW1_REGULAR_FILE_ISATTY_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_nofollow_errors() { + run(PREVIEW1_NOFOLLOW_ERRORS_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_overwrite_preopen() { + run(PREVIEW1_OVERWRITE_PREOPEN_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_exists() { + run(PREVIEW1_PATH_EXISTS_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_filestat() { + run(PREVIEW1_PATH_FILESTAT_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_link() { + run(PREVIEW1_PATH_LINK_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_create_existing() { + run(PREVIEW1_PATH_OPEN_CREATE_EXISTING_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_read_write() { + run(PREVIEW1_PATH_OPEN_READ_WRITE_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_dirfd_not_dir() { + run(PREVIEW1_PATH_OPEN_DIRFD_NOT_DIR_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_missing() { + run(PREVIEW1_PATH_OPEN_MISSING_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_nonblock() { + run(PREVIEW1_PATH_OPEN_NONBLOCK_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_rename_dir_trailing_slashes() { + run(PREVIEW1_PATH_RENAME_DIR_TRAILING_SLASHES_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_rename() { + run(PREVIEW1_PATH_RENAME_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_symlink_trailing_slashes() { + run(PREVIEW1_PATH_SYMLINK_TRAILING_SLASHES_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_poll_oneoff_files() { + run(PREVIEW1_POLL_ONEOFF_FILES_COMPONENT, false) + .await + .unwrap() +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_poll_oneoff_stdio() { + run(PREVIEW1_POLL_ONEOFF_STDIO_COMPONENT, true) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_readlink() { + run(PREVIEW1_READLINK_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_remove_directory() { + run(PREVIEW1_REMOVE_DIRECTORY_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_remove_nonempty_directory() { + run(PREVIEW1_REMOVE_NONEMPTY_DIRECTORY_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_renumber() { + run(PREVIEW1_RENUMBER_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_sched_yield() { + run(PREVIEW1_SCHED_YIELD_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio() { + run(PREVIEW1_STDIO_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio_isatty() { + // If the test process is setup such that stdio is a terminal: + if test_programs_artifacts::stdio_is_terminal() { + // Inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_ISATTY_COMPONENT, true).await.unwrap() + } +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_stdio_not_isatty() { + // Don't inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_NOT_ISATTY_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_create() { + run(PREVIEW1_SYMLINK_CREATE_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_filestat() { + run(PREVIEW1_SYMLINK_FILESTAT_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_symlink_loop() { + run(PREVIEW1_SYMLINK_LOOP_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_unlink_file_trailing_slashes() { + run(PREVIEW1_UNLINK_FILE_TRAILING_SLASHES_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_preopen() { + run(PREVIEW1_PATH_OPEN_PREOPEN_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_unicode_output() { + run(PREVIEW1_UNICODE_OUTPUT_COMPONENT, true).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_file_write() { + run(PREVIEW1_FILE_WRITE_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview1_path_open_lots() { + run(PREVIEW1_PATH_OPEN_LOTS_COMPONENT, false).await.unwrap() +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_sleep() { + run(PREVIEW2_SLEEP_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_random() { + run(PREVIEW2_RANDOM_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_ip_name_lookup() { + run(PREVIEW2_IP_NAME_LOOKUP_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_sockopts() { + run(PREVIEW2_TCP_SOCKOPTS_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_sample_application() { + run(PREVIEW2_TCP_SAMPLE_APPLICATION_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_states() { + run(PREVIEW2_TCP_STATES_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_streams() { + run(PREVIEW2_TCP_STREAMS_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_bind() { + run(PREVIEW2_TCP_BIND_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_tcp_connect() { + run(PREVIEW2_TCP_CONNECT_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_udp_sockopts() { + run(PREVIEW2_UDP_SOCKOPTS_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_udp_sample_application() { + run(PREVIEW2_UDP_SAMPLE_APPLICATION_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_udp_states() { + run(PREVIEW2_UDP_STATES_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_udp_bind() { + run(PREVIEW2_UDP_BIND_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_udp_connect() { + run(PREVIEW2_UDP_CONNECT_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_stream_pollable_correct() { + run(PREVIEW2_STREAM_POLLABLE_CORRECT_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_stream_pollable_traps() { + let e = run(PREVIEW2_STREAM_POLLABLE_TRAPS_COMPONENT, false) + .await + .unwrap_err(); + assert_eq!( + format!("{}", e.source().expect("trap source")), + "resource has children" + ) +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_pollable_correct() { + run(PREVIEW2_POLLABLE_CORRECT_COMPONENT, false) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_pollable_traps() { + let e = run(PREVIEW2_POLLABLE_TRAPS_COMPONENT, false) + .await + .unwrap_err(); + assert_eq!( + format!("{}", e.source().expect("trap source")), + "empty poll list" + ) +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_adapter_badfd() { + run(PREVIEW2_ADAPTER_BADFD_COMPONENT, false).await.unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn preview2_file_read_write() { + run(PREVIEW2_FILE_READ_WRITE_COMPONENT, false) + .await + .unwrap() +} diff --git a/crates/wasi/tests/all/p2/mod.rs b/crates/wasi/tests/all/p2/mod.rs new file mode 100644 index 00000000..77f499e4 --- /dev/null +++ b/crates/wasi/tests/all/p2/mod.rs @@ -0,0 +1,3 @@ +mod api; +mod async_; +mod sync; diff --git a/crates/wasi/tests/all/p2/sync.rs b/crates/wasi/tests/all/p2/sync.rs new file mode 100644 index 00000000..2c88d09a --- /dev/null +++ b/crates/wasi/tests/all/p2/sync.rs @@ -0,0 +1,341 @@ +use crate::store::{Ctx, MyWasiCtx}; +use std::path::Path; +use test_programs_artifacts::*; +use wash_wasi::p2::add_to_linker_sync; +use wash_wasi::p2::bindings::sync::Command; +use wasmtime::Result; +use wasmtime::component::{Component, Linker}; + +fn run(path: &str, inherit_stdio: bool) -> Result<()> { + let path = Path::new(path); + let name = path.file_stem().unwrap().to_str().unwrap(); + let engine = test_programs_artifacts::engine(|_| {}); + let mut linker = Linker::new(&engine); + add_to_linker_sync(&mut linker)?; + + let component = Component::from_file(&engine, path)?; + + for blocking in [false, true] { + let (mut store, _td) = Ctx::new(&engine, name, |builder| { + if inherit_stdio { + builder.inherit_stdio(); + } + builder.allow_blocking_current_thread(blocking); + MyWasiCtx { + wasi: builder.build(), + table: Default::default(), + } + })?; + let command = Command::instantiate(&mut store, &component, &linker)?; + command + .wasi_cli_run() + .call_run(&mut store)? + .map_err(|()| anyhow::anyhow!("run returned a failure"))?; + } + Ok(()) +} + +foreach_preview1!(assert_test_exists); +foreach_preview2!(assert_test_exists); + +// Below here is mechanical: there should be one test for every binary in +// wasi-tests. +#[test_log::test] +fn preview1_big_random_buf() { + run(PREVIEW1_BIG_RANDOM_BUF_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_clock_time_get() { + run(PREVIEW1_CLOCK_TIME_GET_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_close_preopen() { + run(PREVIEW1_CLOSE_PREOPEN_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_dangling_fd() { + run(PREVIEW1_DANGLING_FD_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_dangling_symlink() { + run(PREVIEW1_DANGLING_SYMLINK_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_directory_seek() { + run(PREVIEW1_DIRECTORY_SEEK_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_dir_fd_op_failures() { + run(PREVIEW1_DIR_FD_OP_FAILURES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_fd_advise() { + run(PREVIEW1_FD_ADVISE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_fd_filestat_get() { + run(PREVIEW1_FD_FILESTAT_GET_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_fd_filestat_set() { + run(PREVIEW1_FD_FILESTAT_SET_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_fd_flags_set() { + run(PREVIEW1_FD_FLAGS_SET_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_fd_readdir() { + run(PREVIEW1_FD_READDIR_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_allocate() { + run(PREVIEW1_FILE_ALLOCATE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_pread_pwrite() { + run(PREVIEW1_FILE_PREAD_PWRITE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_read_write() { + run(PREVIEW1_FILE_READ_WRITE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_seek_tell() { + run(PREVIEW1_FILE_SEEK_TELL_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_truncation() { + run(PREVIEW1_FILE_TRUNCATION_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_file_unbuffered_write() { + run(PREVIEW1_FILE_UNBUFFERED_WRITE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_interesting_paths() { + run(PREVIEW1_INTERESTING_PATHS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_regular_file_isatty() { + run(PREVIEW1_REGULAR_FILE_ISATTY_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_nofollow_errors() { + run(PREVIEW1_NOFOLLOW_ERRORS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_overwrite_preopen() { + run(PREVIEW1_OVERWRITE_PREOPEN_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_exists() { + run(PREVIEW1_PATH_EXISTS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_filestat() { + run(PREVIEW1_PATH_FILESTAT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_link() { + run(PREVIEW1_PATH_LINK_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_create_existing() { + run(PREVIEW1_PATH_OPEN_CREATE_EXISTING_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_read_write() { + run(PREVIEW1_PATH_OPEN_READ_WRITE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_dirfd_not_dir() { + run(PREVIEW1_PATH_OPEN_DIRFD_NOT_DIR_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_missing() { + run(PREVIEW1_PATH_OPEN_MISSING_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_nonblock() { + run(PREVIEW1_PATH_OPEN_NONBLOCK_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_rename_dir_trailing_slashes() { + run(PREVIEW1_PATH_RENAME_DIR_TRAILING_SLASHES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_rename() { + run(PREVIEW1_PATH_RENAME_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_symlink_trailing_slashes() { + run(PREVIEW1_PATH_SYMLINK_TRAILING_SLASHES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_poll_oneoff_files() { + run(PREVIEW1_POLL_ONEOFF_FILES_COMPONENT, false).unwrap() +} + +#[test_log::test] +fn preview1_poll_oneoff_stdio() { + run(PREVIEW1_POLL_ONEOFF_STDIO_COMPONENT, true).unwrap() +} +#[test_log::test] +fn preview1_readlink() { + run(PREVIEW1_READLINK_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_remove_directory() { + run(PREVIEW1_REMOVE_DIRECTORY_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_remove_nonempty_directory() { + run(PREVIEW1_REMOVE_NONEMPTY_DIRECTORY_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_renumber() { + run(PREVIEW1_RENUMBER_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_sched_yield() { + run(PREVIEW1_SCHED_YIELD_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_stdio() { + run(PREVIEW1_STDIO_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_stdio_isatty() { + // If the test process is setup such that stdio is a terminal: + if test_programs_artifacts::stdio_is_terminal() { + // Inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_ISATTY_COMPONENT, true).unwrap() + } +} +#[test_log::test] +fn preview1_stdio_not_isatty() { + // Don't inherit stdio, test asserts each is not tty: + run(PREVIEW1_STDIO_NOT_ISATTY_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_symlink_create() { + run(PREVIEW1_SYMLINK_CREATE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_symlink_filestat() { + run(PREVIEW1_SYMLINK_FILESTAT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_symlink_loop() { + run(PREVIEW1_SYMLINK_LOOP_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_unlink_file_trailing_slashes() { + run(PREVIEW1_UNLINK_FILE_TRAILING_SLASHES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_preopen() { + run(PREVIEW1_PATH_OPEN_PREOPEN_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_unicode_output() { + run(PREVIEW1_UNICODE_OUTPUT_COMPONENT, true).unwrap() +} +#[test_log::test] +fn preview1_file_write() { + run(PREVIEW1_FILE_WRITE_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview1_path_open_lots() { + run(PREVIEW1_PATH_OPEN_LOTS_COMPONENT, false).unwrap() +} + +#[test_log::test] +fn preview2_sleep() { + run(PREVIEW2_SLEEP_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_random() { + run(PREVIEW2_RANDOM_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_ip_name_lookup() { + run(PREVIEW2_IP_NAME_LOOKUP_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_sockopts() { + run(PREVIEW2_TCP_SOCKOPTS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_sample_application() { + run(PREVIEW2_TCP_SAMPLE_APPLICATION_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_states() { + run(PREVIEW2_TCP_STATES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_streams() { + run(PREVIEW2_TCP_STREAMS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_bind() { + run(PREVIEW2_TCP_BIND_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_tcp_connect() { + run(PREVIEW2_TCP_CONNECT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_udp_sockopts() { + run(PREVIEW2_UDP_SOCKOPTS_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_udp_sample_application() { + run(PREVIEW2_UDP_SAMPLE_APPLICATION_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_udp_states() { + run(PREVIEW2_UDP_STATES_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_udp_bind() { + run(PREVIEW2_UDP_BIND_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_udp_connect() { + run(PREVIEW2_UDP_CONNECT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_stream_pollable_correct() { + run(PREVIEW2_STREAM_POLLABLE_CORRECT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_stream_pollable_traps() { + let e = run(PREVIEW2_STREAM_POLLABLE_TRAPS_COMPONENT, false).unwrap_err(); + assert_eq!( + format!("{}", e.source().expect("trap source")), + "resource has children" + ) +} +#[test_log::test] +fn preview2_pollable_correct() { + run(PREVIEW2_POLLABLE_CORRECT_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_pollable_traps() { + let e = run(PREVIEW2_POLLABLE_TRAPS_COMPONENT, false).unwrap_err(); + assert_eq!( + format!("{}", e.source().expect("trap source")), + "empty poll list" + ) +} +#[test_log::test] +fn preview2_adapter_badfd() { + run(PREVIEW2_ADAPTER_BADFD_COMPONENT, false).unwrap() +} +#[test_log::test] +fn preview2_file_read_write() { + run(PREVIEW2_FILE_READ_WRITE_COMPONENT, false).unwrap() +} diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs new file mode 100644 index 00000000..4e969352 --- /dev/null +++ b/crates/wasi/tests/all/p3/mod.rs @@ -0,0 +1,153 @@ +use crate::store::{Ctx, MyWasiCtx}; +use anyhow::{Context as _, anyhow}; +use std::path::Path; +use test_programs_artifacts::*; +use wash_wasi::p3::bindings::Command; +use wasmtime::Result; +use wasmtime::component::{Component, Linker}; + +async fn run(path: &str) -> Result<()> { + run_allow_blocking_current_thread(path, false).await +} + +async fn run_allow_blocking_current_thread( + path: &str, + allow_blocking_current_thread: bool, +) -> Result<()> { + let path = Path::new(path); + let name = path.file_stem().unwrap().to_str().unwrap(); + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + config.wasm_component_model_async(true); + }); + let mut linker = Linker::new(&engine); + // TODO: Remove once test components are not built for `wasm32-wasip1` + wash_wasi::p2::add_to_linker_async(&mut linker).context("failed to link `wasi:cli@0.2.x`")?; + wash_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; + + let (mut store, _td) = Ctx::new(&engine, name, |builder| MyWasiCtx { + wasi: builder + .allow_blocking_current_thread(allow_blocking_current_thread) + .build(), + table: Default::default(), + })?; + let component = Component::from_file(&engine, path)?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let command = + Command::new(&mut store, &instance).context("failed to instantiate `wasi:cli/command`")?; + instance + .run_concurrent(&mut store, async move |store| { + command.wasi_cli_run().call_run(store).await + }) + .await + .context("failed to call `wasi:cli/run#run`")? + .context("guest trapped")? + .map_err(|()| anyhow!("`wasi:cli/run#run` failed")) +} + +foreach_p3!(assert_test_exists); + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_cli() -> anyhow::Result<()> { + run(P3_CLI_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_clocks_sleep() -> anyhow::Result<()> { + run(P3_CLOCKS_SLEEP_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_filesystem_file_read_write() -> anyhow::Result<()> { + run(P3_FILESYSTEM_FILE_READ_WRITE_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_filesystem_file_read_write_blocking() -> anyhow::Result<()> { + run_allow_blocking_current_thread(P3_FILESYSTEM_FILE_READ_WRITE_COMPONENT, true).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_random_imports() -> anyhow::Result<()> { + run(P3_RANDOM_IMPORTS_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_ip_name_lookup() -> anyhow::Result<()> { + run(P3_SOCKETS_IP_NAME_LOOKUP_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_bind() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_BIND_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_connect() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_CONNECT_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_sample_application() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_SAMPLE_APPLICATION_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_sockopts() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_SOCKOPTS_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_states() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_STATES_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_tcp_streams() -> anyhow::Result<()> { + run(P3_SOCKETS_TCP_STREAMS_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_udp_bind() -> anyhow::Result<()> { + run(P3_SOCKETS_UDP_BIND_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_udp_connect() -> anyhow::Result<()> { + run(P3_SOCKETS_UDP_CONNECT_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_udp_sample_application() -> anyhow::Result<()> { + run(P3_SOCKETS_UDP_SAMPLE_APPLICATION_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_udp_sockopts() -> anyhow::Result<()> { + run(P3_SOCKETS_UDP_SOCKOPTS_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_sockets_udp_states() -> anyhow::Result<()> { + run(P3_SOCKETS_UDP_STATES_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_readdir() -> anyhow::Result<()> { + run(P3_READDIR_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_readdir_blocking() -> anyhow::Result<()> { + run_allow_blocking_current_thread(P3_READDIR_COMPONENT, true).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_file_write() -> anyhow::Result<()> { + run(P3_FILE_WRITE_COMPONENT).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_file_write_blocking() -> anyhow::Result<()> { + run_allow_blocking_current_thread(P3_FILE_WRITE_COMPONENT, true).await +} diff --git a/crates/wasi/tests/all/store.rs b/crates/wasi/tests/all/store.rs new file mode 100644 index 00000000..186e8a66 --- /dev/null +++ b/crates/wasi/tests/all/store.rs @@ -0,0 +1,86 @@ +use anyhow::Result; +use tempfile::TempDir; +use wash_wasi::{ + DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView, p2::pipe::MemoryOutputPipe, +}; +use wasmtime::component::ResourceTable; +use wasmtime::{Engine, Store}; + +pub struct Ctx { + stdout: MemoryOutputPipe, + stderr: MemoryOutputPipe, + pub wasi: T, +} + +fn prepare_workspace(exe_name: &str) -> Result { + let prefix = format!("wasi_components_{exe_name}_"); + let tempdir = tempfile::Builder::new().prefix(&prefix).tempdir()?; + Ok(tempdir) +} + +impl Ctx { + pub fn new( + engine: &Engine, + name: &str, + configure: impl FnOnce(&mut WasiCtxBuilder) -> T, + ) -> Result<(Store>, TempDir)> { + const MAX_OUTPUT_SIZE: usize = 10 << 20; + let stdout = MemoryOutputPipe::new(MAX_OUTPUT_SIZE); + let stderr = MemoryOutputPipe::new(MAX_OUTPUT_SIZE); + let workspace = prepare_workspace(name)?; + + // Create our wasi context. + let mut builder = WasiCtxBuilder::new(); + builder.stdout(stdout.clone()).stderr(stderr.clone()); + + builder + .args(&[name, "."]) + .inherit_network() + .allow_ip_name_lookup(true); + println!("preopen: {workspace:?}"); + builder.preopened_dir(workspace.path(), ".", DirPerms::all(), FilePerms::all())?; + for (var, val) in test_programs_artifacts::wasi_tests_environment() { + builder.env(var, val); + } + + let supports_ipv6 = std::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, 0)).is_ok(); + if !supports_ipv6 { + builder.env("DISABLE_IPV6", "1"); + } + + let ctx = Ctx { + wasi: configure(&mut builder), + stderr, + stdout, + }; + + Ok((Store::new(&engine, ctx), workspace)) + } +} + +impl Drop for Ctx { + fn drop(&mut self) { + let stdout = self.stdout.contents(); + if !stdout.is_empty() { + println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); + } + let stderr = self.stderr.contents(); + if !stderr.is_empty() { + println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); + } + } +} + +pub struct MyWasiCtx { + pub wasi: WasiCtx, + pub table: ResourceTable, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi.wasi, + table: &mut self.wasi.table, + } + } +} diff --git a/crates/wasi/tests/process_stdin.rs b/crates/wasi/tests/process_stdin.rs new file mode 100644 index 00000000..d88dc952 --- /dev/null +++ b/crates/wasi/tests/process_stdin.rs @@ -0,0 +1,162 @@ +use std::io::{BufRead, Write}; +use std::process::Command; +use wash_wasi::cli::StdinStream; +use wash_wasi::p2::Pollable; + +const VAR_NAME: &str = "__CHILD_PROCESS"; + +fn main() { + if cfg!(miri) { + return; + } + // Skip this tests if it looks like we're in a cross-compiled situation and + // we're emulating this test for a different platform. In that scenario + // emulators (like QEMU) tend to not report signals the same way and such. + if wasmtime_test_util::cargo_test_runner().is_some() { + return; + } + + match std::env::var(VAR_NAME) { + Ok(_) => child_process(), + Err(_) => parent_process(), + } + + fn child_process() { + let mut result_write = std::io::stderr(); + let mut child_running = true; + while child_running { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + 'task: loop { + println!("child: creating stdin"); + let mut stdin = wash_wasi::cli::stdin().p2_stream(); + + println!("child: checking that stdin is not ready"); + assert!( + tokio::time::timeout( + std::time::Duration::from_millis(100), + stdin.ready() + ) + .await + .is_err(), + "stdin available too soon" + ); + + writeln!(&mut result_write, "start").unwrap(); + + println!("child: started"); + + let mut buffer = String::new(); + loop { + println!("child: waiting for stdin to be ready"); + stdin.ready().await; + + println!("child: reading input"); + // We can't effectively test for the case where stdin was closed, so panic if it is... + let bytes = stdin.read(1024).unwrap(); + + println!("child got: {bytes:?}"); + + buffer.push_str(std::str::from_utf8(bytes.as_ref()).unwrap()); + if let Some((line, rest)) = buffer.split_once('\n') { + if line == "all done" { + writeln!(&mut result_write, "done").unwrap(); + println!("child: exiting..."); + child_running = false; + break 'task; + } else if line == "restart_runtime" { + writeln!(&mut result_write, "restarting").unwrap(); + println!("child: restarting runtime..."); + break 'task; + } else if line == "restart_task" { + writeln!(&mut result_write, "restarting").unwrap(); + println!("child: restarting task..."); + continue 'task; + } else { + writeln!(&mut result_write, "{line}").unwrap(); + } + + buffer = rest.to_owned(); + } + } + } + }); + println!("child: runtime exited"); + } + println!("child: exiting"); + } +} + +fn parent_process() { + let me = std::env::current_exe().unwrap(); + let mut cmd = Command::new(me); + cmd.env(VAR_NAME, "1"); + cmd.stdin(std::process::Stdio::piped()); + + if std::env::args().any(|arg| arg == "--nocapture") { + cmd.stdout(std::process::Stdio::inherit()); + } else { + cmd.stdout(std::process::Stdio::null()); + } + + cmd.stderr(std::process::Stdio::piped()); + let mut child = cmd.spawn().unwrap(); + + let mut stdin_write = child.stdin.take().unwrap(); + let mut result_read = std::io::BufReader::new(child.stderr.take().unwrap()); + + let mut line = String::new(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "start\n"); + + for i in 0..5 { + let message = format!("some bytes {i}\n"); + stdin_write.write_all(message.as_bytes()).unwrap(); + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, message); + } + + writeln!(&mut stdin_write, "restart_task").unwrap(); + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "restarting\n"); + line.clear(); + + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "start\n"); + + for i in 0..10 { + let message = format!("more bytes {i}\n"); + stdin_write.write_all(message.as_bytes()).unwrap(); + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, message); + } + + writeln!(&mut stdin_write, "restart_runtime").unwrap(); + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "restarting\n"); + line.clear(); + + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "start\n"); + + for i in 0..17 { + let message = format!("even more bytes {i}\n"); + stdin_write.write_all(message.as_bytes()).unwrap(); + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, message); + } + + writeln!(&mut stdin_write, "all done").unwrap(); + + line.clear(); + result_read.read_line(&mut line).unwrap(); + assert_eq!(line, "done\n"); +} diff --git a/crates/wasi/witx/p0/typenames.witx b/crates/wasi/witx/p0/typenames.witx new file mode 100644 index 00000000..c3213743 --- /dev/null +++ b/crates/wasi/witx/p0/typenames.witx @@ -0,0 +1,746 @@ +;; Type names used by low-level WASI interfaces. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/tree/main/docs/witx.md) +;; for an explanation of what that means. + +(typename $size u32) + +;;; Non-negative file size or length of a region within a file. +(typename $filesize u64) + +;;; Timestamp in nanoseconds. +(typename $timestamp u64) + +;;; Identifiers for clocks. +(typename $clockid + (enum (@witx tag u32) + ;;; The clock measuring real time. Time value zero corresponds with + ;;; 1970-01-01T00:00:00Z. + $realtime + ;;; The store-wide monotonic clock, which is defined as a clock measuring + ;;; real time, whose value cannot be adjusted and which cannot have negative + ;;; clock jumps. The epoch of this clock is undefined. The absolute time + ;;; value of this clock therefore has no meaning. + $monotonic + ;;; The CPU-time clock associated with the current process. + $process_cputime_id + ;;; The CPU-time clock associated with the current thread. + $thread_cputime_id + ) +) + +;;; Error codes returned by functions. +;;; Not all of these error codes are returned by the functions provided by this +;;; API; some are used in higher-level library layers, and others are provided +;;; merely for alignment with POSIX. +(typename $errno + (enum (@witx tag u16) + ;;; No error occurred. System call completed successfully. + $success + ;;; Argument list too long. + $2big + ;;; Permission denied. + $acces + ;;; Address in use. + $addrinuse + ;;; Address not available. + $addrnotavail + ;;; Address family not supported. + $afnosupport + ;;; Resource unavailable, or operation would block. + $again + ;;; Connection already in progress. + $already + ;;; Bad file descriptor. + $badf + ;;; Bad message. + $badmsg + ;;; Device or resource busy. + $busy + ;;; Operation canceled. + $canceled + ;;; No child processes. + $child + ;;; Connection aborted. + $connaborted + ;;; Connection refused. + $connrefused + ;;; Connection reset. + $connreset + ;;; Resource deadlock would occur. + $deadlk + ;;; Destination address required. + $destaddrreq + ;;; Mathematics argument out of domain of function. + $dom + ;;; Reserved. + $dquot + ;;; File exists. + $exist + ;;; Bad address. + $fault + ;;; File too large. + $fbig + ;;; Host is unreachable. + $hostunreach + ;;; Identifier removed. + $idrm + ;;; Illegal byte sequence. + $ilseq + ;;; Operation in progress. + $inprogress + ;;; Interrupted function. + $intr + ;;; Invalid argument. + $inval + ;;; I/O error. + $io + ;;; Socket is connected. + $isconn + ;;; Is a directory. + $isdir + ;;; Too many levels of symbolic links. + $loop + ;;; File descriptor value too large. + $mfile + ;;; Too many links. + $mlink + ;;; Message too large. + $msgsize + ;;; Reserved. + $multihop + ;;; Filename too long. + $nametoolong + ;;; Network is down. + $netdown + ;;; Connection aborted by network. + $netreset + ;;; Network unreachable. + $netunreach + ;;; Too many files open in system. + $nfile + ;;; No buffer space available. + $nobufs + ;;; No such device. + $nodev + ;;; No such file or directory. + $noent + ;;; Executable file format error. + $noexec + ;;; No locks available. + $nolck + ;;; Reserved. + $nolink + ;;; Not enough space. + $nomem + ;;; No message of the desired type. + $nomsg + ;;; Protocol not available. + $noprotoopt + ;;; No space left on device. + $nospc + ;;; Function not supported. + $nosys + ;;; The socket is not connected. + $notconn + ;;; Not a directory or a symbolic link to a directory. + $notdir + ;;; Directory not empty. + $notempty + ;;; State not recoverable. + $notrecoverable + ;;; Not a socket. + $notsock + ;;; Not supported, or operation not supported on socket. + $notsup + ;;; Inappropriate I/O control operation. + $notty + ;;; No such device or address. + $nxio + ;;; Value too large to be stored in data type. + $overflow + ;;; Previous owner died. + $ownerdead + ;;; Operation not permitted. + $perm + ;;; Broken pipe. + $pipe + ;;; Protocol error. + $proto + ;;; Protocol not supported. + $protonosupport + ;;; Protocol wrong type for socket. + $prototype + ;;; Result too large. + $range + ;;; Read-only file system. + $rofs + ;;; Invalid seek. + $spipe + ;;; No such process. + $srch + ;;; Reserved. + $stale + ;;; Connection timed out. + $timedout + ;;; Text file busy. + $txtbsy + ;;; Cross-device link. + $xdev + ;;; Extension: Capabilities insufficient. + $notcapable + ) +) + +;;; File descriptor rights, determining which actions may be performed. +(typename $rights + (flags (@witx repr u64) + ;;; The right to invoke `fd_datasync`. + ;; + ;;; If `rights::path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::dsync`. + $fd_datasync + ;;; The right to invoke `fd_read` and `sock_recv`. + ;; + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + $fd_read + ;;; The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + $fd_seek + ;;; The right to invoke `fd_fdstat_set_flags`. + $fd_fdstat_set_flags + ;;; The right to invoke `fd_sync`. + ;; + ;;; If `rights::path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::rsync` and `fdflags::dsync`. + $fd_sync + ;;; The right to invoke `fd_seek` in such a way that the file offset + ;;; remains unaltered (i.e., `whence::cur` with offset zero), or to + ;;; invoke `fd_tell`. + $fd_tell + ;;; The right to invoke `fd_write` and `sock_send`. + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + $fd_write + ;;; The right to invoke `fd_advise`. + $fd_advise + ;;; The right to invoke `fd_allocate`. + $fd_allocate + ;;; The right to invoke `path_create_directory`. + $path_create_directory + ;;; If `rights::path_open` is set, the right to invoke `path_open` with `oflags::creat`. + $path_create_file + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; source directory. + $path_link_source + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; target directory. + $path_link_target + ;;; The right to invoke `path_open`. + $path_open + ;;; The right to invoke `fd_readdir`. + $fd_readdir + ;;; The right to invoke `path_readlink`. + $path_readlink + ;;; The right to invoke `path_rename` with the file descriptor as the source directory. + $path_rename_source + ;;; The right to invoke `path_rename` with the file descriptor as the target directory. + $path_rename_target + ;;; The right to invoke `path_filestat_get`. + $path_filestat_get + ;;; The right to change a file's size (there is no `path_filestat_set_size`). + ;;; If `rights::path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + $path_filestat_set_size + ;;; The right to invoke `path_filestat_set_times`. + $path_filestat_set_times + ;;; The right to invoke `fd_filestat_get`. + $fd_filestat_get + ;;; The right to invoke `fd_filestat_set_size`. + $fd_filestat_set_size + ;;; The right to invoke `fd_filestat_set_times`. + $fd_filestat_set_times + ;;; The right to invoke `path_symlink`. + $path_symlink + ;;; The right to invoke `path_remove_directory`. + $path_remove_directory + ;;; The right to invoke `path_unlink_file`. + $path_unlink_file + ;;; If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + ;;; If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + $poll_fd_readwrite + ;;; The right to invoke `sock_shutdown`. + $sock_shutdown + ) +) + +;;; A file descriptor handle. +(typename $fd (handle)) + +;;; A region of memory for scatter/gather reads. +(typename $iovec + (record + ;;; The address of the buffer to be filled. + (field $buf (@witx pointer u8)) + ;;; The length of the buffer to be filled. + (field $buf_len $size) + ) +) + +;;; A region of memory for scatter/gather writes. +(typename $ciovec + (record + ;;; The address of the buffer to be written. + (field $buf (@witx const_pointer u8)) + ;;; The length of the buffer to be written. + (field $buf_len $size) + ) +) + +(typename $iovec_array (list $iovec)) +(typename $ciovec_array (list $ciovec)) + +;;; Relative offset within a file. +(typename $filedelta s64) + +;;; The position relative to which to set the offset of the file descriptor. +(typename $whence + (enum (@witx tag u8) + ;;; Seek relative to current position. + $cur + ;;; Seek relative to end-of-file. + $end + ;;; Seek relative to start-of-file. + $set + ) +) + +;;; A reference to the offset of a directory entry. +(typename $dircookie u64) + +;;; The type for the `dirent::d_namlen` field of `dirent` struct. +(typename $dirnamlen u32) + +;;; File serial number that is unique within its file system. +(typename $inode u64) + +;;; The type of a file descriptor or file. +(typename $filetype + (enum (@witx tag u8) + ;;; The type of the file descriptor or file is unknown or is different from any of the other types specified. + $unknown + ;;; The file descriptor or file refers to a block device inode. + $block_device + ;;; The file descriptor or file refers to a character device inode. + $character_device + ;;; The file descriptor or file refers to a directory inode. + $directory + ;;; The file descriptor or file refers to a regular file inode. + $regular_file + ;;; The file descriptor or file refers to a datagram socket. + $socket_dgram + ;;; The file descriptor or file refers to a byte-stream socket. + $socket_stream + ;;; The file refers to a symbolic link inode. + $symbolic_link + ) +) + +;;; A directory entry. +(typename $dirent + (record + ;;; The offset of the next directory entry stored in this directory. + (field $d_next $dircookie) + ;;; The serial number of the file referred to by this directory entry. + (field $d_ino $inode) + ;;; The length of the name of the directory entry. + (field $d_namlen $dirnamlen) + ;;; The type of the file referred to by this directory entry. + (field $d_type $filetype) + ) +) + +;;; File or memory access pattern advisory information. +(typename $advice + (enum (@witx tag u8) + ;;; The application has no advice to give on its behavior with respect to the specified data. + $normal + ;;; The application expects to access the specified data sequentially from lower offsets to higher offsets. + $sequential + ;;; The application expects to access the specified data in a random order. + $random + ;;; The application expects to access the specified data in the near future. + $willneed + ;;; The application expects that it will not access the specified data in the near future. + $dontneed + ;;; The application expects to access the specified data once and then not reuse it thereafter. + $noreuse + ) +) + +;;; File descriptor flags. +(typename $fdflags + (flags (@witx repr u16) + ;;; Append mode: Data written to the file is always appended to the file's end. + $append + ;;; Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + $dsync + ;;; Non-blocking mode. + $nonblock + ;;; Synchronized read I/O operations. + $rsync + ;;; Write according to synchronized I/O file integrity completion. In + ;;; addition to synchronizing the data stored in the file, the implementation + ;;; may also synchronously update the file's metadata. + $sync + ) +) + +;;; File descriptor attributes. +(typename $fdstat + (record + ;;; File type. + (field $fs_filetype $filetype) + ;;; File descriptor flags. + (field $fs_flags $fdflags) + ;;; Rights that apply to this file descriptor. + (field $fs_rights_base $rights) + ;;; Maximum set of rights that may be installed on new file descriptors that + ;;; are created through this file descriptor, e.g., through `path_open`. + (field $fs_rights_inheriting $rights) + ) +) + +;;; Identifier for a device containing a file system. Can be used in combination +;;; with `inode` to uniquely identify a file or directory in the filesystem. +(typename $device u64) + +;;; Which file time attributes to adjust. +(typename $fstflags + (flags (@witx repr u16) + ;;; Adjust the last data access timestamp to the value stored in `filestat::atim`. + $atim + ;;; Adjust the last data access timestamp to the time of clock `clockid::realtime`. + $atim_now + ;;; Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + $mtim + ;;; Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + $mtim_now + ) +) + +;;; Flags determining the method of how paths are resolved. +(typename $lookupflags + (flags (@witx repr u32) + ;;; As long as the resolved path corresponds to a symbolic link, it is expanded. + $symlink_follow + ) +) + +;;; Open flags used by `path_open`. +(typename $oflags + (flags (@witx repr u16) + ;;; Create file if it does not exist. + $creat + ;;; Fail if not a directory. + $directory + ;;; Fail if file already exists. + $excl + ;;; Truncate file to size 0. + $trunc + ) +) + +;;; Number of hard links to an inode. +(typename $linkcount u32) + +;;; File attributes. +(typename $filestat + (record + ;;; Device ID of device containing the file. + (field $dev $device) + ;;; File serial number. + (field $ino $inode) + ;;; File type. + (field $filetype $filetype) + ;;; Number of hard links to the file. + (field $nlink $linkcount) + ;;; For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + (field $size $filesize) + ;;; Last data access timestamp. + (field $atim $timestamp) + ;;; Last data modification timestamp. + (field $mtim $timestamp) + ;;; Last file status change timestamp. + (field $ctim $timestamp) + ) +) + +;;; User-provided value that may be attached to objects that is retained when +;;; extracted from the implementation. +(typename $userdata u64) + +;;; Type of a subscription to an event or its occurrence. +(typename $eventtype + (enum (@witx tag u8) + ;;; The time value of clock `subscription_clock::id` has + ;;; reached timestamp `subscription_clock::timeout`. + $clock + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has data + ;;; available for reading. This event always triggers for regular files. + $fd_read + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has capacity + ;;; available for writing. This event always triggers for regular files. + $fd_write + ) +) + +;;; The state of the file descriptor subscribed to with +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $eventrwflags + (flags (@witx repr u16) + ;;; The peer of this socket has closed or disconnected. + $fd_readwrite_hangup + ) +) + +;;; The contents of an `event` for the `eventtype::fd_read` and +;;; `eventtype::fd_write` variants +(typename $event_fd_readwrite + (record + ;;; The number of bytes available for reading or writing. + (field $nbytes $filesize) + ;;; The state of the file descriptor. + (field $flags $eventrwflags) + ) +) + +;;; An event that occurred. +(typename $event + (record + ;;; User-provided value that got attached to `subscription::userdata`. + (field $userdata $userdata) + ;;; If non-zero, an error that occurred while processing the subscription request. + (field $error $errno) + ;;; The type of event that occurred + (field $type $eventtype) + ;;; The contents of the event, if it is an `eventtype::fd_read` or + ;;; `eventtype::fd_write`. `eventtype::clock` events ignore this field. + (field $fd_readwrite $event_fd_readwrite) + ) +) + +;;; Flags determining how to interpret the timestamp provided in +;;; `subscription_clock::timeout`. +(typename $subclockflags + (flags (@witx repr u16) + ;;; If set, treat the timestamp provided in + ;;; `subscription_clock::timeout` as an absolute timestamp of clock + ;;; `subscription_clock::id`. If clear, treat the timestamp + ;;; provided in `subscription_clock::timeout` relative to the + ;;; current time value of clock `subscription_clock::id`. + $subscription_clock_abstime + ) +) + +;;; The contents of a `subscription` when type is `eventtype::clock`. +(typename $subscription_clock + (record + ;;; The user-defined unique identifier of the clock. + (field $identifier $userdata) + ;;; The clock against which to compare the timestamp. + (field $id $clockid) + ;;; The absolute or relative timestamp. + (field $timeout $timestamp) + ;;; The amount of time that the implementation may wait additionally + ;;; to coalesce with other events. + (field $precision $timestamp) + ;;; Flags specifying whether the timeout is absolute or relative + (field $flags $subclockflags) + ) +) + +;;; The contents of a `subscription` when the variant is +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $subscription_fd_readwrite + (record + ;;; The file descriptor on which to wait for it to become ready for reading or writing. + (field $file_descriptor $fd) + ) +) + +;;; The contents of a `subscription`. +(typename $subscription_u + (union (@witx tag $eventtype) + $subscription_clock + $subscription_fd_readwrite + $subscription_fd_readwrite + ) +) + +;;; Subscription to an event. +(typename $subscription + (record + ;;; User-provided value that is attached to the subscription in the + ;;; implementation and returned through `event::userdata`. + (field $userdata $userdata) + ;;; The type of the event to which to subscribe. + (field $u $subscription_u) + ) +) + +;;; Exit code generated by a process when exiting. +(typename $exitcode u32) + +;;; Signal condition. +(typename $signal + (enum (@witx tag u8) + ;;; No signal. Note that POSIX has special semantics for `kill(pid, 0)`, + ;;; so this value is reserved. + $none + ;;; Hangup. + ;;; Action: Terminates the process. + $hup + ;;; Terminate interrupt signal. + ;;; Action: Terminates the process. + $int + ;;; Terminal quit signal. + ;;; Action: Terminates the process. + $quit + ;;; Illegal instruction. + ;;; Action: Terminates the process. + $ill + ;;; Trace/breakpoint trap. + ;;; Action: Terminates the process. + $trap + ;;; Process abort signal. + ;;; Action: Terminates the process. + $abrt + ;;; Access to an undefined portion of a memory object. + ;;; Action: Terminates the process. + $bus + ;;; Erroneous arithmetic operation. + ;;; Action: Terminates the process. + $fpe + ;;; Kill. + ;;; Action: Terminates the process. + $kill + ;;; User-defined signal 1. + ;;; Action: Terminates the process. + $usr1 + ;;; Invalid memory reference. + ;;; Action: Terminates the process. + $segv + ;;; User-defined signal 2. + ;;; Action: Terminates the process. + $usr2 + ;;; Write on a pipe with no one to read it. + ;;; Action: Ignored. + $pipe + ;;; Alarm clock. + ;;; Action: Terminates the process. + $alrm + ;;; Termination signal. + ;;; Action: Terminates the process. + $term + ;;; Child process terminated, stopped, or continued. + ;;; Action: Ignored. + $chld + ;;; Continue executing, if stopped. + ;;; Action: Continues executing, if stopped. + $cont + ;;; Stop executing. + ;;; Action: Stops executing. + $stop + ;;; Terminal stop signal. + ;;; Action: Stops executing. + $tstp + ;;; Background process attempting read. + ;;; Action: Stops executing. + $ttin + ;;; Background process attempting write. + ;;; Action: Stops executing. + $ttou + ;;; High bandwidth data is available at a socket. + ;;; Action: Ignored. + $urg + ;;; CPU time limit exceeded. + ;;; Action: Terminates the process. + $xcpu + ;;; File size limit exceeded. + ;;; Action: Terminates the process. + $xfsz + ;;; Virtual timer expired. + ;;; Action: Terminates the process. + $vtalrm + ;;; Profiling timer expired. + ;;; Action: Terminates the process. + $prof + ;;; Window changed. + ;;; Action: Ignored. + $winch + ;;; I/O possible. + ;;; Action: Terminates the process. + $poll + ;;; Power failure. + ;;; Action: Terminates the process. + $pwr + ;;; Bad system call. + ;;; Action: Terminates the process. + $sys + ) +) + +;;; Flags provided to `sock_recv`. +(typename $riflags + (flags (@witx repr u16) + ;;; Returns the message without removing it from the socket's receive queue. + $recv_peek + ;;; On byte-stream sockets, block until the full amount of data can be returned. + $recv_waitall + ) +) + +;;; Flags returned by `sock_recv`. +(typename $roflags + (flags (@witx repr u16) + ;;; Returned by `sock_recv`: Message data has been truncated. + $recv_data_truncated + ) +) + +;;; Flags provided to `sock_send`. As there are currently no flags +;;; defined, it must be set to zero. +(typename $siflags u16) + +;;; Which channels on a socket to shut down. +(typename $sdflags + (flags (@witx repr u8) + ;;; Disables further receive operations. + $rd + ;;; Disables further send operations. + $wr + ) +) + +;;; Identifiers for preopened capabilities. +(typename $preopentype + (enum (@witx tag u8) + ;;; A pre-opened directory. + $dir + ) +) + +;;; The contents of a $prestat when type is `preopentype::dir`. +(typename $prestat_dir + (record + ;;; The length of the directory name for use with `fd_prestat_dir_name`. + (field $pr_name_len $size) + ) +) + +;;; Information about a pre-opened capability. +(typename $prestat + (union (@witx tag $preopentype) + $prestat_dir + ) +) diff --git a/crates/wasi/witx/p0/wasi_unstable.witx b/crates/wasi/witx/p0/wasi_unstable.witx new file mode 100644 index 00000000..ee01abcf --- /dev/null +++ b/crates/wasi/witx/p0/wasi_unstable.witx @@ -0,0 +1,513 @@ +;; WASI Preview. This is an evolution of the API that WASI initially +;; launched with. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/blob/main/legacy/tools/witx-docs.md) +;; for an explanation of what that means. + +(use "typenames.witx") + +;;; This API predated the convention of naming modules with a `wasi_unstable_` +;;; prefix and a version number. It is preserved here for compatibility, but +;;; we shouldn't follow this pattern in new APIs. +(module $wasi_unstable + ;;; Linear memory to be accessed by WASI functions that need it. + (import "memory" (memory)) + + ;;; Read command-line argument data. + ;;; The size of the array should match that returned by `args_sizes_get`. + ;;; Each argument is expected to be `\0` terminated. + (@interface func (export "args_get") + (param $argv (@witx pointer (@witx pointer u8))) + (param $argv_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return command-line argument data sizes. + (@interface func (export "args_sizes_get") + ;;; Returns the number of arguments and the size of the argument string + ;;; data, or an error. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Read environment variable data. + ;;; The sizes of the buffers should match that returned by `environ_sizes_get`. + ;;; Key/value pairs are expected to be joined with `=`s, and terminated with `\0`s. + (@interface func (export "environ_get") + (param $environ (@witx pointer (@witx pointer u8))) + (param $environ_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return environment variable data sizes. + (@interface func (export "environ_sizes_get") + ;;; Returns the number of environment variable arguments and the size of the + ;;; environment variable data. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Return the resolution of a clock. + ;;; Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, return + ;;; `errno::inval`. + ;;; Note: This is similar to `clock_getres` in POSIX. + (@interface func (export "clock_res_get") + ;;; The clock for which to return the resolution. + (param $id $clockid) + ;;; The resolution of the clock, or an error if one happened. + (result $error (expected $timestamp (error $errno))) + ) + ;;; Return the time value of a clock. + ;;; Note: This is similar to `clock_gettime` in POSIX. + (@interface func (export "clock_time_get") + ;;; The clock for which to return the time. + (param $id $clockid) + ;;; The maximum lag (exclusive) that the returned time value may have, compared to its actual value. + (param $precision $timestamp) + ;;; The time value of the clock. + (result $error (expected $timestamp (error $errno))) + ) + + ;;; Provide file advisory information on a file descriptor. + ;;; Note: This is similar to `posix_fadvise` in POSIX. + (@interface func (export "fd_advise") + (param $fd $fd) + ;;; The offset within the file to which the advisory applies. + (param $offset $filesize) + ;;; The length of the region to which the advisory applies. + (param $len $filesize) + ;;; The advice. + (param $advice $advice) + (result $error (expected (error $errno))) + ) + + ;;; Force the allocation of space in a file. + ;;; Note: This is similar to `posix_fallocate` in POSIX. + (@interface func (export "fd_allocate") + (param $fd $fd) + ;;; The offset at which to start the allocation. + (param $offset $filesize) + ;;; The length of the area that is allocated. + (param $len $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Close a file descriptor. + ;;; Note: This is similar to `close` in POSIX. + (@interface func (export "fd_close") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Synchronize the data of a file to disk. + ;;; Note: This is similar to `fdatasync` in POSIX. + (@interface func (export "fd_datasync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Get the attributes of a file descriptor. + ;;; Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. + (@interface func (export "fd_fdstat_get") + (param $fd $fd) + ;;; The buffer where the file descriptor's attributes are stored. + (result $error (expected $fdstat (error $errno))) + ) + + ;;; Adjust the flags associated with a file descriptor. + ;;; Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + (@interface func (export "fd_fdstat_set_flags") + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the rights associated with a file descriptor. + ;;; This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights + (@interface func (export "fd_fdstat_set_rights") + (param $fd $fd) + ;;; The desired rights of the file descriptor. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of an open file. + (@interface func (export "fd_filestat_get") + (param $fd $fd) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + ;;; Note: This is similar to `ftruncate` in POSIX. + (@interface func (export "fd_filestat_set_size") + (param $fd $fd) + ;;; The desired file size. + (param $size $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the timestamps of an open file or directory. + ;;; Note: This is similar to `futimens` in POSIX. + (@interface func (export "fd_filestat_set_times") + (param $fd $fd) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Read from a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `preadv` in POSIX. + (@interface func (export "fd_pread") + (param $fd $fd) + ;;; List of scatter/gather vectors in which to store data. + (param $iovs $iovec_array) + ;;; The offset within the file at which to read. + (param $offset $filesize) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_get") + (param $fd $fd) + ;;; The buffer where the description is stored. + (result $error (expected $prestat (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_dir_name") + (param $fd $fd) + ;;; A buffer into which to write the preopened directory name. + (param $path (@witx pointer u8)) + (param $path_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Write to a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `pwritev` in POSIX. + (@interface func (export "fd_pwrite") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + ;;; The offset within the file at which to write. + (param $offset $filesize) + ;;; The number of bytes written. + (result $error (expected $size (error $errno))) + ) + + ;;; Read from a file descriptor. + ;;; Note: This is similar to `readv` in POSIX. + (@interface func (export "fd_read") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $iovs $iovec_array) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Read directory entries from a directory. + ;;; When successful, the contents of the output buffer consist of a sequence of + ;;; directory entries. Each directory entry consists of a `dirent` object, + ;;; followed by `dirent::d_namlen` bytes holding the name of the directory + ;;; entry. + ;; + ;;; This function fills the output buffer as much as possible, potentially + ;;; truncating the last directory entry. This allows the caller to grow its + ;;; read buffer size in case it's too small to fit a single large directory + ;;; entry, or skip the oversized directory entry. + (@interface func (export "fd_readdir") + (param $fd $fd) + ;;; The buffer where directory entries are stored + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The location within the directory to start reading + (param $cookie $dircookie) + ;;; The number of bytes stored in the read buffer. If less than the size of the read buffer, the end of the directory has been reached. + (result $error (expected $size (error $errno))) + ) + + ;;; Atomically replace a file descriptor by renumbering another file descriptor. + ;; + ;;; Due to the strong focus on thread safety, this environment does not provide + ;;; a mechanism to duplicate or renumber a file descriptor to an arbitrary + ;;; number, like `dup2()`. This would be prone to race conditions, as an actual + ;;; file descriptor with the same number could be allocated by a different + ;;; thread at the same time. + ;; + ;;; This function provides a way to atomically renumber file descriptors, which + ;;; would disappear if `dup2()` were to be removed entirely. + (@interface func (export "fd_renumber") + (param $fd $fd) + ;;; The file descriptor to overwrite. + (param $to $fd) + (result $error (expected (error $errno))) + ) + + ;;; Move the offset of a file descriptor. + ;;; Note: This is similar to `lseek` in POSIX. + (@interface func (export "fd_seek") + (param $fd $fd) + ;;; The number of bytes to move. + (param $offset $filedelta) + ;;; The base from which the offset is relative. + (param $whence $whence) + ;;; The new offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Synchronize the data and metadata of a file to disk. + ;;; Note: This is similar to `fsync` in POSIX. + (@interface func (export "fd_sync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Return the current offset of a file descriptor. + ;;; Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + (@interface func (export "fd_tell") + (param $fd $fd) + ;;; The current offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Write to a file descriptor. + ;;; Note: This is similar to `writev` in POSIX. + (@interface func (export "fd_write") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + (result $error (expected $size (error $errno))) + ) + + ;;; Create a directory. + ;;; Note: This is similar to `mkdirat` in POSIX. + (@interface func (export "path_create_directory") + (param $fd $fd) + ;;; The path at which to create the directory. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of a file or directory. + ;;; Note: This is similar to `stat` in POSIX. + (@interface func (export "path_filestat_get") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to inspect. + (param $path string) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the timestamps of a file or directory. + ;;; Note: This is similar to `utimensat` in POSIX. + (@interface func (export "path_filestat_set_times") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to operate on. + (param $path string) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Create a hard link. + ;;; Note: This is similar to `linkat` in POSIX. + (@interface func (export "path_link") + (param $old_fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $old_flags $lookupflags) + ;;; The source path from which to link. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path at which to create the hard link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Open a file or directory. + ;; + ;;; The returned file descriptor is not guaranteed to be the lowest-numbered + ;;; file descriptor not currently open; it is randomized to prevent + ;;; applications from depending on making assumptions about indexes, since this + ;;; is error-prone in multi-threaded contexts. The returned file descriptor is + ;;; guaranteed to be less than 2**31. + ;; + ;;; Note: This is similar to `openat` in POSIX. + (@interface func (export "path_open") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $dirflags $lookupflags) + ;;; The relative path of the file or directory to open, relative to the + ;;; `path_open::fd` directory. + (param $path string) + ;;; The method by which to open the file. + (param $oflags $oflags) + ;;; The initial rights of the newly created file descriptor. The + ;;; implementation is allowed to return a file descriptor with fewer rights + ;;; than specified, if and only if those rights do not apply to the type of + ;;; file being opened. + ;; + ;;; The *base* rights are rights that will apply to operations using the file + ;;; descriptor itself, while the *inheriting* rights are rights that apply to + ;;; file descriptors derived from it. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (param $fdflags $fdflags) + ;;; The file descriptor of the file that has been opened. + (result $error (expected $fd (error $errno))) + ) + + ;;; Read the contents of a symbolic link. + ;;; Note: This is similar to `readlinkat` in POSIX. + (@interface func (export "path_readlink") + (param $fd $fd) + ;;; The path of the symbolic link from which to read. + (param $path string) + ;;; The buffer to which to write the contents of the symbolic link. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The number of bytes placed in the buffer. + (result $error (expected $size (error $errno))) + ) + + ;;; Remove a directory. + ;;; Return `errno::notempty` if the directory is not empty. + ;;; Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + (@interface func (export "path_remove_directory") + (param $fd $fd) + ;;; The path to a directory to remove. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Rename a file or directory. + ;;; Note: This is similar to `renameat` in POSIX. + (@interface func (export "path_rename") + (param $fd $fd) + ;;; The source path of the file or directory to rename. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path to which to rename the file or directory. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Create a symbolic link. + ;;; Note: This is similar to `symlinkat` in POSIX. + (@interface func (export "path_symlink") + ;;; The contents of the symbolic link. + (param $old_path string) + (param $fd $fd) + ;;; The destination path at which to create the symbolic link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + + ;;; Unlink a file. + ;;; Return `errno::isdir` if the path refers to a directory. + ;;; Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + (@interface func (export "path_unlink_file") + (param $fd $fd) + ;;; The path to a file to unlink. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Concurrently poll for the occurrence of a set of events. + (@interface func (export "poll_oneoff") + ;;; The events to which to subscribe. + (param $in (@witx const_pointer $subscription)) + ;;; The events that have occurred. + (param $out (@witx pointer $event)) + ;;; Both the number of subscriptions and events. + (param $nsubscriptions $size) + ;;; The number of events stored. + (result $error (expected $size (error $errno))) + ) + + ;;; Terminate the process normally. An exit code of 0 indicates successful + ;;; termination of the program. The meanings of other values is dependent on + ;;; the environment. + (@interface func (export "proc_exit") + ;;; The exit code returned by the process. + (param $rval $exitcode) + (@witx noreturn) + ) + + ;;; Send a signal to the process of the calling thread. + ;;; Note: This is similar to `raise` in POSIX. + (@interface func (export "proc_raise") + ;;; The signal condition to trigger. + (param $sig $signal) + (result $error (expected (error $errno))) + ) + + ;;; Temporarily yield execution of the calling thread. + ;;; Note: This is similar to `sched_yield` in POSIX. + (@interface func (export "sched_yield") + (result $error (expected (error $errno))) + ) + + ;;; Write high-quality random data into a buffer. + ;;; This function blocks when the implementation is unable to immediately + ;;; provide sufficient high-quality random data. + ;;; This function may execute slowly, so when large mounts of random data are + ;;; required, it's advisable to use this function to seed a pseudo-random + ;;; number generator, rather than to provide the random data directly. + (@interface func (export "random_get") + ;;; The buffer to fill with random data. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Receive a message from a socket. + ;;; Note: This is similar to `recv` in POSIX, though it also supports reading + ;;; the data into multiple buffers in the manner of `readv`. + (@interface func (export "sock_recv") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $ri_data $iovec_array) + ;;; Message flags. + (param $ri_flags $riflags) + ;;; Number of bytes stored in ri_data and message flags. + (result $error (expected (tuple $size $roflags) (error $errno))) + ) + + ;;; Send a message on a socket. + ;;; Note: This is similar to `send` in POSIX, though it also supports writing + ;;; the data from multiple buffers in the manner of `writev`. + (@interface func (export "sock_send") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to retrieve data + (param $si_data $ciovec_array) + ;;; Message flags. + (param $si_flags $siflags) + ;;; Number of bytes transmitted. + (result $error (expected $size (error $errno))) + ) + + ;;; Shut down socket send and receive channels. + ;;; Note: This is similar to `shutdown` in POSIX. + (@interface func (export "sock_shutdown") + (param $fd $fd) + ;;; Which channels on the socket to shut down. + (param $how $sdflags) + (result $error (expected (error $errno))) + ) +) diff --git a/crates/wasi/witx/p1/typenames.witx b/crates/wasi/witx/p1/typenames.witx new file mode 100644 index 00000000..82ea2764 --- /dev/null +++ b/crates/wasi/witx/p1/typenames.witx @@ -0,0 +1,750 @@ +;; Type names used by low-level WASI interfaces. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/blob/main/legacy/tools/witx-docs.md) +;; for an explanation of what that means. + +(typename $size u32) + +;;; Non-negative file size or length of a region within a file. +(typename $filesize u64) + +;;; Timestamp in nanoseconds. +(typename $timestamp u64) + +;;; Identifiers for clocks. +(typename $clockid + (enum (@witx tag u32) + ;;; The clock measuring real time. Time value zero corresponds with + ;;; 1970-01-01T00:00:00Z. + $realtime + ;;; The store-wide monotonic clock, which is defined as a clock measuring + ;;; real time, whose value cannot be adjusted and which cannot have negative + ;;; clock jumps. The epoch of this clock is undefined. The absolute time + ;;; value of this clock therefore has no meaning. + $monotonic + ;;; The CPU-time clock associated with the current process. + $process_cputime_id + ;;; The CPU-time clock associated with the current thread. + $thread_cputime_id + ) +) + +;;; Error codes returned by functions. +;;; Not all of these error codes are returned by the functions provided by this +;;; API; some are used in higher-level library layers, and others are provided +;;; merely for alignment with POSIX. +(typename $errno + (enum (@witx tag u16) + ;;; No error occurred. System call completed successfully. + $success + ;;; Argument list too long. + $2big + ;;; Permission denied. + $acces + ;;; Address in use. + $addrinuse + ;;; Address not available. + $addrnotavail + ;;; Address family not supported. + $afnosupport + ;;; Resource unavailable, or operation would block. + $again + ;;; Connection already in progress. + $already + ;;; Bad file descriptor. + $badf + ;;; Bad message. + $badmsg + ;;; Device or resource busy. + $busy + ;;; Operation canceled. + $canceled + ;;; No child processes. + $child + ;;; Connection aborted. + $connaborted + ;;; Connection refused. + $connrefused + ;;; Connection reset. + $connreset + ;;; Resource deadlock would occur. + $deadlk + ;;; Destination address required. + $destaddrreq + ;;; Mathematics argument out of domain of function. + $dom + ;;; Reserved. + $dquot + ;;; File exists. + $exist + ;;; Bad address. + $fault + ;;; File too large. + $fbig + ;;; Host is unreachable. + $hostunreach + ;;; Identifier removed. + $idrm + ;;; Illegal byte sequence. + $ilseq + ;;; Operation in progress. + $inprogress + ;;; Interrupted function. + $intr + ;;; Invalid argument. + $inval + ;;; I/O error. + $io + ;;; Socket is connected. + $isconn + ;;; Is a directory. + $isdir + ;;; Too many levels of symbolic links. + $loop + ;;; File descriptor value too large. + $mfile + ;;; Too many links. + $mlink + ;;; Message too large. + $msgsize + ;;; Reserved. + $multihop + ;;; Filename too long. + $nametoolong + ;;; Network is down. + $netdown + ;;; Connection aborted by network. + $netreset + ;;; Network unreachable. + $netunreach + ;;; Too many files open in system. + $nfile + ;;; No buffer space available. + $nobufs + ;;; No such device. + $nodev + ;;; No such file or directory. + $noent + ;;; Executable file format error. + $noexec + ;;; No locks available. + $nolck + ;;; Reserved. + $nolink + ;;; Not enough space. + $nomem + ;;; No message of the desired type. + $nomsg + ;;; Protocol not available. + $noprotoopt + ;;; No space left on device. + $nospc + ;;; Function not supported. + $nosys + ;;; The socket is not connected. + $notconn + ;;; Not a directory or a symbolic link to a directory. + $notdir + ;;; Directory not empty. + $notempty + ;;; State not recoverable. + $notrecoverable + ;;; Not a socket. + $notsock + ;;; Not supported, or operation not supported on socket. + $notsup + ;;; Inappropriate I/O control operation. + $notty + ;;; No such device or address. + $nxio + ;;; Value too large to be stored in data type. + $overflow + ;;; Previous owner died. + $ownerdead + ;;; Operation not permitted. + $perm + ;;; Broken pipe. + $pipe + ;;; Protocol error. + $proto + ;;; Protocol not supported. + $protonosupport + ;;; Protocol wrong type for socket. + $prototype + ;;; Result too large. + $range + ;;; Read-only file system. + $rofs + ;;; Invalid seek. + $spipe + ;;; No such process. + $srch + ;;; Reserved. + $stale + ;;; Connection timed out. + $timedout + ;;; Text file busy. + $txtbsy + ;;; Cross-device link. + $xdev + ;;; Extension: Capabilities insufficient. + $notcapable + ) +) + +;;; File descriptor rights, determining which actions may be performed. +(typename $rights + (flags (@witx repr u64) + ;;; The right to invoke `fd_datasync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::dsync`. + $fd_datasync + ;;; The right to invoke `fd_read` and `sock_recv`. + ;; + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + $fd_read + ;;; The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + $fd_seek + ;;; The right to invoke `fd_fdstat_set_flags`. + $fd_fdstat_set_flags + ;;; The right to invoke `fd_sync`. + ;; + ;;; If `path_open` is set, includes the right to invoke + ;;; `path_open` with `fdflags::rsync` and `fdflags::dsync`. + $fd_sync + ;;; The right to invoke `fd_seek` in such a way that the file offset + ;;; remains unaltered (i.e., `whence::cur` with offset zero), or to + ;;; invoke `fd_tell`. + $fd_tell + ;;; The right to invoke `fd_write` and `sock_send`. + ;;; If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + $fd_write + ;;; The right to invoke `fd_advise`. + $fd_advise + ;;; The right to invoke `fd_allocate`. + $fd_allocate + ;;; The right to invoke `path_create_directory`. + $path_create_directory + ;;; If `path_open` is set, the right to invoke `path_open` with `oflags::creat`. + $path_create_file + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; source directory. + $path_link_source + ;;; The right to invoke `path_link` with the file descriptor as the + ;;; target directory. + $path_link_target + ;;; The right to invoke `path_open`. + $path_open + ;;; The right to invoke `fd_readdir`. + $fd_readdir + ;;; The right to invoke `path_readlink`. + $path_readlink + ;;; The right to invoke `path_rename` with the file descriptor as the source directory. + $path_rename_source + ;;; The right to invoke `path_rename` with the file descriptor as the target directory. + $path_rename_target + ;;; The right to invoke `path_filestat_get`. + $path_filestat_get + ;;; The right to change a file's size (there is no `path_filestat_set_size`). + ;;; If `path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + $path_filestat_set_size + ;;; The right to invoke `path_filestat_set_times`. + $path_filestat_set_times + ;;; The right to invoke `fd_filestat_get`. + $fd_filestat_get + ;;; The right to invoke `fd_filestat_set_size`. + $fd_filestat_set_size + ;;; The right to invoke `fd_filestat_set_times`. + $fd_filestat_set_times + ;;; The right to invoke `path_symlink`. + $path_symlink + ;;; The right to invoke `path_remove_directory`. + $path_remove_directory + ;;; The right to invoke `path_unlink_file`. + $path_unlink_file + ;;; If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + ;;; If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + $poll_fd_readwrite + ;;; The right to invoke `sock_shutdown`. + $sock_shutdown + ;;; The right to invoke `sock_accept`. + $sock_accept + ) +) + +;;; A file descriptor handle. +(typename $fd (handle)) + +;;; A region of memory for scatter/gather reads. +(typename $iovec + (record + ;;; The address of the buffer to be filled. + (field $buf (@witx pointer u8)) + ;;; The length of the buffer to be filled. + (field $buf_len $size) + ) +) + +;;; A region of memory for scatter/gather writes. +(typename $ciovec + (record + ;;; The address of the buffer to be written. + (field $buf (@witx const_pointer u8)) + ;;; The length of the buffer to be written. + (field $buf_len $size) + ) +) + +(typename $iovec_array (list $iovec)) +(typename $ciovec_array (list $ciovec)) + +;;; Relative offset within a file. +(typename $filedelta s64) + +;;; The position relative to which to set the offset of the file descriptor. +(typename $whence + (enum (@witx tag u8) + ;;; Seek relative to start-of-file. + $set + ;;; Seek relative to current position. + $cur + ;;; Seek relative to end-of-file. + $end + ) +) + +;;; A reference to the offset of a directory entry. +;;; +;;; The value 0 signifies the start of the directory. +(typename $dircookie u64) + +;;; The type for the `dirent::d_namlen` field of `dirent` struct. +(typename $dirnamlen u32) + +;;; File serial number that is unique within its file system. +(typename $inode u64) + +;;; The type of a file descriptor or file. +(typename $filetype + (enum (@witx tag u8) + ;;; The type of the file descriptor or file is unknown or is different from any of the other types specified. + $unknown + ;;; The file descriptor or file refers to a block device inode. + $block_device + ;;; The file descriptor or file refers to a character device inode. + $character_device + ;;; The file descriptor or file refers to a directory inode. + $directory + ;;; The file descriptor or file refers to a regular file inode. + $regular_file + ;;; The file descriptor or file refers to a datagram socket. + $socket_dgram + ;;; The file descriptor or file refers to a byte-stream socket. + $socket_stream + ;;; The file refers to a symbolic link inode. + $symbolic_link + ) +) + +;;; A directory entry. +(typename $dirent + (record + ;;; The offset of the next directory entry stored in this directory. + (field $d_next $dircookie) + ;;; The serial number of the file referred to by this directory entry. + (field $d_ino $inode) + ;;; The length of the name of the directory entry. + (field $d_namlen $dirnamlen) + ;;; The type of the file referred to by this directory entry. + (field $d_type $filetype) + ) +) + +;;; File or memory access pattern advisory information. +(typename $advice + (enum (@witx tag u8) + ;;; The application has no advice to give on its behavior with respect to the specified data. + $normal + ;;; The application expects to access the specified data sequentially from lower offsets to higher offsets. + $sequential + ;;; The application expects to access the specified data in a random order. + $random + ;;; The application expects to access the specified data in the near future. + $willneed + ;;; The application expects that it will not access the specified data in the near future. + $dontneed + ;;; The application expects to access the specified data once and then not reuse it thereafter. + $noreuse + ) +) + +;;; File descriptor flags. +(typename $fdflags + (flags (@witx repr u16) + ;;; Append mode: Data written to the file is always appended to the file's end. + $append + ;;; Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + $dsync + ;;; Non-blocking mode. + $nonblock + ;;; Synchronized read I/O operations. + $rsync + ;;; Write according to synchronized I/O file integrity completion. In + ;;; addition to synchronizing the data stored in the file, the implementation + ;;; may also synchronously update the file's metadata. + $sync + ) +) + +;;; File descriptor attributes. +(typename $fdstat + (record + ;;; File type. + (field $fs_filetype $filetype) + ;;; File descriptor flags. + (field $fs_flags $fdflags) + ;;; Rights that apply to this file descriptor. + (field $fs_rights_base $rights) + ;;; Maximum set of rights that may be installed on new file descriptors that + ;;; are created through this file descriptor, e.g., through `path_open`. + (field $fs_rights_inheriting $rights) + ) +) + +;;; Identifier for a device containing a file system. Can be used in combination +;;; with `inode` to uniquely identify a file or directory in the filesystem. +(typename $device u64) + +;;; Which file time attributes to adjust. +(typename $fstflags + (flags (@witx repr u16) + ;;; Adjust the last data access timestamp to the value stored in `filestat::atim`. + $atim + ;;; Adjust the last data access timestamp to the time of clock `clockid::realtime`. + $atim_now + ;;; Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + $mtim + ;;; Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + $mtim_now + ) +) + +;;; Flags determining the method of how paths are resolved. +(typename $lookupflags + (flags (@witx repr u32) + ;;; As long as the resolved path corresponds to a symbolic link, it is expanded. + $symlink_follow + ) +) + +;;; Open flags used by `path_open`. +(typename $oflags + (flags (@witx repr u16) + ;;; Create file if it does not exist. + $creat + ;;; Fail if not a directory. + $directory + ;;; Fail if file already exists. + $excl + ;;; Truncate file to size 0. + $trunc + ) +) + +;;; Number of hard links to an inode. +(typename $linkcount u64) + +;;; File attributes. +(typename $filestat + (record + ;;; Device ID of device containing the file. + (field $dev $device) + ;;; File serial number. + (field $ino $inode) + ;;; File type. + (field $filetype $filetype) + ;;; Number of hard links to the file. + (field $nlink $linkcount) + ;;; For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + (field $size $filesize) + ;;; Last data access timestamp. + (field $atim $timestamp) + ;;; Last data modification timestamp. + (field $mtim $timestamp) + ;;; Last file status change timestamp. + (field $ctim $timestamp) + ) +) + +;;; User-provided value that may be attached to objects that is retained when +;;; extracted from the implementation. +(typename $userdata u64) + +;;; Type of a subscription to an event or its occurrence. +(typename $eventtype + (enum (@witx tag u8) + ;;; The time value of clock `subscription_clock::id` has + ;;; reached timestamp `subscription_clock::timeout`. + $clock + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has data + ;;; available for reading. This event always triggers for regular files. + $fd_read + ;;; File descriptor `subscription_fd_readwrite::file_descriptor` has capacity + ;;; available for writing. This event always triggers for regular files. + $fd_write + ) +) + +;;; The state of the file descriptor subscribed to with +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $eventrwflags + (flags (@witx repr u16) + ;;; The peer of this socket has closed or disconnected. + $fd_readwrite_hangup + ) +) + +;;; The contents of an `event` when type is `eventtype::fd_read` or +;;; `eventtype::fd_write`. +(typename $event_fd_readwrite + (record + ;;; The number of bytes available for reading or writing. + (field $nbytes $filesize) + ;;; The state of the file descriptor. + (field $flags $eventrwflags) + ) +) + +;;; An event that occurred. +(typename $event + (record + ;;; User-provided value that got attached to `subscription::userdata`. + (field $userdata $userdata) + ;;; If non-zero, an error that occurred while processing the subscription request. + (field $error $errno) + ;;; The type of event that occurred + (field $type $eventtype) + ;;; The contents of the event, if it is an `eventtype::fd_read` or + ;;; `eventtype::fd_write`. `eventtype::clock` events ignore this field. + (field $fd_readwrite $event_fd_readwrite) + ) +) + +;;; Flags determining how to interpret the timestamp provided in +;;; `subscription_clock::timeout`. +(typename $subclockflags + (flags (@witx repr u16) + ;;; If set, treat the timestamp provided in + ;;; `subscription_clock::timeout` as an absolute timestamp of clock + ;;; `subscription_clock::id`. If clear, treat the timestamp + ;;; provided in `subscription_clock::timeout` relative to the + ;;; current time value of clock `subscription_clock::id`. + $subscription_clock_abstime + ) +) + +;;; The contents of a `subscription` when type is `eventtype::clock`. +(typename $subscription_clock + (record + ;;; The clock against which to compare the timestamp. + (field $id $clockid) + ;;; The absolute or relative timestamp. + (field $timeout $timestamp) + ;;; The amount of time that the implementation may wait additionally + ;;; to coalesce with other events. + (field $precision $timestamp) + ;;; Flags specifying whether the timeout is absolute or relative + (field $flags $subclockflags) + ) +) + +;;; The contents of a `subscription` when type is type is +;;; `eventtype::fd_read` or `eventtype::fd_write`. +(typename $subscription_fd_readwrite + (record + ;;; The file descriptor on which to wait for it to become ready for reading or writing. + (field $file_descriptor $fd) + ) +) + +;;; The contents of a `subscription`. +(typename $subscription_u + (union + (@witx tag $eventtype) + $subscription_clock + $subscription_fd_readwrite + $subscription_fd_readwrite + ) +) + +;;; Subscription to an event. +(typename $subscription + (record + ;;; User-provided value that is attached to the subscription in the + ;;; implementation and returned through `event::userdata`. + (field $userdata $userdata) + ;;; The type of the event to which to subscribe, and its contents + (field $u $subscription_u) + ) +) + +;;; Exit code generated by a process when exiting. +(typename $exitcode u32) + +;;; Signal condition. +(typename $signal + (enum (@witx tag u8) + ;;; No signal. Note that POSIX has special semantics for `kill(pid, 0)`, + ;;; so this value is reserved. + $none + ;;; Hangup. + ;;; Action: Terminates the process. + $hup + ;;; Terminate interrupt signal. + ;;; Action: Terminates the process. + $int + ;;; Terminal quit signal. + ;;; Action: Terminates the process. + $quit + ;;; Illegal instruction. + ;;; Action: Terminates the process. + $ill + ;;; Trace/breakpoint trap. + ;;; Action: Terminates the process. + $trap + ;;; Process abort signal. + ;;; Action: Terminates the process. + $abrt + ;;; Access to an undefined portion of a memory object. + ;;; Action: Terminates the process. + $bus + ;;; Erroneous arithmetic operation. + ;;; Action: Terminates the process. + $fpe + ;;; Kill. + ;;; Action: Terminates the process. + $kill + ;;; User-defined signal 1. + ;;; Action: Terminates the process. + $usr1 + ;;; Invalid memory reference. + ;;; Action: Terminates the process. + $segv + ;;; User-defined signal 2. + ;;; Action: Terminates the process. + $usr2 + ;;; Write on a pipe with no one to read it. + ;;; Action: Ignored. + $pipe + ;;; Alarm clock. + ;;; Action: Terminates the process. + $alrm + ;;; Termination signal. + ;;; Action: Terminates the process. + $term + ;;; Child process terminated, stopped, or continued. + ;;; Action: Ignored. + $chld + ;;; Continue executing, if stopped. + ;;; Action: Continues executing, if stopped. + $cont + ;;; Stop executing. + ;;; Action: Stops executing. + $stop + ;;; Terminal stop signal. + ;;; Action: Stops executing. + $tstp + ;;; Background process attempting read. + ;;; Action: Stops executing. + $ttin + ;;; Background process attempting write. + ;;; Action: Stops executing. + $ttou + ;;; High bandwidth data is available at a socket. + ;;; Action: Ignored. + $urg + ;;; CPU time limit exceeded. + ;;; Action: Terminates the process. + $xcpu + ;;; File size limit exceeded. + ;;; Action: Terminates the process. + $xfsz + ;;; Virtual timer expired. + ;;; Action: Terminates the process. + $vtalrm + ;;; Profiling timer expired. + ;;; Action: Terminates the process. + $prof + ;;; Window changed. + ;;; Action: Ignored. + $winch + ;;; I/O possible. + ;;; Action: Terminates the process. + $poll + ;;; Power failure. + ;;; Action: Terminates the process. + $pwr + ;;; Bad system call. + ;;; Action: Terminates the process. + $sys + ) +) + +;;; Flags provided to `sock_recv`. +(typename $riflags + (flags (@witx repr u16) + ;;; Returns the message without removing it from the socket's receive queue. + $recv_peek + ;;; On byte-stream sockets, block until the full amount of data can be returned. + $recv_waitall + ) +) + +;;; Flags returned by `sock_recv`. +(typename $roflags + (flags (@witx repr u16) + ;;; Returned by `sock_recv`: Message data has been truncated. + $recv_data_truncated + ) +) + +;;; Flags provided to `sock_send`. As there are currently no flags +;;; defined, it must be set to zero. +(typename $siflags u16) + +;;; Which channels on a socket to shut down. +(typename $sdflags + (flags (@witx repr u8) + ;;; Disables further receive operations. + $rd + ;;; Disables further send operations. + $wr + ) +) + +;;; Identifiers for preopened capabilities. +(typename $preopentype + (enum (@witx tag u8) + ;;; A pre-opened directory. + $dir + ) +) + +;;; The contents of a $prestat when type is `preopentype::dir`. +(typename $prestat_dir + (record + ;;; The length of the directory name for use with `fd_prestat_dir_name`. + (field $pr_name_len $size) + ) +) + +;;; Information about a pre-opened capability. +(typename $prestat + (union (@witx tag $preopentype) + $prestat_dir + ) +) + diff --git a/crates/wasi/witx/p1/wasi_snapshot_preview1.witx b/crates/wasi/witx/p1/wasi_snapshot_preview1.witx new file mode 100644 index 00000000..f9e2aa2c --- /dev/null +++ b/crates/wasi/witx/p1/wasi_snapshot_preview1.witx @@ -0,0 +1,521 @@ +;; WASI Preview. This is an evolution of the API that WASI initially +;; launched with. +;; +;; Some content here is derived from [CloudABI](https://github.com/NuxiNL/cloudabi). +;; +;; This is a `witx` file. See [here](https://github.com/WebAssembly/WASI/blob/main/legacy/tools/witx-docs.md) +;; for an explanation of what that means. + +(use "typenames.witx") + +(module $wasi_snapshot_preview1 + ;;; Linear memory to be accessed by WASI functions that need it. + (import "memory" (memory)) + + ;;; Read command-line argument data. + ;;; The size of the array should match that returned by `args_sizes_get`. + ;;; Each argument is expected to be `\0` terminated. + (@interface func (export "args_get") + (param $argv (@witx pointer (@witx pointer u8))) + (param $argv_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return command-line argument data sizes. + (@interface func (export "args_sizes_get") + ;;; Returns the number of arguments and the size of the argument string + ;;; data, or an error. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Read environment variable data. + ;;; The sizes of the buffers should match that returned by `environ_sizes_get`. + ;;; Key/value pairs are expected to be joined with `=`s, and terminated with `\0`s. + (@interface func (export "environ_get") + (param $environ (@witx pointer (@witx pointer u8))) + (param $environ_buf (@witx pointer u8)) + (result $error (expected (error $errno))) + ) + ;;; Return environment variable data sizes. + (@interface func (export "environ_sizes_get") + ;;; Returns the number of environment variable arguments and the size of the + ;;; environment variable data. + (result $error (expected (tuple $size $size) (error $errno))) + ) + + ;;; Return the resolution of a clock. + ;;; Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, + ;;; return `errno::inval`. + ;;; Note: This is similar to `clock_getres` in POSIX. + (@interface func (export "clock_res_get") + ;;; The clock for which to return the resolution. + (param $id $clockid) + ;;; The resolution of the clock, or an error if one happened. + (result $error (expected $timestamp (error $errno))) + ) + ;;; Return the time value of a clock. + ;;; Note: This is similar to `clock_gettime` in POSIX. + (@interface func (export "clock_time_get") + ;;; The clock for which to return the time. + (param $id $clockid) + ;;; The maximum lag (exclusive) that the returned time value may have, compared to its actual value. + (param $precision $timestamp) + ;;; The time value of the clock. + (result $error (expected $timestamp (error $errno))) + ) + + ;;; Provide file advisory information on a file descriptor. + ;;; Note: This is similar to `posix_fadvise` in POSIX. + (@interface func (export "fd_advise") + (param $fd $fd) + ;;; The offset within the file to which the advisory applies. + (param $offset $filesize) + ;;; The length of the region to which the advisory applies. + (param $len $filesize) + ;;; The advice. + (param $advice $advice) + (result $error (expected (error $errno))) + ) + + ;;; Force the allocation of space in a file. + ;;; Note: This is similar to `posix_fallocate` in POSIX. + (@interface func (export "fd_allocate") + (param $fd $fd) + ;;; The offset at which to start the allocation. + (param $offset $filesize) + ;;; The length of the area that is allocated. + (param $len $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Close a file descriptor. + ;;; Note: This is similar to `close` in POSIX. + (@interface func (export "fd_close") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Synchronize the data of a file to disk. + ;;; Note: This is similar to `fdatasync` in POSIX. + (@interface func (export "fd_datasync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Get the attributes of a file descriptor. + ;;; Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. + (@interface func (export "fd_fdstat_get") + (param $fd $fd) + ;;; The buffer where the file descriptor's attributes are stored. + (result $error (expected $fdstat (error $errno))) + ) + + ;;; Adjust the flags associated with a file descriptor. + ;;; Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + (@interface func (export "fd_fdstat_set_flags") + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the rights associated with a file descriptor. + ;;; This can only be used to remove rights, and returns `errno::notcapable` if called in a way that would attempt to add rights + (@interface func (export "fd_fdstat_set_rights") + (param $fd $fd) + ;;; The desired rights of the file descriptor. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of an open file. + (@interface func (export "fd_filestat_get") + (param $fd $fd) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + ;;; Note: This is similar to `ftruncate` in POSIX. + (@interface func (export "fd_filestat_set_size") + (param $fd $fd) + ;;; The desired file size. + (param $size $filesize) + (result $error (expected (error $errno))) + ) + + ;;; Adjust the timestamps of an open file or directory. + ;;; Note: This is similar to `futimens` in POSIX. + (@interface func (export "fd_filestat_set_times") + (param $fd $fd) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Read from a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `preadv` in POSIX. + (@interface func (export "fd_pread") + (param $fd $fd) + ;;; List of scatter/gather vectors in which to store data. + (param $iovs $iovec_array) + ;;; The offset within the file at which to read. + (param $offset $filesize) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_get") + (param $fd $fd) + ;;; The buffer where the description is stored. + (result $error (expected $prestat (error $errno))) + ) + + ;;; Return a description of the given preopened file descriptor. + (@interface func (export "fd_prestat_dir_name") + (param $fd $fd) + ;;; A buffer into which to write the preopened directory name. + (param $path (@witx pointer u8)) + (param $path_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Write to a file descriptor, without using and updating the file descriptor's offset. + ;;; Note: This is similar to `pwritev` in POSIX. + (@interface func (export "fd_pwrite") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + ;;; The offset within the file at which to write. + (param $offset $filesize) + ;;; The number of bytes written. + (result $error (expected $size (error $errno))) + ) + + ;;; Read from a file descriptor. + ;;; Note: This is similar to `readv` in POSIX. + (@interface func (export "fd_read") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $iovs $iovec_array) + ;;; The number of bytes read. + (result $error (expected $size (error $errno))) + ) + + ;;; Read directory entries from a directory. + ;;; When successful, the contents of the output buffer consist of a sequence of + ;;; directory entries. Each directory entry consists of a `dirent` object, + ;;; followed by `dirent::d_namlen` bytes holding the name of the directory + ;;; entry. + ;; + ;;; This function fills the output buffer as much as possible, potentially + ;;; truncating the last directory entry. This allows the caller to grow its + ;;; read buffer size in case it's too small to fit a single large directory + ;;; entry, or skip the oversized directory entry. + (@interface func (export "fd_readdir") + (param $fd $fd) + ;;; The buffer where directory entries are stored + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The location within the directory to start reading + (param $cookie $dircookie) + ;;; The number of bytes stored in the read buffer. If less than the size of the read buffer, the end of the directory has been reached. + (result $error (expected $size (error $errno))) + ) + + ;;; Atomically replace a file descriptor by renumbering another file descriptor. + ;; + ;;; Due to the strong focus on thread safety, this environment does not provide + ;;; a mechanism to duplicate or renumber a file descriptor to an arbitrary + ;;; number, like `dup2()`. This would be prone to race conditions, as an actual + ;;; file descriptor with the same number could be allocated by a different + ;;; thread at the same time. + ;; + ;;; This function provides a way to atomically renumber file descriptors, which + ;;; would disappear if `dup2()` were to be removed entirely. + (@interface func (export "fd_renumber") + (param $fd $fd) + ;;; The file descriptor to overwrite. + (param $to $fd) + (result $error (expected (error $errno))) + ) + + ;;; Move the offset of a file descriptor. + ;;; Note: This is similar to `lseek` in POSIX. + (@interface func (export "fd_seek") + (param $fd $fd) + ;;; The number of bytes to move. + (param $offset $filedelta) + ;;; The base from which the offset is relative. + (param $whence $whence) + ;;; The new offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Synchronize the data and metadata of a file to disk. + ;;; Note: This is similar to `fsync` in POSIX. + (@interface func (export "fd_sync") + (param $fd $fd) + (result $error (expected (error $errno))) + ) + + ;;; Return the current offset of a file descriptor. + ;;; Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + (@interface func (export "fd_tell") + (param $fd $fd) + ;;; The current offset of the file descriptor, relative to the start of the file. + (result $error (expected $filesize (error $errno))) + ) + + ;;; Write to a file descriptor. + ;;; Note: This is similar to `writev` in POSIX. + (@interface func (export "fd_write") + (param $fd $fd) + ;;; List of scatter/gather vectors from which to retrieve data. + (param $iovs $ciovec_array) + (result $error (expected $size (error $errno))) + ) + + ;;; Create a directory. + ;;; Note: This is similar to `mkdirat` in POSIX. + (@interface func (export "path_create_directory") + (param $fd $fd) + ;;; The path at which to create the directory. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Return the attributes of a file or directory. + ;;; Note: This is similar to `stat` in POSIX. + (@interface func (export "path_filestat_get") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to inspect. + (param $path string) + ;;; The buffer where the file's attributes are stored. + (result $error (expected $filestat (error $errno))) + ) + + ;;; Adjust the timestamps of a file or directory. + ;;; Note: This is similar to `utimensat` in POSIX. + (@interface func (export "path_filestat_set_times") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $flags $lookupflags) + ;;; The path of the file or directory to operate on. + (param $path string) + ;;; The desired values of the data access timestamp. + (param $atim $timestamp) + ;;; The desired values of the data modification timestamp. + (param $mtim $timestamp) + ;;; A bitmask indicating which timestamps to adjust. + (param $fst_flags $fstflags) + (result $error (expected (error $errno))) + ) + + ;;; Create a hard link. + ;;; Note: This is similar to `linkat` in POSIX. + (@interface func (export "path_link") + (param $old_fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $old_flags $lookupflags) + ;;; The source path from which to link. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path at which to create the hard link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Open a file or directory. + ;; + ;;; The returned file descriptor is not guaranteed to be the lowest-numbered + ;;; file descriptor not currently open; it is randomized to prevent + ;;; applications from depending on making assumptions about indexes, since this + ;;; is error-prone in multi-threaded contexts. The returned file descriptor is + ;;; guaranteed to be less than 2**31. + ;; + ;;; Note: This is similar to `openat` in POSIX. + (@interface func (export "path_open") + (param $fd $fd) + ;;; Flags determining the method of how the path is resolved. + (param $dirflags $lookupflags) + ;;; The relative path of the file or directory to open, relative to the + ;;; `path_open::fd` directory. + (param $path string) + ;;; The method by which to open the file. + (param $oflags $oflags) + ;;; The initial rights of the newly created file descriptor. The + ;;; implementation is allowed to return a file descriptor with fewer rights + ;;; than specified, if and only if those rights do not apply to the type of + ;;; file being opened. + ;; + ;;; The *base* rights are rights that will apply to operations using the file + ;;; descriptor itself, while the *inheriting* rights are rights that apply to + ;;; file descriptors derived from it. + (param $fs_rights_base $rights) + (param $fs_rights_inheriting $rights) + (param $fdflags $fdflags) + ;;; The file descriptor of the file that has been opened. + (result $error (expected $fd (error $errno))) + ) + + ;;; Read the contents of a symbolic link. + ;;; Note: This is similar to `readlinkat` in POSIX. + (@interface func (export "path_readlink") + (param $fd $fd) + ;;; The path of the symbolic link from which to read. + (param $path string) + ;;; The buffer to which to write the contents of the symbolic link. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + ;;; The number of bytes placed in the buffer. + (result $error (expected $size (error $errno))) + ) + + ;;; Remove a directory. + ;;; Return `errno::notempty` if the directory is not empty. + ;;; Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + (@interface func (export "path_remove_directory") + (param $fd $fd) + ;;; The path to a directory to remove. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Rename a file or directory. + ;;; Note: This is similar to `renameat` in POSIX. + (@interface func (export "path_rename") + (param $fd $fd) + ;;; The source path of the file or directory to rename. + (param $old_path string) + ;;; The working directory at which the resolution of the new path starts. + (param $new_fd $fd) + ;;; The destination path to which to rename the file or directory. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + ;;; Create a symbolic link. + ;;; Note: This is similar to `symlinkat` in POSIX. + (@interface func (export "path_symlink") + ;;; The contents of the symbolic link. + (param $old_path string) + (param $fd $fd) + ;;; The destination path at which to create the symbolic link. + (param $new_path string) + (result $error (expected (error $errno))) + ) + + + ;;; Unlink a file. + ;;; Return `errno::isdir` if the path refers to a directory. + ;;; Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + (@interface func (export "path_unlink_file") + (param $fd $fd) + ;;; The path to a file to unlink. + (param $path string) + (result $error (expected (error $errno))) + ) + + ;;; Concurrently poll for the occurrence of a set of events. + (@interface func (export "poll_oneoff") + ;;; The events to which to subscribe. + (param $in (@witx const_pointer $subscription)) + ;;; The events that have occurred. + (param $out (@witx pointer $event)) + ;;; Both the number of subscriptions and events. + (param $nsubscriptions $size) + ;;; The number of events stored. + (result $error (expected $size (error $errno))) + ) + + ;;; Terminate the process normally. An exit code of 0 indicates successful + ;;; termination of the program. The meanings of other values is dependent on + ;;; the environment. + (@interface func (export "proc_exit") + ;;; The exit code returned by the process. + (param $rval $exitcode) + (@witx noreturn) + ) + + ;;; Send a signal to the process of the calling thread. + ;;; Note: This is similar to `raise` in POSIX. + (@interface func (export "proc_raise") + ;;; The signal condition to trigger. + (param $sig $signal) + (result $error (expected (error $errno))) + ) + + ;;; Temporarily yield execution of the calling thread. + ;;; Note: This is similar to `sched_yield` in POSIX. + (@interface func (export "sched_yield") + (result $error (expected (error $errno))) + ) + + ;;; Write high-quality random data into a buffer. + ;;; This function blocks when the implementation is unable to immediately + ;;; provide sufficient high-quality random data. + ;;; This function may execute slowly, so when large mounts of random data are + ;;; required, it's advisable to use this function to seed a pseudo-random + ;;; number generator, rather than to provide the random data directly. + (@interface func (export "random_get") + ;;; The buffer to fill with random data. + (param $buf (@witx pointer u8)) + (param $buf_len $size) + (result $error (expected (error $errno))) + ) + + ;;; Accept a new incoming connection. + ;;; Note: This is similar to `accept` in POSIX. + (@interface func (export "sock_accept") + ;;; The listening socket. + (param $fd $fd) + ;;; The desired values of the file descriptor flags. + (param $flags $fdflags) + ;;; New socket connection + (result $error (expected $fd (error $errno))) + ) + + ;;; Receive a message from a socket. + ;;; Note: This is similar to `recv` in POSIX, though it also supports reading + ;;; the data into multiple buffers in the manner of `readv`. + (@interface func (export "sock_recv") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to store data. + (param $ri_data $iovec_array) + ;;; Message flags. + (param $ri_flags $riflags) + ;;; Number of bytes stored in ri_data and message flags. + (result $error (expected (tuple $size $roflags) (error $errno))) + ) + + ;;; Send a message on a socket. + ;;; Note: This is similar to `send` in POSIX, though it also supports writing + ;;; the data from multiple buffers in the manner of `writev`. + (@interface func (export "sock_send") + (param $fd $fd) + ;;; List of scatter/gather vectors to which to retrieve data + (param $si_data $ciovec_array) + ;;; Message flags. + (param $si_flags $siflags) + ;;; Number of bytes transmitted. + (result $error (expected $size (error $errno))) + ) + + ;;; Shut down socket send and receive channels. + ;;; Note: This is similar to `shutdown` in POSIX. + (@interface func (export "sock_shutdown") + (param $fd $fd) + ;;; Which channels on the socket to shut down. + (param $how $sdflags) + (result $error (expected (error $errno))) + ) +) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d0ead5ec..4d22d0ec 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,8 @@ [toolchain] channel = "stable" components = ["clippy", "rustfmt"] +targets = [ + "wasm32-unknown-unknown", + "wasm32-wasip1", + "wasm32-wasip2" +]