diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ea233b1..9bd96cdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,8 @@ jobs: shell: bash - name: Add wasm32-wasi Rust target run: rustup target add wasm32-wasi + - name: Add wasm32-unknown-unknown Rust target + run: rustup target add wasm32-unknown-unknown - name: Cache cargo uses: actions/cache@v3 with: @@ -60,6 +62,8 @@ jobs: shell: bash - name: Add wasm32-wasi Rust target run: rustup target add wasm32-wasi + - name: Add wasm32-unknown-unknown Rust target + run: rustup target add wasm32-unknown-unknown - name: Cache cargo uses: actions/cache@v3 with: diff --git a/Cargo.lock b/Cargo.lock index d1471e5c..a67a1bbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,16 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli", + "gimli 0.28.1", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli 0.29.0", ] [[package]] @@ -133,16 +142,16 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ - "addr2line", + "addr2line 0.22.0", "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.32.2", + "object 0.35.0", "rustc-demangle", ] @@ -179,6 +188,10 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byte-array-literals" +version = "0.0.0" + [[package]] name = "byteorder" version = "1.5.0" @@ -399,7 +412,7 @@ dependencies = [ "cranelift-control", "cranelift-entity 0.108.1", "cranelift-isle", - "gimli", + "gimli 0.28.1", "hashbrown 0.14.5", "log", "regalloc2", @@ -872,6 +885,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "h2" version = "0.3.26" @@ -1299,22 +1318,22 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" dependencies = [ + "crc32fast", + "hashbrown 0.14.5", + "indexmap", "memchr", ] [[package]] name = "object" -version = "0.33.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ - "crc32fast", - "hashbrown 0.14.5", - "indexmap", "memchr", ] @@ -1371,9 +1390,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1461,9 +1480,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -1784,18 +1803,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1929,6 +1948,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spdx" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ef1a0fa1e39ac22972c8db23ff89aea700ab96aa87114e1fb55937a631a0c9" +dependencies = [ + "smallvec", +] + [[package]] name = "spin" version = "0.9.8" @@ -2353,6 +2381,18 @@ dependencies = [ "wat", ] +[[package]] +name = "viceroy-component-adapter" +version = "0.0.0" +dependencies = [ + "bitflags 2.5.0", + "byte-array-literals", + "object 0.33.0", + "wasi", + "wasm-encoder 0.208.1", + "wit-bindgen-rust-macro", +] + [[package]] name = "viceroy-lib" version = "0.9.8" @@ -2388,11 +2428,13 @@ dependencies = [ "tracing", "tracing-futures", "url", + "wasm-encoder 0.208.1", "wasmparser 0.208.1", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-nn", "wiggle", + "wit-component", ] [[package]] @@ -2490,6 +2532,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6425e84e42f7f558478e40ecc2287912cb319f2ca68e5c0bb93c61d4fc63fa17" dependencies = [ "leb128", + "wasmparser 0.208.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a05336882dae732ce6bd48b7e11fe597293cb72c13da4f35d7d5f8d53b2a7" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-metadata" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a2c4280ad374a6db3d76d4bb61e2ec4b3b9ce5469cc4f2bbc5708047a2bbff" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.208.1", + "wasmparser 0.208.1", ] [[package]] @@ -2535,7 +2603,7 @@ version = "21.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92a1370c66a0022e6d92dcc277e2c84f5dece19569670b8ce7db8162560d8b6" dependencies = [ - "addr2line", + "addr2line 0.21.0", "anyhow", "async-trait", "bumpalo", @@ -2543,7 +2611,7 @@ dependencies = [ "cfg-if", "encoding_rs", "fxprof-processed-profile", - "gimli", + "gimli 0.28.1", "hashbrown 0.14.5", "indexmap", "ittapi", @@ -2626,7 +2694,7 @@ dependencies = [ "syn", "wasmtime-component-util", "wasmtime-wit-bindgen", - "wit-parser", + "wit-parser 0.207.0", ] [[package]] @@ -2649,7 +2717,7 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "cranelift-wasm", - "gimli", + "gimli 0.28.1", "log", "object 0.33.0", "target-lexicon", @@ -2668,7 +2736,7 @@ dependencies = [ "anyhow", "cpp_demangle", "cranelift-entity 0.108.1", - "gimli", + "gimli 0.28.1", "indexmap", "log", "object 0.33.0", @@ -2807,7 +2875,7 @@ checksum = "97b27054fed6be4f3800aba5766f7ef435d4220ce290788f021a08d4fa573108" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", + "gimli 0.28.1", "object 0.33.0", "target-lexicon", "wasmparser 0.207.0", @@ -2825,7 +2893,7 @@ dependencies = [ "anyhow", "heck 0.4.1", "indexmap", - "wit-parser", + "wit-parser 0.207.0", ] [[package]] @@ -2839,24 +2907,24 @@ dependencies = [ [[package]] name = "wast" -version = "208.0.1" +version = "209.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00b3f023b4e2ccd2e054e240294263db52ae962892e6523e550783c83a67f1" +checksum = "8fffef2ff6147e4d12e972765fd75332c6a11c722571d4ab7a780d81ffc8f0a4" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.208.1", + "wasm-encoder 0.209.1", ] [[package]] name = "wat" -version = "1.208.1" +version = "1.209.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ed38e59176550214c025ea2bd0eeefd8e86b92d0af6698d5ba95020ec2e07b" +checksum = "42203ec0271d113f8eb1f77ebc624886530cecb35915a7f63a497131f16e4d24" dependencies = [ - "wast 208.0.1", + "wast 209.0.1", ] [[package]] @@ -2941,7 +3009,7 @@ checksum = "1dc69899ccb2da7daa4df31426dcfd284b104d1a85e1dae35806df0c46187f87" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", + "gimli 0.28.1", "regalloc2", "smallvec", "target-lexicon", @@ -3100,9 +3168,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] @@ -3117,6 +3185,64 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "wit-bindgen-core" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7076a12e69af6e1f6093bd16657d7ae61c30cfd3c5f62321046eb863b17ab1e2" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.208.1", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8ca0dd2aa75452450da1906391aba9d3a43d95fa920e872361ea00acc452a5" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d734e18bdf439ed86afe9d85fc5bfa38db4b676fc0e0a0dae95bd8f14de039" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef7dd0e47f5135dd8739ccc5b188ab8b7e27e1d64df668aa36680f0b8646db8" +dependencies = [ + "anyhow", + "bitflags 2.5.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.208.1", + "wasm-metadata", + "wasmparser 0.208.1", + "wit-parser 0.208.1", +] + [[package]] name = "wit-parser" version = "0.207.0" @@ -3135,6 +3261,24 @@ dependencies = [ "wasmparser 0.207.0", ] +[[package]] +name = "wit-parser" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516417a730725fe3e6c9e2efc8d86697480f80496d32b24e62736950704c047c" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.23", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.208.1", +] + [[package]] name = "witx" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index d471c4df..3d9e7093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "cli", "lib", + "crates/adapter", + "crates/adapter/byte-array-literals", ] resolver = "2" @@ -43,3 +45,28 @@ wasmtime-wasi = "21.0.0" wasmtime-wasi-nn = "21.0.0" wiggle = "21.0.0" wasmparser = "0.208.0" +wasm-encoder = { version = "0.208.0", features = ["wasmparser"] } +wit-component = "0.208.0" + +# Adapter dependencies +byte-array-literals = { path = "crates/adapter/byte-array-literals" } +bitflags = { version = "2.5.0", default-features = false } +object = { version = "0.33", default-features = false, features = ["archive"] } +wasi = { version = "0.11.0", default-features = false } +wit-bindgen-rust-macro = { version = "0.25.0", default-features = false } + +[profile.release.package.viceroy-component-adapter] +opt-level = 's' +strip = 'debuginfo' + +[profile.dev.package.viceroy-component-adapter] +# Make dev look like a release build since this adapter module won't work with +# a debug build that uses data segments and such. +incremental = false +opt-level = 's' +# Omit assertions, which include failure messages which require string +# initializers. +debug-assertions = false +# Omit integer overflow checks, which include failure messages which require +# string initializers. +overflow-checks = false diff --git a/Makefile b/Makefile index f727a2f0..428ede2f 100644 --- a/Makefile +++ b/Makefile @@ -73,3 +73,13 @@ package-check: rm publish rm -rf .cargo/ rm -rf verify-publishable/ + +# Re-generate the adapter, and move it into `lib/adapter` +.PHONY: adapter +adapter: + cargo build --release \ + -p viceroy-component-adapter \ + --target wasm32-unknown-unknown + mkdir -p lib/adapter + cp target/wasm32-unknown-unknown/release/viceroy_component_adapter.wasm \ + lib/data/viceroy-component-adapter.wasm diff --git a/cli/src/main.rs b/cli/src/main.rs index 5e63336f..1c02d222 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -93,6 +93,39 @@ pub async fn main() -> ExitCode { Err(_) => ExitCode::FAILURE, } } + Commands::Adapt(adapt_args) => { + install_tracing_subscriber(adapt_args.verbosity()); + let input = adapt_args.input(); + let output = adapt_args.output(); + let bytes = match std::fs::read(&input) { + Ok(bytes) => bytes, + Err(_) => { + event!( + Level::ERROR, + "Failed to read module from: {}", + input.display() + ); + return ExitCode::FAILURE; + } + }; + + let module = match viceroy_lib::adapt::adapt_bytes(&bytes) { + Ok(module) => module, + Err(e) => { + event!(Level::ERROR, "Failed to adapt module: {e}"); + return ExitCode::FAILURE; + } + }; + + event!(Level::INFO, "Writing component to: {}", output.display()); + match std::fs::write(output, module) { + Ok(_) => ExitCode::SUCCESS, + Err(e) => { + event!(Level::ERROR, "Failed to write component: {e}"); + return ExitCode::FAILURE; + } + } + } } } @@ -243,6 +276,7 @@ async fn create_execution_context( args.wasi_modules(), guest_profile_path, args.unknown_import_behavior(), + args.adapt(), )? .with_log_stderr(args.log_stderr()) .with_log_stdout(args.log_stdout()); diff --git a/cli/src/opts.rs b/cli/src/opts.rs index e4295728..4f34ad78 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -40,6 +40,9 @@ pub enum Commands { /// Run the input wasm once and then exit. Run(RunArgs), + + /// Adapt core wasm to a component. + Adapt(AdaptArgs), } #[derive(Debug, Args, Clone)] @@ -66,7 +69,7 @@ pub struct RunArgs { pub struct SharedArgs { /// The path to the service's Wasm module. #[arg(value_parser = check_module, required=true)] - input: Option, + input: Option, /// The path to a TOML file containing `local_server` configuration. #[arg(short = 'C', long = "config")] config_path: Option, @@ -108,6 +111,10 @@ pub struct SharedArgs { /// effect if you set RUST_LOG to a value before starting Viceroy #[arg(short = 'v', action = clap::ArgAction::Count)] verbosity: u8, + /// Whether or not to automatically adapt core-wasm modules to + /// components before running them. + #[arg(long = "adapt")] + adapt: bool, } #[derive(Debug, Clone)] @@ -168,7 +175,7 @@ impl RunArgs { impl SharedArgs { /// The path to the service's Wasm binary. pub fn input(&self) -> PathBuf { - PathBuf::from(self.input.as_ref().unwrap()) + self.input.as_ref().unwrap().clone() } /// The path to a `local_server` configuration file. @@ -210,6 +217,50 @@ impl SharedArgs { pub fn verbosity(&self) -> u8 { self.verbosity } + + pub fn adapt(&self) -> bool { + self.adapt + } +} + +#[derive(Args, Debug, Clone)] +pub struct AdaptArgs { + /// The path to the Wasm module to adapt. + #[arg(value_parser = check_module, required=true)] + input: PathBuf, + + /// The output name + #[arg(short = 'o', long = "output")] + output: Option, + + /// Verbosity of logs for Viceroy. `-v` sets the log level to INFO, + /// `-vv` to DEBUG, and `-vvv` to TRACE. This option will not take + /// effect if you set RUST_LOG to a value before starting Viceroy + #[arg(short = 'v', action = clap::ArgAction::Count)] + verbosity: u8, +} + +impl AdaptArgs { + pub(crate) fn input(&self) -> PathBuf { + self.input.clone() + } + + pub(crate) fn output(&self) -> PathBuf { + if let Some(output) = self.output.as_ref() { + return output.clone(); + } + + let mut output = PathBuf::from(self.input.file_name().expect("input filename")); + output.set_extension("component.wasm"); + output + } + + /// Verbosity of logs for Viceroy. `-v` sets the log level to DEBUG and + /// `-vv` to TRACE. This option will not take effect if you set RUST_LOG + /// to a value before starting Viceroy + pub fn verbosity(&self) -> u8 { + self.verbosity + } } /// Enum of available (experimental) wasi modules @@ -254,11 +305,11 @@ impl From<&ExperimentalModule> for ExperimentalModuleArg { /// binary or text format. /// /// [opts]: struct.Opts.html -fn check_module(s: &str) -> Result { +fn check_module(s: &str) -> Result { let path = PathBuf::from(s); - let contents = std::fs::read(path)?; + let contents = std::fs::read(&path)?; match wat::parse_bytes(&contents) { - Ok(_) => Ok(s.to_string()), + Ok(_) => Ok(path), _ => Err(Error::FileFormat), } } diff --git a/cli/tests/integration/common.rs b/cli/tests/integration/common.rs index b1a4b498..e4849316 100644 --- a/cli/tests/integration/common.rs +++ b/cli/tests/integration/common.rs @@ -276,12 +276,14 @@ impl Test { self.backends.start_servers().await; } + let adapt_core_wasm = false; let ctx = ExecuteCtx::new( &self.module_path, ProfilingStrategy::None, HashSet::new(), None, self.unknown_import_behavior, + adapt_core_wasm, )? .with_backends(self.backends.backend_configs().await) .with_dictionaries(self.dictionaries.clone()) diff --git a/cli/tests/trap-test/Cargo.lock b/cli/tests/trap-test/Cargo.lock index 286948ab..098b8fcf 100644 --- a/cli/tests/trap-test/Cargo.lock +++ b/cli/tests/trap-test/Cargo.lock @@ -8,7 +8,16 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli", + "gimli 0.28.1", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli 0.29.0", ] [[package]] @@ -148,16 +157,16 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ - "addr2line", + "addr2line 0.22.0", "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.32.2", + "object 0.35.0", "rustc-demangle", ] @@ -426,7 +435,7 @@ dependencies = [ "cranelift-control", "cranelift-entity 0.108.1", "cranelift-isle", - "gimli", + "gimli 0.28.1", "hashbrown 0.14.5", "log", "regalloc2", @@ -880,6 +889,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "h2" version = "0.3.26" @@ -1306,22 +1321,22 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" dependencies = [ + "crc32fast", + "hashbrown 0.14.5", + "indexmap", "memchr", ] [[package]] name = "object" -version = "0.33.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ - "crc32fast", - "hashbrown 0.14.5", - "indexmap", "memchr", ] @@ -1372,9 +1387,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1462,9 +1477,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -1785,18 +1800,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1905,6 +1920,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spdx" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ef1a0fa1e39ac22972c8db23ff89aea700ab96aa87114e1fb55937a631a0c9" +dependencies = [ + "smallvec", +] + [[package]] name = "spin" version = "0.9.8" @@ -2336,11 +2360,13 @@ dependencies = [ "tracing", "tracing-futures", "url", + "wasm-encoder 0.208.1", "wasmparser 0.208.1", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-nn", "wiggle", + "wit-component", ] [[package]] @@ -2438,6 +2464,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6425e84e42f7f558478e40ecc2287912cb319f2ca68e5c0bb93c61d4fc63fa17" dependencies = [ "leb128", + "wasmparser 0.208.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a05336882dae732ce6bd48b7e11fe597293cb72c13da4f35d7d5f8d53b2a7" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-metadata" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a2c4280ad374a6db3d76d4bb61e2ec4b3b9ce5469cc4f2bbc5708047a2bbff" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.208.1", + "wasmparser 0.208.1", ] [[package]] @@ -2483,7 +2535,7 @@ version = "21.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92a1370c66a0022e6d92dcc277e2c84f5dece19569670b8ce7db8162560d8b6" dependencies = [ - "addr2line", + "addr2line 0.21.0", "anyhow", "async-trait", "bumpalo", @@ -2491,7 +2543,7 @@ dependencies = [ "cfg-if", "encoding_rs", "fxprof-processed-profile", - "gimli", + "gimli 0.28.1", "hashbrown 0.14.5", "indexmap", "ittapi", @@ -2574,7 +2626,7 @@ dependencies = [ "syn", "wasmtime-component-util", "wasmtime-wit-bindgen", - "wit-parser", + "wit-parser 0.207.0", ] [[package]] @@ -2597,7 +2649,7 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "cranelift-wasm", - "gimli", + "gimli 0.28.1", "log", "object 0.33.0", "target-lexicon", @@ -2616,7 +2668,7 @@ dependencies = [ "anyhow", "cpp_demangle", "cranelift-entity 0.108.1", - "gimli", + "gimli 0.28.1", "indexmap", "log", "object 0.33.0", @@ -2755,7 +2807,7 @@ checksum = "97b27054fed6be4f3800aba5766f7ef435d4220ce290788f021a08d4fa573108" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", + "gimli 0.28.1", "object 0.33.0", "target-lexicon", "wasmparser 0.207.0", @@ -2773,7 +2825,7 @@ dependencies = [ "anyhow", "heck 0.4.1", "indexmap", - "wit-parser", + "wit-parser 0.207.0", ] [[package]] @@ -2787,24 +2839,24 @@ dependencies = [ [[package]] name = "wast" -version = "208.0.1" +version = "209.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00b3f023b4e2ccd2e054e240294263db52ae962892e6523e550783c83a67f1" +checksum = "8fffef2ff6147e4d12e972765fd75332c6a11c722571d4ab7a780d81ffc8f0a4" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.208.1", + "wasm-encoder 0.209.1", ] [[package]] name = "wat" -version = "1.208.1" +version = "1.209.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ed38e59176550214c025ea2bd0eeefd8e86b92d0af6698d5ba95020ec2e07b" +checksum = "42203ec0271d113f8eb1f77ebc624886530cecb35915a7f63a497131f16e4d24" dependencies = [ - "wast 208.0.1", + "wast 209.0.1", ] [[package]] @@ -2889,7 +2941,7 @@ checksum = "1dc69899ccb2da7daa4df31426dcfd284b104d1a85e1dae35806df0c46187f87" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", + "gimli 0.28.1", "regalloc2", "smallvec", "target-lexicon", @@ -3048,9 +3100,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] @@ -3065,6 +3117,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "wit-component" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef7dd0e47f5135dd8739ccc5b188ab8b7e27e1d64df668aa36680f0b8646db8" +dependencies = [ + "anyhow", + "bitflags 2.5.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.208.1", + "wasm-metadata", + "wasmparser 0.208.1", + "wit-parser 0.208.1", +] + [[package]] name = "wit-parser" version = "0.207.0" @@ -3083,6 +3154,24 @@ dependencies = [ "wasmparser 0.207.0", ] +[[package]] +name = "wit-parser" +version = "0.208.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516417a730725fe3e6c9e2efc8d86697480f80496d32b24e62736950704c047c" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.23", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.208.1", +] + [[package]] name = "witx" version = "0.9.1" diff --git a/cli/tests/trap-test/src/main.rs b/cli/tests/trap-test/src/main.rs index 1023106c..d8ab745b 100644 --- a/cli/tests/trap-test/src/main.rs +++ b/cli/tests/trap-test/src/main.rs @@ -16,12 +16,14 @@ pub type TestResult = Result<(), Error>; #[tokio::test(flavor = "multi_thread")] async fn fatal_error_traps() -> TestResult { let module_path = format!("{RUST_FIXTURE_PATH}/response.wasm"); + let adapt_core_wasm = false; let ctx = ExecuteCtx::new( module_path, ProfilingStrategy::None, HashSet::new(), None, viceroy_lib::config::UnknownImportBehavior::LinkError, + adapt_core_wasm, )?; let req = Request::get("http://127.0.0.1:7676/").body(Body::from(""))?; let resp = ctx diff --git a/crates/adapter/Cargo.toml b/crates/adapter/Cargo.toml new file mode 100644 index 00000000..4c5866b1 --- /dev/null +++ b/crates/adapter/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "viceroy-component-adapter" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +wasi = { workspace = true } +wit-bindgen-rust-macro = { workspace = true } +byte-array-literals = { workspace = true } +bitflags = { workspace = true } + +[build-dependencies] +wasm-encoder = { workspace = true } +object = { workspace = true } + +[lib] +test = false +crate-type = ["cdylib"] diff --git a/crates/adapter/README.md b/crates/adapter/README.md new file mode 100644 index 00000000..7d66179b --- /dev/null +++ b/crates/adapter/README.md @@ -0,0 +1,29 @@ +# The Fastly Component Adapter + +This crate builds a wasm module that adapts both the preview1 api and the fastly +compute host calls to the component model. It started as a fork of the +[wasi_snapshot_preview1] component adapter with the `proxy` feature manually +expanded out, with all of the fastly-specific functionality mostly being added +in the `src/fastly` tree. The exception to this is the reactor export defined +by the compute world of `compute.wit`, whose definition is in `src/lib.rs` +instead of being defined in `src/fastly`, as the `wit-bindgen::generate!` makes +assumptions about relative module paths that make it hard to define elsewhere. + +Changes to the adapter require running the top-level `make adapter` target, and +committing the resulting `lib/data/viceroy-component-adapter.wasm` wasm module. +This is a bit unfortunate, but as there's no way to hook the packaging step with +cargo, committing the binary is the easiest way to ensure that fresh checkouts +of this repository and packaged versions of the crates both build seamlessly. + +## Adding New Host Calls + +When adding new host calls, the adapter will need to be updated to know how they +should be adapted to the component model. In most cases, this will involve +updating the `/lib/wit/deps/fastly/compute.wit` package to describe what the +component imports of the new host call should look like, implementing it in both +`/lib/src/wiggle_abi` and `/lib/src/component`, and then finally adding a +version of the host call to the adapter in `src/fastly`. As the adapter builds +with the same `compute.wit` that `viceroy-lib` does, the imports will +automatically be available through the top-level `bindings` module. + +[wasi_snapshot_preview1]: https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi-preview1-component-adapter diff --git a/crates/adapter/build.rs b/crates/adapter/build.rs new file mode 100644 index 00000000..2762dd6b --- /dev/null +++ b/crates/adapter/build.rs @@ -0,0 +1,318 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + let wasm = build_raw_intrinsics(); + let archive = build_archive(&wasm); + + std::fs::write(out_dir.join("libwasm-raw-intrinsics.a"), &archive).unwrap(); + println!("cargo:rustc-link-lib=static=wasm-raw-intrinsics"); + println!( + "cargo:rustc-link-search=native={}", + out_dir.to_str().unwrap() + ); + + // Some specific flags to `wasm-ld` to inform the shape of this adapter. + // Notably we're importing memory from the main module and additionally our + // own module has no stack at all since it's specifically allocated at + // startup. + println!("cargo:rustc-link-arg=--import-memory"); + println!("cargo:rustc-link-arg=-zstack-size=0"); +} + +/// This function will produce a wasm module which is itself an object file +/// that is the basic equivalent of: +/// +/// ```rust +/// std::arch::global_asm!( +/// " +/// .globaltype internal_state_ptr, i32 +/// internal_state_ptr: +/// " +/// ); +/// +/// #[no_mangle] +/// extern "C" fn get_state_ptr() -> *mut u8 { +/// unsafe { +/// let ret: *mut u8; +/// std::arch::asm!( +/// " +/// global.get internal_state_ptr +/// ", +/// out(local) ret, +/// options(nostack, readonly) +/// ); +/// ret +/// } +/// } +/// +/// #[no_mangle] +/// extern "C" fn set_state_ptr(val: *mut u8) { +/// unsafe { +/// std::arch::asm!( +/// " +/// local.get {} +/// global.set internal_state_ptr +/// ", +/// in(local) val, +/// options(nostack, readonly) +/// ); +/// } +/// } +/// +/// // And likewise for `allocation_state`, `get_allocation_state`, and `set_allocation_state` +/// ``` +/// +/// The main trickiness here is getting the `reloc.CODE` and `linking` sections +/// right. +fn build_raw_intrinsics() -> Vec { + use wasm_encoder::Instruction::*; + use wasm_encoder::*; + + let mut module = Module::new(); + + let mut types = TypeSection::new(); + types.function([], [ValType::I32]); + types.function([ValType::I32], []); + module.section(&types); + + // Declare the functions, using the type we just added. + let mut funcs = FunctionSection::new(); + funcs.function(0); + funcs.function(1); + funcs.function(0); + funcs.function(1); + module.section(&funcs); + + // Declare the globals. + let mut globals = GlobalSection::new(); + // internal_state_ptr + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + shared: false, + }, + &ConstExpr::i32_const(0), + ); + // allocation_state + globals.global( + GlobalType { + val_type: ValType::I32, + mutable: true, + shared: false, + }, + &ConstExpr::i32_const(0), + ); + module.section(&globals); + + // Here the `code` section is defined. This is tricky because an offset is + // needed within the code section itself for the `reloc.CODE` section + // later. At this time `wasm-encoder` doesn't give enough functionality to + // use the high-level APIs. so everything is done manually here. + // + // First the function bodies are created and then they're appended into a + // code section. + + let mut code = Vec::new(); + 4u32.encode(&mut code); // number of functions + + let global_get = 0x23; + let global_set = 0x24; + + let encode = |code: &mut _, global, instruction| { + assert!(global < 0x7F); + + let mut body = Vec::new(); + 0u32.encode(&mut body); // no locals + if instruction == global_set { + LocalGet(0).encode(&mut body); + } + let global_offset = body.len() + 1; + // global.get $global ;; but with maximal encoding of $global + body.extend_from_slice(&[instruction, 0x80u8 + global, 0x80, 0x80, 0x80, 0x00]); + End.encode(&mut body); + body.len().encode(code); // length of the function + let offset = code.len() + global_offset; + code.extend_from_slice(&body); // the function itself + offset + }; + + let internal_state_ptr_ref1 = encode(&mut code, 0, global_get); // get_state_ptr + let internal_state_ptr_ref2 = encode(&mut code, 0, global_set); // set_state_ptr + let allocation_state_ref1 = encode(&mut code, 1, global_get); // get_allocation_state + let allocation_state_ref2 = encode(&mut code, 1, global_set); // set_allocation_state + + module.section(&RawSection { + id: SectionId::Code as u8, + data: &code, + }); + + // Here the linking section is constructed. There is one symbol for each function and global. The injected + // globals here are referenced in the relocations below. + // + // More information about this format is at + // https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md + { + let mut linking = Vec::new(); + linking.push(0x02); // version + + linking.push(0x08); // `WASM_SYMBOL_TABLE` + let mut subsection = Vec::new(); + 6u32.encode(&mut subsection); // 6 symbols (4 functions + 2 globals) + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 0u32.encode(&mut subsection); // function index + "get_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 1u32.encode(&mut subsection); // function index + "set_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 2u32.encode(&mut subsection); // function index + "get_allocation_state".encode(&mut subsection); // symbol name + + subsection.push(0x00); // SYMTAB_FUNCTION + 0x00.encode(&mut subsection); // flags + 3u32.encode(&mut subsection); // function index + "set_allocation_state".encode(&mut subsection); // symbol name + + subsection.push(0x02); // SYMTAB_GLOBAL + 0x02.encode(&mut subsection); // flags (WASM_SYM_BINDING_LOCAL) + 0u32.encode(&mut subsection); // global index + "internal_state_ptr".encode(&mut subsection); // symbol name + + subsection.push(0x02); // SYMTAB_GLOBAL + 0x00.encode(&mut subsection); // flags + 1u32.encode(&mut subsection); // global index + "allocation_state".encode(&mut subsection); // symbol name + + subsection.encode(&mut linking); + module.section(&CustomSection { + name: "linking".into(), + data: linking.into(), + }); + } + + // A `reloc.CODE` section is appended here with relocations for the + // `global`-referencing instructions that were added. + { + let mut reloc = Vec::new(); + 3u32.encode(&mut reloc); // target section (code is the 4th section, 3 when 0-indexed) + 4u32.encode(&mut reloc); // 4 relocations + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + internal_state_ptr_ref1.encode(&mut reloc); // offset + 4u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + internal_state_ptr_ref2.encode(&mut reloc); // offset + 4u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + allocation_state_ref1.encode(&mut reloc); // offset + 5u32.encode(&mut reloc); // symbol index + + reloc.push(0x07); // R_WASM_GLOBAL_INDEX_LEB + allocation_state_ref2.encode(&mut reloc); // offset + 5u32.encode(&mut reloc); // symbol index + + module.section(&CustomSection { + name: "reloc.CODE".into(), + data: reloc.into(), + }); + } + + module.finish() +} + +/// This function produces the output of `llvm-ar crus libfoo.a foo.o` given +/// the object file above as input. The archive is what's eventually fed to +/// LLD. +/// +/// Like above this is still tricky, mainly around the production of the symbol +/// table. +fn build_archive(wasm: &[u8]) -> Vec { + use object::{bytes_of, endian::BigEndian, U32Bytes}; + + let mut archive = Vec::new(); + archive.extend_from_slice(&object::archive::MAGIC); + + // The symbol table is in the "GNU" format which means it has a structure + // that looks like: + // + // * a big-endian 32-bit integer for the number of symbols + // * N big-endian 32-bit integers for the offset to the object file, within + // the entire archive, for which object has the symbol + // * N nul-delimited strings for each symbol + // + // Here we're building an archive with just a few symbols so it's a bit + // easier. Note though we don't know the offset of our `intrinsics.o` up + // front so it's left as 0 for now and filled in later. + + let syms = [ + "get_state_ptr", + "set_state_ptr", + "get_allocation_state", + "set_allocation_state", + "allocation_state", + ]; + + let mut symbol_table = Vec::new(); + symbol_table.extend_from_slice(bytes_of(&U32Bytes::new(BigEndian, syms.len() as u32))); + for _ in syms.iter() { + symbol_table.extend_from_slice(bytes_of(&U32Bytes::new(BigEndian, 0))); + } + for s in syms.iter() { + symbol_table.extend_from_slice(&std::ffi::CString::new(*s).unwrap().into_bytes_with_nul()); + } + + archive.extend_from_slice(bytes_of(&object::archive::Header { + name: *b"/ ", + date: *b"0 ", + uid: *b"0 ", + gid: *b"0 ", + mode: *b"0 ", + size: format!("{:<10}", symbol_table.len()) + .as_bytes() + .try_into() + .unwrap(), + terminator: object::archive::TERMINATOR, + })); + let symtab_offset = archive.len(); + archive.extend_from_slice(&symbol_table); + + // All archive members must start on even offsets + if archive.len() & 1 == 1 { + archive.push(0x00); + } + + // Now that we have the starting offset of the `intrinsics.o` file go back + // and fill in the offset within the symbol table generated earlier. + let member_offset = archive.len(); + for (index, _) in syms.iter().enumerate() { + let index = index + 1; + archive[symtab_offset + (index * 4)..][..4].copy_from_slice(bytes_of(&U32Bytes::new( + BigEndian, + member_offset.try_into().unwrap(), + ))); + } + + archive.extend_from_slice(object::bytes_of(&object::archive::Header { + name: *b"intrinsics.o ", + date: *b"0 ", + uid: *b"0 ", + gid: *b"0 ", + mode: *b"644 ", + size: format!("{:<10}", wasm.len()).as_bytes().try_into().unwrap(), + terminator: object::archive::TERMINATOR, + })); + archive.extend_from_slice(&wasm); + archive +} diff --git a/crates/adapter/byte-array-literals/Cargo.toml b/crates/adapter/byte-array-literals/Cargo.toml new file mode 100644 index 00000000..6f27edb0 --- /dev/null +++ b/crates/adapter/byte-array-literals/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "byte-array-literals" +version = "0.0.0" +publish = false + +[lib] +proc-macro = true +test = false +doctest = false diff --git a/crates/adapter/byte-array-literals/README.md b/crates/adapter/byte-array-literals/README.md new file mode 100644 index 00000000..15685494 --- /dev/null +++ b/crates/adapter/byte-array-literals/README.md @@ -0,0 +1,15 @@ +# byte-array-literals + +This crate exists to solve a very peculiar problem for the +`wasi-preview1-component-adapter`: we want to use string literals in our +source code, but the resulting binary (when compiled for +wasm32-unknown-unknown) cannot contain any data sections. + +The answer that @sunfishcode discovered is that these string literals, if +represented as an array of u8 literals, these will somehow not end up in the +data section, at least when compiled with opt-level='s' on today's rustc +(1.69.0). So, this crate exists to transform these literals using a proc +macro. + +It is very possible this cheat code will abruptly stop working in some future +compiler, but we'll cross that bridge when we get to it. diff --git a/crates/adapter/byte-array-literals/src/lib.rs b/crates/adapter/byte-array-literals/src/lib.rs new file mode 100644 index 00000000..ea9f013d --- /dev/null +++ b/crates/adapter/byte-array-literals/src/lib.rs @@ -0,0 +1,98 @@ +extern crate proc_macro; + +use proc_macro::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree}; + +/// Expand a `str` literal into a byte array. +#[proc_macro] +pub fn str(input: TokenStream) -> TokenStream { + let rv = convert_str(input); + + vec![TokenTree::Group(Group::new( + Delimiter::Bracket, + rv.into_iter().collect(), + ))] + .into_iter() + .collect() +} + +/// The same as `str` but appends a `'\n'`. +#[proc_macro] +pub fn str_nl(input: TokenStream) -> TokenStream { + let mut rv = convert_str(input); + + rv.push(TokenTree::Literal(Literal::u8_suffixed(b'\n'))); + + vec![TokenTree::Group(Group::new( + Delimiter::Bracket, + rv.into_iter().collect(), + ))] + .into_iter() + .collect() +} + +fn convert_str(input: TokenStream) -> Vec { + let mut it = input.into_iter(); + + let mut tokens = Vec::new(); + match it.next() { + Some(TokenTree::Literal(l)) => { + for b in to_string(l).into_bytes() { + tokens.push(TokenTree::Literal(Literal::u8_suffixed(b))); + tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone))); + } + } + _ => panic!(), + } + + assert!(it.next().is_none()); + tokens +} + +fn to_string(lit: Literal) -> String { + let formatted = lit.to_string(); + + let mut it = formatted.chars(); + assert_eq!(it.next(), Some('"')); + + let mut rv = String::new(); + loop { + match it.next() { + Some('"') => match it.next() { + Some(_) => panic!(), + None => break, + }, + Some('\\') => match it.next() { + Some('x') => { + let hi = it.next().unwrap().to_digit(16).unwrap(); + let lo = it.next().unwrap().to_digit(16).unwrap(); + let v = (hi << 16) | lo; + rv.push(v as u8 as char); + } + Some('u') => { + assert_eq!(it.next(), Some('{')); + let mut c = it.next().unwrap(); + let mut ch = 0; + while let Some(v) = c.to_digit(16) { + ch *= 16; + ch |= v; + c = it.next().unwrap(); + } + assert_eq!(c, '}'); + rv.push(::std::char::from_u32(ch).unwrap()); + } + Some('0') => rv.push('\0'), + Some('\\') => rv.push('\\'), + Some('\"') => rv.push('\"'), + Some('r') => rv.push('\r'), + Some('n') => rv.push('\n'), + Some('t') => rv.push('\t'), + Some(_) => panic!(), + None => panic!(), + }, + Some(c) => rv.push(c), + None => panic!(), + } + } + + rv +} diff --git a/crates/adapter/src/descriptors.rs b/crates/adapter/src/descriptors.rs new file mode 100644 index 00000000..d013877f --- /dev/null +++ b/crates/adapter/src/descriptors.rs @@ -0,0 +1,238 @@ +use crate::bindings::wasi::cli::{stderr, stdin, stdout}; +use crate::bindings::wasi::io::streams::{InputStream, OutputStream}; +use crate::{BumpArena, ImportAlloc, TrappingUnwrap}; +use core::cell::{Cell, OnceCell, UnsafeCell}; +use core::mem::MaybeUninit; +use wasi::{Errno, Fd}; + +pub const MAX_DESCRIPTORS: usize = 128; + +#[repr(C)] +pub enum Descriptor { + /// A closed descriptor, holding a reference to the previous closed + /// descriptor to support reusing them. + Closed(Option), + + /// Input and/or output wasi-streams, along with stream metadata. + Streams(Streams), + + Bad, +} + +/// Input and/or output wasi-streams, along with a stream type that +/// identifies what kind of stream they are and possibly supporting +/// type-specific operations like seeking. +pub struct Streams { + /// The input stream, if present. + pub input: OnceCell, + + /// The output stream, if present. + pub output: OnceCell, + + /// Information about the source of the stream. + pub type_: StreamType, +} + +impl Streams { + /// Return the input stream, initializing it on the fly if needed. + pub fn get_read_stream(&self) -> Result<&InputStream, Errno> { + match self.input.get() { + Some(wasi_stream) => Ok(wasi_stream), + + // proxy worlds don't have filesystem access + None => Err(wasi::ERRNO_BADF), + } + } + + /// Return the output stream, initializing it on the fly if needed. + pub fn get_write_stream(&self) -> Result<&OutputStream, Errno> { + match self.output.get() { + Some(wasi_stream) => Ok(wasi_stream), + + // proxy worlds don't have filesystem access + None => Err(wasi::ERRNO_BADF), + } + } +} + +pub enum StreamType { + /// Streams for implementing stdio. + Stdio(Stdio), +} + +pub enum Stdio { + Stdin, + Stdout, + Stderr, +} + +#[repr(C)] +pub struct Descriptors { + /// Storage of mapping from preview1 file descriptors to preview2 file + /// descriptors. + table: UnsafeCell>, + table_len: Cell, + + /// Points to the head of a free-list of closed file descriptors. + closed: Option, +} + +impl Descriptors { + pub fn new(_import_alloc: &ImportAlloc, _arena: &BumpArena) -> Self { + let d = Descriptors { + table: UnsafeCell::new(MaybeUninit::uninit()), + table_len: Cell::new(0), + closed: None, + }; + + fn new_once(val: T) -> OnceCell { + let cell = OnceCell::new(); + let _ = cell.set(val); + cell + } + + d.push(Descriptor::Streams(Streams { + input: new_once(stdin::get_stdin()), + output: OnceCell::new(), + type_: StreamType::Stdio(Stdio::Stdin), + })) + .trapping_unwrap(); + d.push(Descriptor::Streams(Streams { + input: OnceCell::new(), + output: new_once(stdout::get_stdout()), + type_: StreamType::Stdio(Stdio::Stdout), + })) + .trapping_unwrap(); + d.push(Descriptor::Streams(Streams { + input: OnceCell::new(), + output: new_once(stderr::get_stderr()), + type_: StreamType::Stdio(Stdio::Stderr), + })) + .trapping_unwrap(); + + d + } + + fn push(&self, desc: Descriptor) -> Result { + unsafe { + let table = (*self.table.get()).as_mut_ptr(); + let len = usize::try_from(self.table_len.get()).trapping_unwrap(); + if len >= (*table).len() { + return Err(wasi::ERRNO_NOMEM); + } + core::ptr::addr_of_mut!((*table)[len]).write(desc); + self.table_len.set(u16::try_from(len + 1).trapping_unwrap()); + Ok(Fd::from(u32::try_from(len).trapping_unwrap())) + } + } + + fn table(&self) -> &[Descriptor] { + unsafe { + std::slice::from_raw_parts( + (*self.table.get()).as_ptr().cast(), + usize::try_from(self.table_len.get()).trapping_unwrap(), + ) + } + } + + fn table_mut(&mut self) -> &mut [Descriptor] { + unsafe { + std::slice::from_raw_parts_mut( + (*self.table.get()).as_mut_ptr().cast(), + usize::try_from(self.table_len.get()).trapping_unwrap(), + ) + } + } + + pub fn open(&mut self, d: Descriptor) -> Result { + match self.closed { + // No closed descriptors: expand table + None => self.push(d), + Some(freelist_head) => { + // Pop an item off the freelist + let freelist_desc = self.get_mut(freelist_head).trapping_unwrap(); + let next_closed = match freelist_desc { + Descriptor::Closed(next) => *next, + _ => unreachable!("impossible: freelist points to a closed descriptor"), + }; + // Write descriptor to the entry at the nead of the list + *freelist_desc = d; + // Point closed to the following item + self.closed = next_closed; + Ok(freelist_head) + } + } + } + + pub fn get(&self, fd: Fd) -> Result<&Descriptor, Errno> { + self.table() + .get(usize::try_from(fd).trapping_unwrap()) + .ok_or(wasi::ERRNO_BADF) + } + + pub fn get_mut(&mut self, fd: Fd) -> Result<&mut Descriptor, Errno> { + self.table_mut() + .get_mut(usize::try_from(fd).trapping_unwrap()) + .ok_or(wasi::ERRNO_BADF) + } + + // Internal: close a fd, returning the descriptor. + fn close_(&mut self, fd: Fd) -> Result { + // Throw an error if closing an fd which is already closed + match self.get(fd)? { + Descriptor::Closed(_) => Err(wasi::ERRNO_BADF)?, + _ => {} + } + // Mutate the descriptor to be closed, and push the closed fd onto the head of the linked list: + let last_closed = self.closed; + let prev = std::mem::replace(self.get_mut(fd)?, Descriptor::Closed(last_closed)); + self.closed = Some(fd); + Ok(prev) + } + + // Close an fd. + pub fn close(&mut self, fd: Fd) -> Result<(), Errno> { + drop(self.close_(fd)?); + Ok(()) + } + + // Expand the table by pushing a closed descriptor to the end. Used for renumbering. + fn push_closed(&mut self) -> Result<(), Errno> { + let old_closed = self.closed; + let new_closed = self.push(Descriptor::Closed(old_closed))?; + self.closed = Some(new_closed); + Ok(()) + } + + // Implementation of fd_renumber + pub fn renumber(&mut self, from_fd: Fd, to_fd: Fd) -> Result<(), Errno> { + // First, ensure from_fd is in bounds: + let _ = self.get(from_fd)?; + // Expand table until to_fd is in bounds as well: + while self.table_len.get() as u32 <= to_fd { + self.push_closed()?; + } + // Then, close from_fd and put its contents into to_fd: + let desc = self.close_(from_fd)?; + // TODO FIXME if this overwrites a preopen, do we need to clear it from the preopen table? + *self.get_mut(to_fd)? = desc; + + Ok(()) + } + + // A bunch of helper functions implemented in terms of the above pub functions: + + pub fn get_read_stream(&self, fd: Fd) -> Result<&InputStream, Errno> { + match self.get(fd)? { + Descriptor::Streams(streams) => streams.get_read_stream(), + Descriptor::Closed(_) | Descriptor::Bad => Err(wasi::ERRNO_BADF), + } + } + + pub fn get_write_stream(&self, fd: Fd) -> Result<&OutputStream, Errno> { + match self.get(fd)? { + Descriptor::Streams(streams) => streams.get_write_stream(), + Descriptor::Closed(_) | Descriptor::Bad => Err(wasi::ERRNO_BADF), + } + } +} diff --git a/crates/adapter/src/fastly/cache.rs b/crates/adapter/src/fastly/cache.rs new file mode 100644 index 00000000..16d86488 --- /dev/null +++ b/crates/adapter/src/fastly/cache.rs @@ -0,0 +1,465 @@ +use super::{convert_result, BodyHandle, FastlyStatus, RequestHandle}; +use crate::{alloc_result, with_buffer, TrappingUnwrap}; + +pub type CacheHandle = u32; + +pub type CacheObjectLength = u64; +pub type CacheDurationNs = u64; +pub type CacheHitCount = u64; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct CacheLookupOptions { + pub request_headers: RequestHandle, +} + +bitflags::bitflags! { + #[repr(transparent)] + pub struct CacheLookupOptionsMask: u32 { + const _RESERVED = 1 << 0; + const REQUEST_HEADERS = 1 << 1; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct CacheWriteOptions { + pub max_age_ns: u64, + pub request_headers: RequestHandle, + pub vary_rule_ptr: *const u8, + pub vary_rule_len: usize, + pub initial_age_ns: u64, + pub stale_while_revalidate_ns: u64, + pub surrogate_keys_ptr: *const u8, + pub surrogate_keys_len: usize, + pub length: CacheObjectLength, + pub user_metadata_ptr: *const u8, + pub user_metadata_len: usize, +} + +bitflags::bitflags! { + #[repr(transparent)] + pub struct CacheWriteOptionsMask: u32 { + const _RESERVED = 1 << 0; + const REQUEST_HEADERS = 1 << 1; + const VARY_RULE = 1 << 2; + const INITIAL_AGE_NS = 1 << 3; + const STALE_WHILE_REVALIDATE_NS = 1 << 4; + const SURROGATE_KEYS = 1 << 5; + const LENGTH = 1 << 6; + const USER_METADATA = 1 << 7; + const SENSITIVE_DATA = 1 << 8; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct CacheGetBodyOptions { + pub from: u64, + pub to: u64, +} + +bitflags::bitflags! { + #[repr(transparent)] + pub struct CacheGetBodyOptionsMask: u32 { + const _RESERVED = 1 << 0; + const FROM = 1 << 1; + const TO = 1 << 2; + } +} + +bitflags::bitflags! { + #[repr(transparent)] + pub struct CacheLookupState: u32 { + const FOUND = 1 << 0; + const USABLE = 1 << 1; + const STALE = 1 << 2; + const MUST_INSERT_OR_UPDATE = 1 << 3; + } +} + +mod cache { + use super::*; + use crate::bindings::fastly::api::cache; + use core::slice; + + impl From for cache::LookupOptionsMask { + fn from(value: CacheLookupOptionsMask) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::REQUEST_HEADERS, + value.contains(CacheLookupOptionsMask::REQUEST_HEADERS), + ); + flags + } + } + + impl From for cache::LookupOptions { + fn from(value: CacheLookupOptions) -> Self { + Self { + request_headers: value.request_headers, + } + } + } + + impl From for cache::WriteOptionsMask { + fn from(value: CacheWriteOptionsMask) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::RESERVED, + value.contains(CacheWriteOptionsMask::_RESERVED), + ); + flags.set( + Self::REQUEST_HEADERS, + value.contains(CacheWriteOptionsMask::REQUEST_HEADERS), + ); + flags.set( + Self::VARY_RULE, + value.contains(CacheWriteOptionsMask::VARY_RULE), + ); + flags.set( + Self::INITIAL_AGE_NS, + value.contains(CacheWriteOptionsMask::INITIAL_AGE_NS), + ); + flags.set( + Self::STALE_WHILE_REVALIDATE_NS, + value.contains(CacheWriteOptionsMask::STALE_WHILE_REVALIDATE_NS), + ); + flags.set( + Self::SURROGATE_KEYS, + value.contains(CacheWriteOptionsMask::SURROGATE_KEYS), + ); + flags.set(Self::LENGTH, value.contains(CacheWriteOptionsMask::LENGTH)); + flags.set( + Self::USER_METADATA, + value.contains(CacheWriteOptionsMask::USER_METADATA), + ); + flags.set( + Self::SENSITIVE_DATA, + value.contains(CacheWriteOptionsMask::SENSITIVE_DATA), + ); + flags + } + } + + impl From for CacheLookupState { + fn from(value: cache::LookupState) -> Self { + let mut flags = Self::empty(); + flags.set(Self::FOUND, value.contains(cache::LookupState::FOUND)); + flags.set(Self::USABLE, value.contains(cache::LookupState::USABLE)); + flags.set(Self::STALE, value.contains(cache::LookupState::STALE)); + flags.set( + Self::MUST_INSERT_OR_UPDATE, + value.contains(cache::LookupState::MUST_INSERT_OR_UPDATE), + ); + flags + } + } + + #[export_name = "fastly_cache#lookup"] + pub fn lookup( + cache_key_ptr: *const u8, + cache_key_len: usize, + options_mask: CacheLookupOptionsMask, + options: *const CacheLookupOptions, + cache_handle_out: *mut CacheHandle, + ) -> FastlyStatus { + let cache_key = unsafe { slice::from_raw_parts(cache_key_ptr, cache_key_len) }; + let options_mask = cache::LookupOptionsMask::from(options_mask); + let options = unsafe { cache::LookupOptions::from(*options) }; + match cache::lookup(cache_key, options_mask, options) { + Ok(res) => { + unsafe { + *cache_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + unsafe fn write_options(options: *const CacheWriteOptions) -> cache::WriteOptions { + // NOTE: this is only really safe because we never mutate the vectors -- we only need + // vectors to satisfy the interface produced by the DynamicBackendConfig record, + // `register_dynamic_backend` will never mutate the vectors it's given. + macro_rules! make_vec { + ($ptr_field:ident, $len_field:ident) => {{ + let len = usize::try_from((*options).$len_field).trapping_unwrap(); + Vec::from_raw_parts((*options).$ptr_field as *mut _, len, len) + }}; + } + + cache::WriteOptions { + max_age_ns: (*options).max_age_ns, + request_headers: (*options).request_headers, + vary_rule: make_vec!(vary_rule_ptr, vary_rule_len), + initial_age_ns: (*options).initial_age_ns, + stale_while_revalidate_ns: (*options).stale_while_revalidate_ns, + surrogate_keys: make_vec!(surrogate_keys_ptr, surrogate_keys_len), + length: (*options).length, + user_metadata: make_vec!(user_metadata_ptr, user_metadata_len), + } + } + + #[export_name = "fastly_cache#insert"] + pub fn insert( + cache_key_ptr: *const u8, + cache_key_len: usize, + options_mask: CacheWriteOptionsMask, + options: *const CacheWriteOptions, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let cache_key = unsafe { slice::from_raw_parts(cache_key_ptr, cache_key_len) }; + let options_mask = cache::WriteOptionsMask::from(options_mask); + + let options = unsafe { write_options(options) }; + + let res = cache::insert(cache_key, options_mask, &options); + + std::mem::forget(options); + + match res { + Ok(res) => { + unsafe { + *body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#transaction_lookup"] + pub fn transaction_lookup( + cache_key_ptr: *const u8, + cache_key_len: usize, + options_mask: CacheLookupOptionsMask, + options: *const CacheLookupOptions, + cache_handle_out: *mut CacheHandle, + ) -> FastlyStatus { + let cache_key = unsafe { slice::from_raw_parts(cache_key_ptr, cache_key_len) }; + let options_mask = cache::LookupOptionsMask::from(options_mask); + let options = unsafe { cache::LookupOptions::from(*options) }; + match cache::transaction_lookup(cache_key, options_mask, options) { + Ok(res) => { + unsafe { + *cache_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#transaction_insert"] + pub fn transaction_insert( + handle: CacheHandle, + options_mask: CacheWriteOptionsMask, + options: *const CacheWriteOptions, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let options_mask = cache::WriteOptionsMask::from(options_mask); + let options = unsafe { write_options(options) }; + let res = cache::transaction_insert(handle, options_mask, &options); + + std::mem::forget(options); + + match res { + Ok(res) => { + unsafe { + *body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#transaction_insert_and_stream_back"] + pub fn transaction_insert_and_stream_back( + handle: CacheHandle, + options_mask: CacheWriteOptionsMask, + options: *const CacheWriteOptions, + body_handle_out: *mut BodyHandle, + cache_handle_out: *mut CacheHandle, + ) -> FastlyStatus { + let options_mask = cache::WriteOptionsMask::from(options_mask); + let options = unsafe { write_options(options) }; + let res = cache::transaction_insert_and_stream_back(handle, options_mask, &options); + std::mem::forget(options); + match res { + Ok((body_handle, cache_handle)) => { + unsafe { + *body_handle_out = body_handle; + *cache_handle_out = cache_handle; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#transaction_update"] + pub fn transaction_update( + handle: CacheHandle, + options_mask: CacheWriteOptionsMask, + options: *const CacheWriteOptions, + ) -> FastlyStatus { + let options_mask = cache::WriteOptionsMask::from(options_mask); + let options = unsafe { write_options(options) }; + let res = cache::transaction_update(handle, options_mask, &options); + std::mem::forget(options); + convert_result(res) + } + + #[export_name = "fastly_cache#transaction_cancel"] + pub fn transaction_cancel(handle: CacheHandle) -> FastlyStatus { + convert_result(cache::transaction_cancel(handle)) + } + + #[export_name = "fastly_cache#close"] + pub fn close(handle: CacheHandle) -> FastlyStatus { + convert_result(cache::close(handle)) + } + + #[export_name = "fastly_cache#get_state"] + pub fn get_state( + handle: CacheHandle, + cache_lookup_state_out: *mut CacheLookupState, + ) -> FastlyStatus { + match cache::get_state(handle) { + Ok(res) => { + unsafe { + *cache_lookup_state_out = res.into(); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_user_metadata"] + pub fn get_user_metadata( + handle: CacheHandle, + user_metadata_out_ptr: *mut u8, + user_metadata_out_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + alloc_result!( + user_metadata_out_ptr, + user_metadata_out_len, + nwritten_out, + { cache::get_user_metadata(handle) } + ) + } + + impl From for cache::GetBodyOptionsMask { + fn from(value: CacheGetBodyOptionsMask) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::RESERVED, + value.contains(CacheGetBodyOptionsMask::_RESERVED), + ); + flags.set(Self::FROM, value.contains(CacheGetBodyOptionsMask::FROM)); + flags.set(Self::TO, value.contains(CacheGetBodyOptionsMask::TO)); + flags + } + } + + impl From for cache::GetBodyOptions { + fn from(value: CacheGetBodyOptions) -> Self { + Self { + from: value.from, + to: value.to, + } + } + } + + #[export_name = "fastly_cache#get_body"] + pub fn get_body( + handle: CacheHandle, + options_mask: CacheGetBodyOptionsMask, + options: *const CacheGetBodyOptions, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let options_mask = cache::GetBodyOptionsMask::from(options_mask); + let options = unsafe { cache::GetBodyOptions::from(*options) }; + match cache::get_body(handle, options_mask, options) { + Ok(res) => { + unsafe { + *body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_length"] + pub fn get_length(handle: CacheHandle, length_out: *mut CacheObjectLength) -> FastlyStatus { + match cache::get_length(handle) { + Ok(res) => { + unsafe { + *length_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_max_age_ns"] + pub fn get_max_age_ns(handle: CacheHandle, duration_out: *mut CacheDurationNs) -> FastlyStatus { + match cache::get_max_age_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_stale_while_revalidate_ns"] + pub fn get_stale_while_revalidate_ns( + handle: CacheHandle, + duration_out: *mut CacheDurationNs, + ) -> FastlyStatus { + match cache::get_stale_while_revalidate_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_age_ns"] + pub fn get_age_ns(handle: CacheHandle, duration_out: *mut CacheDurationNs) -> FastlyStatus { + match cache::get_age_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#get_hits"] + pub fn get_hits(handle: CacheHandle, hits_out: *mut CacheHitCount) -> FastlyStatus { + match cache::get_hits(handle) { + Ok(res) => { + unsafe { + *hits_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} diff --git a/crates/adapter/src/fastly/config_store.rs b/crates/adapter/src/fastly/config_store.rs new file mode 100644 index 00000000..5db9ddbc --- /dev/null +++ b/crates/adapter/src/fastly/config_store.rs @@ -0,0 +1,53 @@ +use super::FastlyStatus; +use crate::{bindings::fastly::api::config_store, with_buffer, TrappingUnwrap}; +use core::slice; + +pub type ConfigStoreHandle = u32; + +#[export_name = "fastly_config_store#open"] +pub fn open( + name: *const u8, + name_len: usize, + store_handle_out: *mut ConfigStoreHandle, +) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + match config_store::open(name) { + Ok(res) => { + unsafe { + *store_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } +} + +#[export_name = "fastly_config_store#get"] +pub fn get( + store_handle: ConfigStoreHandle, + key: *const u8, + key_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, +) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key, key_len) }; + with_buffer!( + value, + value_max_len, + { + config_store::get( + store_handle, + key, + u64::try_from(value_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res) + } + ) +} diff --git a/crates/adapter/src/fastly/core.rs b/crates/adapter/src/fastly/core.rs new file mode 100644 index 00000000..375529e4 --- /dev/null +++ b/crates/adapter/src/fastly/core.rs @@ -0,0 +1,2808 @@ +// The following type aliases are used for readability of definitions in this module. They should +// not be confused with types of similar names in the `fastly` crate which are used to provide safe +// wrappers around these definitions. + +use super::{convert_result, FastlyStatus}; +use crate::{alloc_result, with_buffer, TrappingUnwrap}; + +impl From for u32 { + fn from(value: crate::bindings::fastly::api::http_types::HttpVersion) -> Self { + use crate::bindings::fastly::api::http_types::HttpVersion; + match value { + HttpVersion::Http09 => 0, + HttpVersion::Http10 => 1, + HttpVersion::Http11 => 2, + HttpVersion::H2 => 3, + HttpVersion::H3 => 4, + } + } +} + +impl TryFrom for crate::bindings::fastly::api::http_types::HttpVersion { + type Error = u32; + + fn try_from(value: u32) -> Result { + use crate::bindings::fastly::api::http_types::HttpVersion; + match value { + 0 => Ok(HttpVersion::Http09), + 1 => Ok(HttpVersion::Http10), + 2 => Ok(HttpVersion::Http11), + 3 => Ok(HttpVersion::H2), + 4 => Ok(HttpVersion::H3), + _ => Err(value), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u32)] +pub enum BodyWriteEnd { + Back = 0, + Front = 1, +} + +/// Determines how the framing headers (`Content-Length`/`Transfer-Encoding`) are set for a +/// request or response. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u32)] +pub enum FramingHeadersMode { + /// Determine the framing headers automatically based on the message body, and discard any framing + /// headers already set in the message. This is the default behavior. + /// + /// In automatic mode, a `Content-Length` is used when the size of the body can be determined + /// before it is sent. Requests/responses sent in streaming mode, where headers are sent immediately + /// but the content of the body is streamed later, will receive a `Transfer-Encoding: chunked` + /// to accommodate the dynamic generation of the body. + Automatic = 0, + + /// Use the exact framing headers set in the message, falling back to [`Automatic`][`Self::Automatic`] + /// if invalid. + /// + /// In "from headers" mode, any `Content-Length` or `Transfer-Encoding` headers will be honored. + /// You must ensure that those headers have correct values permitted by the + /// [HTTP/1.1 specification][spec]. If the provided headers are not permitted by the spec, + /// the headers will revert to automatic mode and a log diagnostic will be issued about what was + /// wrong. If a `Content-Length` is permitted by the spec, but the value doesn't match the size of + /// the actual body, the body will either be truncated (if it is too long), or the connection will + /// be hung up early (if it is too short). + /// + /// [spec]: https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1 + ManuallyFromHeaders = 1, +} + +/// Determines whether the client is encouraged to stop using the current connection and to open a +/// new one for the next request. +/// +/// Most applications do not need to change this setting. +#[doc(hidden)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u32)] +pub enum HttpKeepaliveMode { + /// This is the default behavor. + Automatic = 0, + + /// Send `Connection: close` in HTTP/1 and a GOAWAY frame in HTTP/2 and HTTP/3. This prompts + /// the client to close the current connection and to open a new one for the next request. + NoKeepalive = 1, +} + +pub type PendingObjectStoreLookupHandle = u32; +pub type PendingObjectStoreInsertHandle = u32; +pub type PendingObjectStoreDeleteHandle = u32; +pub type BodyHandle = u32; +pub type PendingRequestHandle = u32; +pub type RequestHandle = u32; +pub type ResponseHandle = u32; +pub type DictionaryHandle = u32; +pub type KVStoreHandle = u32; +pub type SecretStoreHandle = u32; +pub type SecretHandle = u32; +pub type AsyncItemHandle = u32; + +const INVALID_HANDLE: u32 = u32::MAX - 1; + +#[repr(C)] +pub struct DynamicBackendConfig { + pub host_override: *const u8, + pub host_override_len: u32, + pub connect_timeout_ms: u32, + pub first_byte_timeout_ms: u32, + pub between_bytes_timeout_ms: u32, + pub ssl_min_version: u32, + pub ssl_max_version: u32, + pub cert_hostname: *const u8, + pub cert_hostname_len: u32, + pub ca_cert: *const u8, + pub ca_cert_len: u32, + pub ciphers: *const u8, + pub ciphers_len: u32, + pub sni_hostname: *const u8, + pub sni_hostname_len: u32, + pub client_certificate: *const u8, + pub client_certificate_len: u32, + pub client_key: SecretHandle, +} + +impl Default for DynamicBackendConfig { + fn default() -> Self { + DynamicBackendConfig { + host_override: std::ptr::null(), + host_override_len: 0, + connect_timeout_ms: 0, + first_byte_timeout_ms: 0, + between_bytes_timeout_ms: 0, + ssl_min_version: 0, + ssl_max_version: 0, + cert_hostname: std::ptr::null(), + cert_hostname_len: 0, + ca_cert: std::ptr::null(), + ca_cert_len: 0, + ciphers: std::ptr::null(), + ciphers_len: 0, + sni_hostname: std::ptr::null(), + sni_hostname_len: 0, + client_certificate: std::ptr::null(), + client_certificate_len: 0, + client_key: 0, + } + } +} + +bitflags::bitflags! { + /// `Content-Encoding` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct ContentEncodings: u32 { + const GZIP = 1 << 0; + } +} + +impl From for crate::bindings::fastly::api::http_req::ContentEncodings { + fn from(value: ContentEncodings) -> Self { + let mut flags = Self::empty(); + flags.set(Self::GZIP, value.contains(ContentEncodings::GZIP)); + flags + } +} + +bitflags::bitflags! { + /// `BackendConfigOptions` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct BackendConfigOptions: u32 { + const RESERVED = 1 << 0; + const HOST_OVERRIDE = 1 << 1; + const CONNECT_TIMEOUT = 1 << 2; + const FIRST_BYTE_TIMEOUT = 1 << 3; + const BETWEEN_BYTES_TIMEOUT = 1 << 4; + const USE_SSL = 1 << 5; + const SSL_MIN_VERSION = 1 << 6; + const SSL_MAX_VERSION = 1 << 7; + const CERT_HOSTNAME = 1 << 8; + const CA_CERT = 1 << 9; + const CIPHERS = 1 << 10; + const SNI_HOSTNAME = 1 << 11; + const DONT_POOL = 1 << 12; + const CLIENT_CERT = 1 << 13; + const GRPC = 1 << 14; + } +} + +impl From for crate::bindings::fastly::api::http_types::BackendConfigOptions { + fn from(options: BackendConfigOptions) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::RESERVED, + options.contains(BackendConfigOptions::RESERVED), + ); + flags.set( + Self::HOST_OVERRIDE, + options.contains(BackendConfigOptions::HOST_OVERRIDE), + ); + flags.set( + Self::CONNECT_TIMEOUT, + options.contains(BackendConfigOptions::CONNECT_TIMEOUT), + ); + flags.set( + Self::FIRST_BYTE_TIMEOUT, + options.contains(BackendConfigOptions::FIRST_BYTE_TIMEOUT), + ); + flags.set( + Self::BETWEEN_BYTES_TIMEOUT, + options.contains(BackendConfigOptions::BETWEEN_BYTES_TIMEOUT), + ); + flags.set( + Self::USE_SSL, + options.contains(BackendConfigOptions::USE_SSL), + ); + flags.set( + Self::SSL_MIN_VERSION, + options.contains(BackendConfigOptions::SSL_MIN_VERSION), + ); + flags.set( + Self::SSL_MAX_VERSION, + options.contains(BackendConfigOptions::SSL_MAX_VERSION), + ); + flags.set( + Self::CERT_HOSTNAME, + options.contains(BackendConfigOptions::CERT_HOSTNAME), + ); + flags.set( + Self::CA_CERT, + options.contains(BackendConfigOptions::CA_CERT), + ); + flags.set( + Self::CIPHERS, + options.contains(BackendConfigOptions::CIPHERS), + ); + flags.set( + Self::SNI_HOSTNAME, + options.contains(BackendConfigOptions::SNI_HOSTNAME), + ); + flags.set( + Self::DONT_POOL, + options.contains(BackendConfigOptions::DONT_POOL), + ); + flags.set( + Self::CLIENT_CERT, + options.contains(BackendConfigOptions::CLIENT_CERT), + ); + flags.set(Self::GRPC, options.contains(BackendConfigOptions::GRPC)); + flags + } +} + +pub mod fastly_abi { + use super::*; + + pub const ABI_VERSION: u64 = 1; + + #[export_name = "fastly_abi#init"] + /// Tell the runtime what ABI version this program is using (FASTLY_ABI_VERSION) + pub fn init(abi_version: u64) -> FastlyStatus { + if abi_version != ABI_VERSION { + FastlyStatus::UNKNOWN_ERROR + } else { + FastlyStatus::OK + } + } +} + +pub mod fastly_uap { + use super::*; + use crate::bindings::fastly::api::uap; + use core::slice; + + #[export_name = "fastly_uap#parse"] + pub fn parse( + user_agent: *const u8, + user_agent_max_len: usize, + family: *mut u8, + family_max_len: usize, + family_written: *mut usize, + major: *mut u8, + major_max_len: usize, + major_written: *mut usize, + minor: *mut u8, + minor_max_len: usize, + minor_written: *mut usize, + patch: *mut u8, + patch_max_len: usize, + patch_written: *mut usize, + ) -> FastlyStatus { + let user_agent = unsafe { slice::from_raw_parts(user_agent, user_agent_max_len) }; + let ua = match uap::parse(user_agent) { + Ok(ua) => ua, + Err(e) => return e.into(), + }; + + alloc_result!(family, family_max_len, family_written, { + ua.family(u64::try_from(family_max_len).trapping_unwrap()) + }); + + alloc_result!(major, major_max_len, major_written, { + ua.major(u64::try_from(major_max_len).trapping_unwrap()) + }); + + alloc_result!(minor, minor_max_len, minor_written, { + ua.minor(u64::try_from(minor_max_len).trapping_unwrap()) + }); + + alloc_result!(patch, patch_max_len, patch_written, { + ua.patch(u64::try_from(patch_max_len).trapping_unwrap()) + }); + + FastlyStatus::OK + } +} + +pub mod fastly_http_body { + use super::*; + use crate::bindings::fastly::api::http_body; + use core::slice; + + #[export_name = "fastly_http_body#append"] + pub fn append(dst_handle: BodyHandle, src_handle: BodyHandle) -> FastlyStatus { + convert_result(http_body::append(dst_handle, src_handle)) + } + + #[export_name = "fastly_http_body#new"] + pub fn new(handle_out: *mut BodyHandle) -> FastlyStatus { + match http_body::new() { + Ok(handle) => { + unsafe { + *handle_out = handle; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_body#read"] + pub fn read( + body_handle: BodyHandle, + buf: *mut u8, + buf_len: usize, + nread_out: *mut usize, + ) -> FastlyStatus { + alloc_result!(buf, buf_len, nread_out, { + http_body::read(body_handle, u32::try_from(buf_len).trapping_unwrap()) + }) + } + + // overeager warning for extern declarations is a rustc bug: https://github.com/rust-lang/rust/issues/79581 + #[allow(clashing_extern_declarations)] + #[export_name = "fastly_http_body#write"] + pub fn write( + body_handle: BodyHandle, + buf: *const u8, + buf_len: usize, + end: BodyWriteEnd, + nwritten_out: *mut usize, + ) -> FastlyStatus { + let end = match end { + BodyWriteEnd::Back => http_body::WriteEnd::Back, + BodyWriteEnd::Front => http_body::WriteEnd::Front, + }; + match http_body::write( + body_handle, + unsafe { slice::from_raw_parts(buf, buf_len) }, + end, + ) { + Ok(len) => { + unsafe { + *nwritten_out = usize::try_from(len).trapping_unwrap(); + } + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + /// Close a body, freeing its resources and causing any sends to finish. + #[export_name = "fastly_http_body#close"] + pub fn close(body_handle: BodyHandle) -> FastlyStatus { + convert_result(http_body::close(body_handle)) + } + + #[export_name = "fastly_http_body#trailer_append"] + pub fn trailer_append( + body_handle: BodyHandle, + name: *const u8, + name_len: usize, + value: *const u8, + value_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let value = unsafe { slice::from_raw_parts(value, value_len) }; + convert_result(http_body::trailer_append(body_handle, name, value)) + } + + #[export_name = "fastly_http_body#trailer_names_get"] + pub fn trailer_names_get( + body_handle: BodyHandle, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + with_buffer!( + buf, + buf_len, + { + http_body::trailer_names_get( + body_handle, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_body#trailer_value_get"] + pub fn trailer_value_get( + body_handle: BodyHandle, + name: *const u8, + name_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + with_buffer!( + value, + value_max_len, + { + http_body::trailer_value_get( + body_handle, + name, + u64::try_from(value_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res); + } + ) + } + + #[export_name = "fastly_http_body#trailer_values_get"] + pub fn trailer_values_get( + body_handle: BodyHandle, + name: *const u8, + name_len: usize, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + with_buffer!( + buf, + buf_len, + { + http_body::trailer_values_get( + body_handle, + name, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_body#known_length"] + pub fn known_length(body_handle: BodyHandle, length_out: *mut u64) -> FastlyStatus { + match http_body::known_length(body_handle) { + Ok(len) => { + unsafe { + *length_out = len; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_log { + use core::slice; + + use super::*; + use crate::bindings::fastly; + + #[export_name = "fastly_log#endpoint_get"] + pub fn endpoint_get( + name: *const u8, + name_len: usize, + endpoint_handle_out: *mut u32, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + match fastly::api::log::endpoint_get(name) { + Ok(res) => { + unsafe { + *endpoint_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + // overeager warning for extern declarations is a rustc bug: https://github.com/rust-lang/rust/issues/79581 + #[allow(clashing_extern_declarations)] + #[export_name = "fastly_log#write"] + pub fn write( + endpoint_handle: u32, + msg: *const u8, + msg_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + let msg = unsafe { slice::from_raw_parts(msg, msg_len) }; + match fastly::api::log::write(endpoint_handle, msg) { + Ok(res) => { + unsafe { + *nwritten_out = usize::try_from(res).trapping_unwrap(); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_http_req { + use core::slice; + + use super::*; + use crate::{ + bindings::fastly::{ + self, + api::{http_req, http_types}, + }, + TrappingUnwrap, + }; + + bitflags::bitflags! { + #[derive(Default, Clone, Debug, PartialEq, Eq)] + #[repr(transparent)] + pub struct SendErrorDetailMask: u32 { + const RESERVED = 1 << 0; + const DNS_ERROR_RCODE = 1 << 1; + const DNS_ERROR_INFO_CODE = 1 << 2; + const TLS_ALERT_ID = 1 << 3; + } + } + + #[repr(u32)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum SendErrorDetailTag { + Uninitialized, + Ok, + DnsTimeout, + DnsError, + DestinationNotFound, + DestinationUnavailable, + DestinationIpUnroutable, + ConnectionRefused, + ConnectionTerminated, + ConnectionTimeout, + ConnectionLimitReached, + TlsCertificateError, + TlsConfigurationError, + HttpIncompleteResponse, + HttpResponseHeaderSectionTooLarge, + HttpResponseBodyTooLarge, + HttpResponseTimeout, + HttpResponseStatusInvalid, + HttpUpgradeFailed, + HttpProtocolError, + HttpRequestCacheKeyInvalid, + HttpRequestUriInvalid, + InternalError, + TlsAlertReceived, + TlsProtocolError, + } + + #[repr(C)] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct SendErrorDetail { + pub tag: SendErrorDetailTag, + pub mask: SendErrorDetailMask, + pub dns_error_rcode: u16, + pub dns_error_info_code: u16, + pub tls_alert_id: u8, + } + + impl SendErrorDetail { + pub fn uninitialized_all() -> Self { + Self { + tag: SendErrorDetailTag::Uninitialized, + mask: SendErrorDetailMask::all(), + dns_error_rcode: Default::default(), + dns_error_info_code: Default::default(), + tls_alert_id: Default::default(), + } + } + } + + impl From for fastly::api::http_req::CacheOverrideTag { + fn from(tag: u32) -> Self { + let flag_present = |n: u32| tag & (1 << n) != 0; + + let mut flags = Self::empty(); + flags.set(Self::PASS, flag_present(0)); + flags.set(Self::TTL, flag_present(1)); + flags.set(Self::STALE_WHILE_REVALIDATE, flag_present(2)); + flags.set(Self::PCI, flag_present(3)); + flags + } + } + + impl Into for fastly::api::http_req::ClientCertVerifyResult { + fn into(self) -> u32 { + use fastly::api::http_req::ClientCertVerifyResult; + match self { + ClientCertVerifyResult::Ok => 0, + ClientCertVerifyResult::BadCertificate => 1, + ClientCertVerifyResult::CertificateRevoked => 2, + ClientCertVerifyResult::CertificateExpired => 3, + ClientCertVerifyResult::UnknownCa => 4, + ClientCertVerifyResult::CertificateMissing => 5, + ClientCertVerifyResult::CertificateUnknown => 6, + } + } + } + + impl Into for fastly::api::http_req::SendErrorDetailTag { + fn into(self) -> SendErrorDetailTag { + match self { + http_req::SendErrorDetailTag::Uninitialized => SendErrorDetailTag::Uninitialized, + http_req::SendErrorDetailTag::Ok => SendErrorDetailTag::Ok, + http_req::SendErrorDetailTag::DnsTimeout => SendErrorDetailTag::DnsTimeout, + http_req::SendErrorDetailTag::DnsError => SendErrorDetailTag::DnsError, + http_req::SendErrorDetailTag::DestinationNotFound => { + SendErrorDetailTag::DestinationNotFound + } + http_req::SendErrorDetailTag::DestinationUnavailable => { + SendErrorDetailTag::DestinationUnavailable + } + http_req::SendErrorDetailTag::DestinationIpUnroutable => { + SendErrorDetailTag::DestinationIpUnroutable + } + http_req::SendErrorDetailTag::ConnectionRefused => { + SendErrorDetailTag::ConnectionRefused + } + http_req::SendErrorDetailTag::ConnectionTerminated => { + SendErrorDetailTag::ConnectionTerminated + } + http_req::SendErrorDetailTag::ConnectionTimeout => { + SendErrorDetailTag::ConnectionTimeout + } + http_req::SendErrorDetailTag::ConnectionLimitReached => { + SendErrorDetailTag::ConnectionLimitReached + } + http_req::SendErrorDetailTag::TlsCertificateError => { + SendErrorDetailTag::TlsCertificateError + } + http_req::SendErrorDetailTag::TlsConfigurationError => { + SendErrorDetailTag::TlsConfigurationError + } + http_req::SendErrorDetailTag::HttpIncompleteResponse => { + SendErrorDetailTag::HttpIncompleteResponse + } + http_req::SendErrorDetailTag::HttpResponseHeaderSectionTooLarge => { + SendErrorDetailTag::HttpResponseHeaderSectionTooLarge + } + http_req::SendErrorDetailTag::HttpResponseBodyTooLarge => { + SendErrorDetailTag::HttpResponseBodyTooLarge + } + http_req::SendErrorDetailTag::HttpResponseTimeout => { + SendErrorDetailTag::HttpResponseTimeout + } + http_req::SendErrorDetailTag::HttpResponseStatusInvalid => { + SendErrorDetailTag::HttpResponseStatusInvalid + } + http_req::SendErrorDetailTag::HttpUpgradeFailed => { + SendErrorDetailTag::HttpUpgradeFailed + } + http_req::SendErrorDetailTag::HttpProtocolError => { + SendErrorDetailTag::HttpProtocolError + } + http_req::SendErrorDetailTag::HttpRequestCacheKeyInvalid => { + SendErrorDetailTag::HttpRequestCacheKeyInvalid + } + http_req::SendErrorDetailTag::HttpRequestUriInvalid => { + SendErrorDetailTag::HttpRequestUriInvalid + } + http_req::SendErrorDetailTag::InternalError => SendErrorDetailTag::InternalError, + http_req::SendErrorDetailTag::TlsAlertReceived => { + SendErrorDetailTag::TlsAlertReceived + } + http_req::SendErrorDetailTag::TlsProtocolError => { + SendErrorDetailTag::TlsProtocolError + } + } + } + } + + impl Into for http_req::SendErrorDetailMask { + fn into(self) -> SendErrorDetailMask { + let mut flags = SendErrorDetailMask::empty(); + flags.set( + SendErrorDetailMask::RESERVED, + self.contains(http_req::SendErrorDetailMask::RESERVED), + ); + flags.set( + SendErrorDetailMask::DNS_ERROR_RCODE, + self.contains(http_req::SendErrorDetailMask::DNS_ERROR_RCODE), + ); + flags.set( + SendErrorDetailMask::DNS_ERROR_INFO_CODE, + self.contains(http_req::SendErrorDetailMask::DNS_ERROR_INFO_CODE), + ); + flags.set( + SendErrorDetailMask::TLS_ALERT_ID, + self.contains(http_req::SendErrorDetailMask::TLS_ALERT_ID), + ); + flags + } + } + + impl Default for http_req::SendErrorDetail { + fn default() -> Self { + Self { + tag: http_req::SendErrorDetailTag::Uninitialized, + mask: http_req::SendErrorDetailMask::empty(), + dns_error_rcode: Default::default(), + dns_error_info_code: Default::default(), + tls_alert_id: Default::default(), + } + } + } + + impl Into for http_req::SendErrorDetail { + fn into(self) -> SendErrorDetail { + SendErrorDetail { + tag: self.tag.into(), + mask: self.mask.into(), + dns_error_rcode: self.dns_error_rcode, + dns_error_info_code: self.dns_error_info_code, + tls_alert_id: self.tls_alert_id, + } + } + } + + impl From for http_req::SendErrorDetail { + fn from(tag: http_req::SendErrorDetailTag) -> Self { + Self { + tag, + ..Self::default() + } + } + } + + impl From for SendErrorDetail { + fn from(tag: http_req::SendErrorDetailTag) -> Self { + http_req::SendErrorDetail::from(tag).into() + } + } + + #[export_name = "fastly_http_req#body_downstream_get"] + pub fn body_downstream_get( + req_handle_out: *mut RequestHandle, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + crate::State::with::(|state| { + unsafe { + *req_handle_out = state.request.get().trapping_unwrap(); + *body_handle_out = state.request_body.get().trapping_unwrap(); + } + Ok(()) + }) + } + + #[export_name = "fastly_http_req#cache_override_set"] + pub fn cache_override_set( + req_handle: RequestHandle, + tag: u32, + ttl: u32, + swr: u32, + ) -> FastlyStatus { + use fastly::api::http_req::CacheOverrideTag; + let tag = CacheOverrideTag::from(tag); + convert_result(fastly::api::http_req::cache_override_set( + req_handle, tag, ttl, swr, + )) + } + + #[export_name = "fastly_http_req#cache_override_v2_set"] + pub fn cache_override_v2_set( + req_handle: RequestHandle, + tag: u32, + ttl: u32, + swr: u32, + sk: *const u8, + sk_len: usize, + ) -> FastlyStatus { + use fastly::api::http_req::CacheOverrideTag; + let tag = CacheOverrideTag::from(tag); + let sk = unsafe { slice::from_raw_parts(sk, sk_len) }; + convert_result(fastly::api::http_req::cache_override_v2_set( + req_handle, + tag, + ttl, + swr, + (!sk.is_empty()).then_some(sk), + )) + } + + #[export_name = "fastly_http_req#framing_headers_mode_set"] + pub fn framing_headers_mode_set( + req_handle: RequestHandle, + mode: FramingHeadersMode, + ) -> FastlyStatus { + let mode = match mode { + FramingHeadersMode::Automatic => fastly::api::http_types::FramingHeadersMode::Automatic, + FramingHeadersMode::ManuallyFromHeaders => { + fastly::api::http_types::FramingHeadersMode::ManuallyFromHeaders + } + }; + + convert_result(fastly::api::http_req::framing_headers_mode_set( + req_handle, mode, + )) + } + + #[export_name = "fastly_http_req#downstream_client_ip_addr"] + pub fn downstream_client_ip_addr( + addr_octets_out: *mut u8, + nwritten_out: *mut usize, + ) -> FastlyStatus { + alloc_result!(addr_octets_out, 16, nwritten_out, { + fastly::api::http_req::downstream_client_ip_addr() + }) + } + + #[export_name = "fastly_http_req#downstream_server_ip_addr"] + pub fn downstream_server_ip_addr( + addr_octets_out: *mut u8, + nwritten_out: *mut usize, + ) -> FastlyStatus { + alloc_result!(addr_octets_out, 16, nwritten_out, { + fastly::api::http_req::downstream_server_ip_addr() + }) + } + + #[export_name = "fastly_http_req#downstream_client_h2_fingerprint"] + pub fn downstream_client_h2_fingerprint( + h2fp_out: *mut u8, + h2fp_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(h2fp_out, h2fp_max_len, nwritten, { + fastly::api::http_req::downstream_client_h2_fingerprint( + u64::try_from(h2fp_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_client_request_id"] + pub fn downstream_client_request_id( + reqid_out: *mut u8, + reqid_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(reqid_out, reqid_max_len, nwritten, { + fastly::api::http_req::downstream_client_request_id( + u64::try_from(reqid_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_client_oh_fingerprint"] + pub fn downstream_client_oh_fingerprint( + ohfp_out: *mut u8, + ohfp_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(ohfp_out, ohfp_max_len, nwritten, { + fastly::api::http_req::downstream_client_oh_fingerprint( + u64::try_from(ohfp_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_tls_cipher_openssl_name"] + pub fn downstream_tls_cipher_openssl_name( + cipher_out: *mut u8, + cipher_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(cipher_out, cipher_max_len, nwritten, { + fastly::api::http_req::downstream_tls_cipher_openssl_name( + u64::try_from(cipher_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_tls_protocol"] + pub fn downstream_tls_protocol( + protocol_out: *mut u8, + protocol_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(protocol_out, protocol_max_len, nwritten, { + fastly::api::http_req::downstream_tls_protocol( + u64::try_from(protocol_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_tls_client_hello"] + pub fn downstream_tls_client_hello( + client_hello_out: *mut u8, + client_hello_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(client_hello_out, client_hello_max_len, nwritten, { + fastly::api::http_req::downstream_tls_client_hello( + u64::try_from(client_hello_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_tls_ja3_md5"] + pub fn downstream_tls_ja3_md5(ja3_md5_out: *mut u8, nwritten_out: *mut usize) -> FastlyStatus { + alloc_result!(ja3_md5_out, 16, nwritten_out, { + fastly::api::http_req::downstream_tls_ja3_md5() + }) + } + + #[export_name = "fastly_http_req#downstream_tls_ja4"] + pub fn downstream_tls_ja4( + ja4_out: *mut u8, + ja4_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(ja4_out, ja4_max_len, nwritten, { + fastly::api::http_req::downstream_tls_ja4(u64::try_from(ja4_max_len).trapping_unwrap()) + }) + } + + #[export_name = "fastly_http_req#downstream_compliance_region"] + pub fn downstream_compliance_region( + region_out: *mut u8, + region_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(region_out, region_max_len, nwritten, { + fastly::api::http_req::downstream_compliance_region( + u64::try_from(region_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#downstream_tls_raw_client_certificate"] + pub fn downstream_tls_raw_client_certificate( + client_certificate_out: *mut u8, + client_certificate_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!( + client_certificate_out, + client_certificate_max_len, + nwritten, + { + fastly::api::http_req::downstream_tls_raw_client_certificate( + u64::try_from(client_certificate_max_len).trapping_unwrap(), + ) + } + ) + } + + #[export_name = "fastly_http_req#downstream_tls_client_cert_verify_result"] + pub fn downstream_tls_client_cert_verify_result(verify_result_out: *mut u32) -> FastlyStatus { + match fastly::api::http_req::downstream_tls_client_cert_verify_result() { + Ok(res) => { + unsafe { + *verify_result_out = res.into(); + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#header_append"] + pub fn header_append( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + value: *const u8, + value_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let value = unsafe { slice::from_raw_parts(value, value_len) }; + convert_result(fastly::api::http_req::header_append( + req_handle, name, value, + )) + } + + #[export_name = "fastly_http_req#header_insert"] + pub fn header_insert( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + value: *const u8, + value_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let value = unsafe { slice::from_raw_parts(value, value_len) }; + convert_result(fastly::api::http_req::header_insert( + req_handle, name, value, + )) + } + + #[export_name = "fastly_http_req#original_header_names_get"] + pub fn original_header_names_get( + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + with_buffer!( + buf, + buf_len, + { + fastly::api::http_req::original_header_names_get( + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_req#original_header_count"] + pub fn original_header_count(count_out: *mut u32) -> FastlyStatus { + match fastly::api::http_req::original_header_count() { + Ok(count) => { + unsafe { + *count_out = count; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#header_names_get"] + pub fn header_names_get( + req_handle: RequestHandle, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + with_buffer!( + buf, + buf_len, + { + fastly::api::http_req::header_names_get( + req_handle, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_req#header_values_get"] + pub fn header_values_get( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + with_buffer!( + buf, + buf_len, + { + fastly::api::http_req::header_values_get( + req_handle, + unsafe { slice::from_raw_parts(name, name_len) }, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_req#header_values_set"] + pub fn header_values_set( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + values: *const u8, + values_len: usize, + ) -> FastlyStatus { + convert_result(fastly::api::http_req::header_values_set( + req_handle, + unsafe { slice::from_raw_parts(name, name_len) }, + unsafe { slice::from_raw_parts(values, values_len) }, + )) + } + + #[export_name = "fastly_http_req#header_value_get"] + pub fn header_value_get( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + with_buffer!( + value, + value_max_len, + { + fastly::api::http_req::header_value_get( + req_handle, + name, + u64::try_from(value_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res); + } + ) + } + + #[export_name = "fastly_http_req#header_remove"] + pub fn header_remove( + req_handle: RequestHandle, + name: *const u8, + name_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + convert_result(fastly::api::http_req::header_remove(req_handle, name)) + } + + #[export_name = "fastly_http_req#method_get"] + pub fn method_get( + req_handle: RequestHandle, + method: *mut u8, + method_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(method, method_max_len, nwritten, { + fastly::api::http_req::method_get( + req_handle, + u64::try_from(method_max_len).trapping_unwrap(), + ) + }) + } + + #[export_name = "fastly_http_req#method_set"] + pub fn method_set( + req_handle: RequestHandle, + method: *const u8, + method_len: usize, + ) -> FastlyStatus { + let method = unsafe { slice::from_raw_parts(method, method_len) }; + convert_result(fastly::api::http_req::method_set(req_handle, method)) + } + + #[export_name = "fastly_http_req#new"] + pub fn new(req_handle_out: *mut RequestHandle) -> FastlyStatus { + match fastly::api::http_req::new() { + Ok(res) => { + unsafe { + *req_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#send"] + pub fn send( + req_handle: RequestHandle, + body_handle: BodyHandle, + backend: *const u8, + backend_len: usize, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + match fastly::api::http_req::send(req_handle, body_handle, backend) { + Ok((resp_handle, resp_body_handle)) => { + unsafe { + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#send_v2"] + pub fn send_v2( + req_handle: RequestHandle, + body_handle: BodyHandle, + backend: *const u8, + backend_len: usize, + error_detail: *mut SendErrorDetail, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + match fastly::api::http_req::send_v2(req_handle, body_handle, backend) { + Ok((resp_handle, resp_body_handle)) => { + unsafe { + *error_detail = http_req::SendErrorDetailTag::Ok.into(); + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + + FastlyStatus::OK + } + Err((detail, e)) => { + unsafe { + *error_detail = detail + .unwrap_or_else(|| http_req::SendErrorDetailTag::Ok.into()) + .into(); + *resp_handle_out = INVALID_HANDLE; + *resp_body_handle_out = INVALID_HANDLE; + } + + e.into() + } + } + } + + #[export_name = "fastly_http_req#send_async"] + pub fn send_async( + req_handle: RequestHandle, + body_handle: BodyHandle, + backend: *const u8, + backend_len: usize, + pending_req_handle_out: *mut PendingRequestHandle, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + match http_req::send_async(req_handle, body_handle, backend) { + Ok(res) => { + unsafe { + *pending_req_handle_out = res; + } + + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#send_async_streaming"] + pub fn send_async_streaming( + req_handle: RequestHandle, + body_handle: BodyHandle, + backend: *const u8, + backend_len: usize, + pending_req_handle_out: *mut PendingRequestHandle, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + match http_req::send_async_streaming(req_handle, body_handle, backend) { + Ok(res) => { + unsafe { + *pending_req_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#upgrade_websocket"] + pub fn upgrade_websocket(backend: *const u8, backend_len: usize) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + convert_result(http_req::upgrade_websocket(backend)) + } + + #[export_name = "fastly_http_req#redirect_to_websocket_proxy"] + pub fn redirect_to_websocket_proxy(backend: *const u8, backend_len: usize) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + convert_result(http_req::redirect_to_websocket_proxy(backend)) + } + + #[export_name = "fastly_http_req#redirect_to_websocket_proxy_v2"] + pub fn redirect_to_websocket_proxy_v2( + req: RequestHandle, + backend: *const u8, + backend_len: usize, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + convert_result(http_req::redirect_to_websocket_proxy_v2(req, backend)) + } + + #[export_name = "fastly_http_req#redirect_to_grip_proxy"] + pub fn redirect_to_grip_proxy(backend: *const u8, backend_len: usize) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + convert_result(http_req::redirect_to_grip_proxy(backend)) + } + + #[export_name = "fastly_http_req#redirect_to_grip_proxy_v2"] + pub fn redirect_to_grip_proxy_v2( + req: RequestHandle, + backend: *const u8, + backend_len: usize, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend, backend_len) }; + convert_result(http_req::redirect_to_grip_proxy_v2(req, backend)) + } + + #[export_name = "fastly_http_req#register_dynamic_backend"] + pub fn register_dynamic_backend( + name_prefix: *const u8, + name_prefix_len: usize, + target: *const u8, + target_len: usize, + config_mask: BackendConfigOptions, + config: *const DynamicBackendConfig, + ) -> FastlyStatus { + let name_prefix = unsafe { slice::from_raw_parts(name_prefix, name_prefix_len) }; + let target = unsafe { slice::from_raw_parts(target, target_len) }; + + let options = http_types::BackendConfigOptions::from(config_mask); + + // NOTE: this is only really safe because we never mutate the vectors -- we only need + // vectors to satisfy the interface produced by the DynamicBackendConfig record, + // `register_dynamic_backend` will never mutate the vectors it's given. + macro_rules! make_vec { + ($ptr_field:ident, $len_field:ident) => { + unsafe { + let len = usize::try_from((*config).$len_field).trapping_unwrap(); + Vec::from_raw_parts((*config).$ptr_field as *mut _, len, len) + } + }; + } + + let config = http_req::DynamicBackendConfig { + host_override: make_vec!(host_override, host_override_len), + connect_timeout: unsafe { (*config).connect_timeout_ms }, + first_byte_timeout: unsafe { (*config).first_byte_timeout_ms }, + between_bytes_timeout: unsafe { (*config).between_bytes_timeout_ms }, + ssl_min_version: unsafe { (*config).ssl_min_version }.try_into().ok(), + ssl_max_version: unsafe { (*config).ssl_max_version }.try_into().ok(), + cert_hostname: make_vec!(cert_hostname, cert_hostname_len), + ca_cert: make_vec!(ca_cert, ca_cert_len), + ciphers: make_vec!(ciphers, ciphers_len), + sni_hostname: make_vec!(sni_hostname, sni_hostname_len), + client_cert: make_vec!(client_certificate, client_certificate_len), + client_key: unsafe { (*config).client_key }, + }; + + let res = http_req::register_dynamic_backend(name_prefix, target, options, &config); + + std::mem::forget(config); + + convert_result(res) + } + + #[export_name = "fastly_http_req#uri_get"] + pub fn uri_get( + req_handle: RequestHandle, + uri: *mut u8, + uri_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + alloc_result!(uri, uri_max_len, nwritten, { + fastly::api::http_req::uri_get(req_handle, u64::try_from(uri_max_len).trapping_unwrap()) + }) + } + + #[export_name = "fastly_http_req#uri_set"] + pub fn uri_set(req_handle: RequestHandle, uri: *const u8, uri_len: usize) -> FastlyStatus { + let uri = unsafe { slice::from_raw_parts(uri, uri_len) }; + convert_result(http_req::uri_set(req_handle, uri)) + } + + #[export_name = "fastly_http_req#version_get"] + pub fn version_get(req_handle: RequestHandle, version: *mut u32) -> FastlyStatus { + match fastly::api::http_req::version_get(req_handle) { + Ok(res) => { + unsafe { + *version = res.into(); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#version_set"] + pub fn version_set(req_handle: RequestHandle, version: u32) -> FastlyStatus { + match http_types::HttpVersion::try_from(version) { + Ok(version) => convert_result(crate::bindings::fastly::api::http_req::version_set( + req_handle, version, + )), + + Err(_) => FastlyStatus::INVALID_ARGUMENT, + } + } + + #[export_name = "fastly_http_req#pending_req_poll_v2"] + pub fn pending_req_poll_v2( + pending_req_handle: PendingRequestHandle, + error_detail: *mut SendErrorDetail, + is_done_out: *mut i32, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + match http_req::pending_req_poll_v2(pending_req_handle) { + Ok(res) => unsafe { + *error_detail = http_req::SendErrorDetailTag::Ok.into(); + match res { + Some((resp_handle, resp_body_handle)) => { + *is_done_out = 1; + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + + None => { + *is_done_out = 0; + *resp_handle_out = INVALID_HANDLE; + *resp_body_handle_out = INVALID_HANDLE; + } + } + + FastlyStatus::OK + }, + Err((detail, e)) => { + unsafe { + *error_detail = detail + .unwrap_or_else(|| http_req::SendErrorDetailTag::Ok.into()) + .into(); + *is_done_out = 0; + *resp_handle_out = INVALID_HANDLE; + *resp_body_handle_out = INVALID_HANDLE; + } + e.into() + } + } + } + + #[export_name = "fastly_http_req#pending_req_select"] + pub fn pending_req_select( + pending_req_handles: *const PendingRequestHandle, + pending_req_handles_len: usize, + done_index_out: *mut i32, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let pending_req_handles = + unsafe { slice::from_raw_parts(pending_req_handles, pending_req_handles_len) }; + match http_req::pending_req_select(pending_req_handles) { + Ok((idx, (resp_handle, resp_body_handle))) => { + unsafe { + *done_index_out = i32::try_from(idx).trapping_unwrap(); + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#pending_req_select_v2"] + pub fn pending_req_select_v2( + pending_req_handles: *const PendingRequestHandle, + pending_req_handles_len: usize, + error_detail: *mut SendErrorDetail, + done_index_out: *mut i32, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let pending_req_handles = + unsafe { slice::from_raw_parts(pending_req_handles, pending_req_handles_len) }; + match http_req::pending_req_select_v2(pending_req_handles) { + Ok((idx, (resp_handle, resp_body_handle))) => { + unsafe { + *done_index_out = i32::try_from(idx).trapping_unwrap(); + *error_detail = http_req::SendErrorDetailTag::Ok.into(); + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + FastlyStatus::OK + } + Err((detail, e)) => { + unsafe { + *error_detail = detail + .unwrap_or_else(|| http_req::SendErrorDetailTag::Ok.into()) + .into(); + *resp_handle_out = INVALID_HANDLE; + *resp_body_handle_out = INVALID_HANDLE; + } + e.into() + } + } + } + + #[export_name = "fastly_http_req#pending_req_wait_v2"] + pub fn pending_req_wait_v2( + pending_req_handle: PendingRequestHandle, + error_detail: *mut SendErrorDetail, + resp_handle_out: *mut ResponseHandle, + resp_body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + match http_req::pending_req_wait_v2(pending_req_handle) { + Ok((resp_handle, resp_body_handle)) => { + unsafe { + *error_detail = http_req::SendErrorDetailTag::Ok.into(); + *resp_handle_out = resp_handle; + *resp_body_handle_out = resp_body_handle; + } + + FastlyStatus::OK + } + Err((detail, e)) => { + unsafe { + *error_detail = detail + .unwrap_or_else(|| http_req::SendErrorDetailTag::Ok.into()) + .into(); + *resp_handle_out = INVALID_HANDLE; + *resp_body_handle_out = INVALID_HANDLE; + } + e.into() + } + } + } + + #[export_name = "fastly_http_req#fastly_key_is_valid"] + pub fn fastly_key_is_valid(is_valid_out: *mut u32) -> FastlyStatus { + match http_req::fastly_key_is_valid() { + Ok(res) => { + unsafe { + *is_valid_out = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_req#close"] + pub fn close(req_handle: RequestHandle) -> FastlyStatus { + convert_result(http_req::close(req_handle)) + } + + #[export_name = "fastly_http_req#auto_decompress_response_set"] + pub fn auto_decompress_response_set( + req_handle: RequestHandle, + encodings: ContentEncodings, + ) -> FastlyStatus { + convert_result(http_req::auto_decompress_response_set( + req_handle, + encodings.into(), + )) + } +} + +pub mod fastly_http_resp { + use core::slice; + + use super::*; + use crate::bindings::fastly::{self, api::http_resp}; + + #[export_name = "fastly_http_resp#header_append"] + pub fn header_append( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + value: *const u8, + value_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let value = unsafe { slice::from_raw_parts(value, value_len) }; + convert_result(http_resp::header_append(resp_handle, name, value)) + } + + #[export_name = "fastly_http_resp#header_insert"] + pub fn header_insert( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + value: *const u8, + value_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let value = unsafe { slice::from_raw_parts(value, value_len) }; + convert_result(http_resp::header_insert(resp_handle, name, value)) + } + + #[export_name = "fastly_http_resp#header_names_get"] + pub fn header_names_get( + resp_handle: ResponseHandle, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + with_buffer!( + buf, + buf_len, + { + http_resp::header_names_get( + resp_handle, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_resp#header_value_get"] + pub fn header_value_get( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + with_buffer!( + value, + value_max_len, + { + http_resp::header_value_get( + resp_handle, + name, + u64::try_from(value_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res); + } + ) + } + + #[export_name = "fastly_http_resp#header_values_get"] + pub fn header_values_get( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + buf: *mut u8, + buf_len: usize, + cursor: u32, + ending_cursor: *mut i64, + nwritten: *mut usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + with_buffer!( + buf, + buf_len, + { + http_resp::header_values_get( + resp_handle, + name, + u64::try_from(buf_len).trapping_unwrap(), + cursor, + ) + }, + |res| { + let (written, end) = match res { + Some((bytes, next)) => { + let written = bytes.len(); + let end = match next { + Some(next) => i64::from(next), + None => -1, + }; + + std::mem::forget(bytes); + + (written, end) + } + None => (0, -1), + }; + + unsafe { + *nwritten = written; + *ending_cursor = end; + } + } + ) + } + + #[export_name = "fastly_http_resp#header_values_set"] + pub fn header_values_set( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + values: *const u8, + values_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + let values = unsafe { slice::from_raw_parts(values, values_len) }; + convert_result(http_resp::header_values_set(resp_handle, name, values)) + } + + #[export_name = "fastly_http_resp#header_remove"] + pub fn header_remove( + resp_handle: ResponseHandle, + name: *const u8, + name_len: usize, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + convert_result(http_resp::header_remove(resp_handle, name)) + } + + #[export_name = "fastly_http_resp#new"] + pub fn new(handle_out: *mut ResponseHandle) -> FastlyStatus { + match fastly::api::http_resp::new() { + Ok(handle) => { + unsafe { + *handle_out = handle; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_resp#send_downstream"] + pub fn send_downstream( + resp_handle: ResponseHandle, + body_handle: BodyHandle, + streaming: u32, + ) -> FastlyStatus { + convert_result(fastly::api::http_resp::send_downstream( + resp_handle, + body_handle, + streaming != 0, + )) + } + + #[export_name = "fastly_http_resp#status_get"] + pub fn status_get(resp_handle: ResponseHandle, status: *mut u16) -> FastlyStatus { + match http_resp::status_get(resp_handle) { + Ok(res) => { + unsafe { + *status = res; + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_resp#status_set"] + pub fn status_set(resp_handle: ResponseHandle, status: u16) -> FastlyStatus { + convert_result(fastly::api::http_resp::status_set(resp_handle, status)) + } + + #[export_name = "fastly_http_resp#version_get"] + pub fn version_get(resp_handle: ResponseHandle, version: *mut u32) -> FastlyStatus { + match fastly::api::http_resp::version_get(resp_handle) { + Ok(res) => { + unsafe { + *version = res.into(); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_http_resp#version_set"] + pub fn version_set(resp_handle: ResponseHandle, version: u32) -> FastlyStatus { + match crate::bindings::fastly::api::http_types::HttpVersion::try_from(version) { + Ok(version) => convert_result(crate::bindings::fastly::api::http_resp::version_set( + resp_handle, + version, + )), + + Err(_) => FastlyStatus::INVALID_ARGUMENT, + } + } + + #[export_name = "fastly_http_resp#framing_headers_mode_set"] + pub fn framing_headers_mode_set( + resp_handle: ResponseHandle, + mode: FramingHeadersMode, + ) -> FastlyStatus { + let mode = match mode { + FramingHeadersMode::Automatic => fastly::api::http_types::FramingHeadersMode::Automatic, + FramingHeadersMode::ManuallyFromHeaders => { + fastly::api::http_types::FramingHeadersMode::ManuallyFromHeaders + } + }; + + convert_result(fastly::api::http_resp::framing_headers_mode_set( + resp_handle, + mode, + )) + } + + #[doc(hidden)] + #[export_name = "fastly_http_resp#http_keepalive_mode_set"] + pub fn http_keepalive_mode_set( + resp_handle: ResponseHandle, + mode: HttpKeepaliveMode, + ) -> FastlyStatus { + let mode = match mode { + HttpKeepaliveMode::Automatic => fastly::api::http_resp::KeepaliveMode::Automatic, + HttpKeepaliveMode::NoKeepalive => fastly::api::http_resp::KeepaliveMode::NoKeepalive, + }; + + convert_result(fastly::api::http_resp::http_keepalive_mode_set( + resp_handle, + mode, + )) + } + + #[export_name = "fastly_http_resp#close"] + pub fn close(resp_handle: ResponseHandle) -> FastlyStatus { + convert_result(fastly::api::http_resp::close(resp_handle)) + } +} + +pub mod fastly_dictionary { + use core::slice; + + use super::*; + use crate::bindings::fastly::api::dictionary; + + #[export_name = "fastly_dictionary#open"] + pub fn open( + name: *const u8, + name_len: usize, + dict_handle_out: *mut DictionaryHandle, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name, name_len) }; + match dictionary::open(name) { + Ok(res) => { + unsafe { + *dict_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_dictionary#get"] + pub fn get( + dict_handle: DictionaryHandle, + key: *const u8, + key_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key, key_len) }; + with_buffer!( + value, + value_max_len, + { + dictionary::get( + dict_handle, + key, + u64::try_from(value_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res) + } + ) + } +} + +pub mod fastly_geo { + use super::*; + use crate::bindings::fastly::api::geo; + use core::slice; + + #[export_name = "fastly_geo#lookup"] + pub fn lookup( + addr_octets: *const u8, + addr_len: usize, + buf: *mut u8, + buf_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + let addr = unsafe { slice::from_raw_parts(addr_octets, addr_len) }; + alloc_result!(buf, buf_len, nwritten_out, { + geo::lookup(addr, u64::try_from(buf_len).trapping_unwrap()) + }) + } +} + +pub mod fastly_device_detection { + use super::*; + use crate::bindings::fastly::api::device_detection; + use core::slice; + + #[export_name = "fastly_device_detection#lookup"] + pub fn lookup( + user_agent: *const u8, + user_agent_max_len: usize, + buf: *mut u8, + buf_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + let user_agent = unsafe { slice::from_raw_parts(user_agent, user_agent_max_len) }; + alloc_result!(buf, buf_len, nwritten_out, { + device_detection::lookup(user_agent, u64::try_from(buf_len).trapping_unwrap()) + }) + } +} + +pub mod fastly_erl { + use super::*; + use crate::bindings::fastly::api::erl; + use core::slice; + + #[export_name = "fastly_erl#check_rate"] + pub fn check_rate( + rc: *const u8, + rc_max_len: usize, + entry: *const u8, + entry_max_len: usize, + delta: u32, + window: u32, + limit: u32, + pb: *const u8, + pb_max_len: usize, + ttl: u32, + value: *mut u32, + ) -> FastlyStatus { + let rc = unsafe { slice::from_raw_parts(rc, rc_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + let pb = unsafe { slice::from_raw_parts(pb, pb_max_len) }; + match erl::check_rate(rc, entry, delta, window, limit, pb, ttl) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_erl#ratecounter_increment"] + pub fn ratecounter_increment( + rc: *const u8, + rc_max_len: usize, + entry: *const u8, + entry_max_len: usize, + delta: u32, + ) -> FastlyStatus { + let rc = unsafe { slice::from_raw_parts(rc, rc_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + convert_result(erl::ratecounter_increment(rc, entry, delta)) + } + + #[export_name = "fastly_erl#ratecounter_lookup_rate"] + pub fn ratecounter_lookup_rate( + rc: *const u8, + rc_max_len: usize, + entry: *const u8, + entry_max_len: usize, + window: u32, + value: *mut u32, + ) -> FastlyStatus { + let rc = unsafe { slice::from_raw_parts(rc, rc_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + match erl::ratecounter_lookup_rate(rc, entry, window) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_erl#ratecounter_lookup_count"] + pub fn ratecounter_lookup_count( + rc: *const u8, + rc_max_len: usize, + entry: *const u8, + entry_max_len: usize, + duration: u32, + value: *mut u32, + ) -> FastlyStatus { + let rc = unsafe { slice::from_raw_parts(rc, rc_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + match erl::ratecounter_lookup_count(rc, entry, duration) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_erl#penaltybox_add"] + pub fn penaltybox_add( + pb: *const u8, + pb_max_len: usize, + entry: *const u8, + entry_max_len: usize, + ttl: u32, + ) -> FastlyStatus { + let pb = unsafe { slice::from_raw_parts(pb, pb_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + convert_result(erl::penaltybox_add(pb, entry, ttl)) + } + + #[export_name = "fastly_erl#penaltybox_has"] + pub fn penaltybox_has( + pb: *const u8, + pb_max_len: usize, + entry: *const u8, + entry_max_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let pb = unsafe { slice::from_raw_parts(pb, pb_max_len) }; + let entry = unsafe { slice::from_raw_parts(entry, entry_max_len) }; + match erl::penaltybox_has(pb, entry) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_kv_store { + use super::*; + use crate::bindings::fastly::api::kv_store; + use core::slice; + + #[export_name = "fastly_object_store#open"] + pub fn open( + name_ptr: *const u8, + name_len: usize, + kv_store_handle_out: *mut KVStoreHandle, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name_ptr, name_len) }; + match kv_store::open(name) { + Ok(None) => { + unsafe { + *kv_store_handle_out = INVALID_HANDLE; + } + + FastlyStatus::NONE + } + Ok(Some(res)) => { + unsafe { + *kv_store_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#lookup"] + pub fn lookup( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::lookup(kv_store_handle, key) { + Ok(res) => { + unsafe { + *body_handle_out = res.unwrap_or(INVALID_HANDLE); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#lookup_async"] + pub fn lookup_async( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + pending_body_handle_out: *mut PendingObjectStoreLookupHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::lookup_async(kv_store_handle, key) { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#pending_lookup_wait"] + pub fn pending_lookup_wait( + pending_body_handle: PendingObjectStoreLookupHandle, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + match kv_store::pending_lookup_wait(pending_body_handle) { + Ok(res) => { + unsafe { + *body_handle_out = res.unwrap_or(INVALID_HANDLE); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#insert"] + pub fn insert( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + body_handle: BodyHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + convert_result(kv_store::insert(kv_store_handle, key, body_handle)) + } + + #[export_name = "fastly_object_store#insert_async"] + pub fn insert_async( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + body_handle: BodyHandle, + pending_body_handle_out: *mut PendingObjectStoreInsertHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::insert_async(kv_store_handle, key, body_handle) { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#pending_insert_wait"] + pub fn pending_insert_wait( + pending_body_handle: PendingObjectStoreInsertHandle, + ) -> FastlyStatus { + convert_result(kv_store::pending_insert_wait(pending_body_handle)) + } + + #[export_name = "fastly_object_store#delete_async"] + pub fn delete_async( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + pending_body_handle_out: *mut PendingObjectStoreDeleteHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::delete_async(kv_store_handle, key) { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_object_store#pending_delete_wait"] + pub fn pending_delete_wait( + pending_body_handle: PendingObjectStoreDeleteHandle, + ) -> FastlyStatus { + convert_result(kv_store::pending_delete_wait(pending_body_handle)) + } +} + +pub mod fastly_secret_store { + use super::*; + use crate::bindings::fastly::api::secret_store; + use core::slice; + + #[export_name = "fastly_secret_store#open"] + pub fn open( + secret_store_name_ptr: *const u8, + secret_store_name_len: usize, + secret_store_handle_out: *mut SecretStoreHandle, + ) -> FastlyStatus { + let secret_store_name = + unsafe { slice::from_raw_parts(secret_store_name_ptr, secret_store_name_len) }; + match secret_store::open(secret_store_name) { + Ok(res) => { + unsafe { + *secret_store_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_secret_store#get"] + pub fn get( + secret_store_handle: SecretStoreHandle, + secret_name_ptr: *const u8, + secret_name_len: usize, + secret_handle_out: *mut SecretHandle, + ) -> FastlyStatus { + let secret_name = unsafe { slice::from_raw_parts(secret_name_ptr, secret_name_len) }; + match secret_store::get(secret_store_handle, secret_name) { + Ok(Some(res)) => { + unsafe { + *secret_handle_out = res; + } + FastlyStatus::OK + } + + Ok(None) => FastlyStatus::NONE, + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_secret_store#plaintext"] + pub fn plaintext( + secret_handle: SecretHandle, + plaintext_buf: *mut u8, + plaintext_max_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + with_buffer!( + plaintext_buf, + plaintext_max_len, + { + secret_store::plaintext( + secret_handle, + u64::try_from(plaintext_max_len).trapping_unwrap(), + ) + }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten_out = res.len(); + } + std::mem::forget(res); + } + ) + } + + #[export_name = "fastly_secret_store#from_bytes"] + pub fn from_bytes( + plaintext_buf: *const u8, + plaintext_len: usize, + secret_handle_out: *mut SecretHandle, + ) -> FastlyStatus { + let plaintext = unsafe { slice::from_raw_parts(plaintext_buf, plaintext_len) }; + match secret_store::from_bytes(plaintext) { + Ok(res) => { + unsafe { + *secret_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_backend { + use super::*; + use crate::bindings::fastly::api::{backend, http_types}; + use core::slice; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[repr(u32)] + pub enum BackendHealth { + Unknown, + Healthy, + Unhealthy, + } + + impl From for BackendHealth { + fn from(value: backend::BackendHealth) -> Self { + match value { + backend::BackendHealth::Unknown => BackendHealth::Unknown, + backend::BackendHealth::Healthy => BackendHealth::Healthy, + backend::BackendHealth::Unhealthy => BackendHealth::Unhealthy, + } + } + } + + impl From for u32 { + fn from(val: http_types::TlsVersion) -> Self { + match val { + http_types::TlsVersion::Tls1 => 0, + http_types::TlsVersion::Tls11 => 1, + http_types::TlsVersion::Tls12 => 2, + http_types::TlsVersion::Tls13 => 3, + } + } + } + + impl TryFrom for http_types::TlsVersion { + type Error = u32; + + fn try_from(val: u32) -> Result { + match val { + 0 => Ok(http_types::TlsVersion::Tls1), + 1 => Ok(http_types::TlsVersion::Tls11), + 2 => Ok(http_types::TlsVersion::Tls12), + 3 => Ok(http_types::TlsVersion::Tls13), + _ => Err(val), + } + } + } + + #[export_name = "fastly_backend#exists"] + pub fn exists( + backend_ptr: *const u8, + backend_len: usize, + backend_exists_out: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::exists(backend) { + Ok(res) => { + unsafe { + *backend_exists_out = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#is_healthy"] + pub fn is_healthy( + backend_ptr: *const u8, + backend_len: usize, + backend_health_out: *mut BackendHealth, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::is_healthy(backend) { + Ok(res) => { + unsafe { + *backend_health_out = BackendHealth::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#is_dynamic"] + pub fn is_dynamic(backend_ptr: *const u8, backend_len: usize, value: *mut u32) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::is_dynamic(backend) { + Ok(res) => { + unsafe { + *value = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_host"] + pub fn get_host( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + alloc_result!(value, value_max_len, nwritten, { + backend::get_host(backend, u64::try_from(value_max_len).trapping_unwrap()) + }) + } + + #[export_name = "fastly_backend#get_override_host"] + pub fn get_override_host( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u8, + value_max_len: usize, + nwritten: *mut usize, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + with_buffer!( + value, + value_max_len, + { backend::get_override_host(backend, u64::try_from(value_max_len).trapping_unwrap()) }, + |res| { + let res = res.ok_or(FastlyStatus::NONE)?; + unsafe { + *nwritten = res.len(); + } + std::mem::forget(res); + } + ) + } + + #[export_name = "fastly_backend#get_port"] + pub fn get_port(backend_ptr: *const u8, backend_len: usize, value: *mut u16) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_port(backend) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_connect_timeout_ms"] + pub fn get_connect_timeout_ms( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_connect_timeout_ms(backend) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_first_byte_timeout_ms"] + pub fn get_first_byte_timeout_ms( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_first_byte_timeout_ms(backend) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_between_bytes_timeout_ms"] + pub fn get_between_bytes_timeout_ms( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_between_bytes_timeout_ms(backend) { + Ok(res) => { + unsafe { + *value = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#is_ssl"] + pub fn is_ssl(backend_ptr: *const u8, backend_len: usize, value: *mut u32) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::is_ssl(backend) { + Ok(res) => { + unsafe { + *value = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_ssl_min_version"] + pub fn get_ssl_min_version( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_ssl_min_version(backend) { + Ok(res) => { + unsafe { + *value = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_backend#get_ssl_max_version"] + pub fn get_ssl_max_version( + backend_ptr: *const u8, + backend_len: usize, + value: *mut u32, + ) -> FastlyStatus { + let backend = unsafe { slice::from_raw_parts(backend_ptr, backend_len) }; + match backend::get_ssl_max_version(backend) { + Ok(res) => { + unsafe { + *value = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_async_io { + use super::*; + use crate::bindings::fastly::api::async_io; + use core::slice; + + #[export_name = "fastly_async_io#select"] + pub fn select( + async_item_handles: *const AsyncItemHandle, + async_item_handles_len: usize, + timeout_ms: u32, + done_index_out: *mut u32, + ) -> FastlyStatus { + let async_item_handles = + unsafe { slice::from_raw_parts(async_item_handles, async_item_handles_len) }; + match async_io::select(async_item_handles, timeout_ms) { + Ok(Some(res)) => { + unsafe { + *done_index_out = res; + } + FastlyStatus::OK + } + + Ok(None) => { + unsafe { + *done_index_out = u32::MAX; + } + FastlyStatus::NONE + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_async_io#is_ready"] + pub fn is_ready(async_item_handle: AsyncItemHandle, ready_out: *mut u32) -> FastlyStatus { + match async_io::is_ready(async_item_handle) { + Ok(res) => { + unsafe { + *ready_out = u32::from(res); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + +pub mod fastly_purge { + use super::*; + use crate::bindings::fastly::api::purge; + use core::slice; + + bitflags::bitflags! { + #[derive(Default)] + #[repr(transparent)] + pub struct PurgeOptionsMask: u32 { + const SOFT_PURGE = 1 << 0; + const RET_BUF = 1 << 1; + } + } + + impl From for purge::PurgeOptionsMask { + fn from(value: PurgeOptionsMask) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::SOFT_PURGE, + value.contains(PurgeOptionsMask::SOFT_PURGE), + ); + flags.set(Self::RET_BUF, value.contains(PurgeOptionsMask::RET_BUF)); + flags + } + } + + #[derive(Debug)] + #[repr(C)] + pub struct PurgeOptions { + pub ret_buf_ptr: *mut u8, + pub ret_buf_len: usize, + pub ret_buf_nwritten_out: *mut usize, + } + + #[export_name = "fastly_purge#purge_surrogate_key"] + pub fn purge_surrogate_key( + surrogate_key_ptr: *const u8, + surrogate_key_len: usize, + options_mask: PurgeOptionsMask, + options: *mut PurgeOptions, + ) -> FastlyStatus { + let surrogate_key = unsafe { slice::from_raw_parts(surrogate_key_ptr, surrogate_key_len) }; + with_buffer!( + unsafe { (*options).ret_buf_ptr }, + unsafe { (*options).ret_buf_len }, + { + purge::purge_surrogate_key( + surrogate_key, + options_mask.into(), + u64::try_from(unsafe { (*options).ret_buf_len }).trapping_unwrap(), + ) + }, + |res| { + if let Some(res) = res { + unsafe { + *(*options).ret_buf_nwritten_out = res.len(); + } + std::mem::forget(res); + } + } + ) + } +} diff --git a/crates/adapter/src/fastly/error.rs b/crates/adapter/src/fastly/error.rs new file mode 100644 index 00000000..0c7fecf3 --- /dev/null +++ b/crates/adapter/src/fastly/error.rs @@ -0,0 +1,48 @@ +use crate::StateError; + +#[derive(PartialEq, Eq)] +#[repr(C)] +pub struct FastlyStatus(i32); + +impl StateError for FastlyStatus { + const SUCCESS: Self = Self::OK; +} + +impl FastlyStatus { + pub const OK: Self = FastlyStatus(0); + pub const UNKNOWN_ERROR: Self = FastlyStatus(1); + pub const INVALID_ARGUMENT: Self = Self(2); + pub const NONE: Self = FastlyStatus(10); +} + +impl From for FastlyStatus { + fn from(err: crate::bindings::fastly::api::types::Error) -> Self { + use crate::bindings::fastly::api::types::Error; + FastlyStatus(match err { + // use black_box here to prevent rustc/llvm from generating a switch table + Error::UnknownError => std::hint::black_box(100), + Error::GenericError => 1, + Error::InvalidArgument => Self::INVALID_ARGUMENT.0, + Error::BadHandle => 3, + Error::BufferLen => 4, + Error::Unsupported => 5, + Error::BadAlign => 6, + Error::HttpInvalid => 7, + Error::HttpUser => 8, + Error::HttpIncomplete => 9, + Error::OptionalNone => 10, + Error::HttpHeadTooLarge => 11, + Error::HttpInvalidStatus => 12, + Error::LimitExceeded => 13, + }) + } +} + +pub(crate) fn convert_result( + res: Result<(), crate::bindings::fastly::api::types::Error>, +) -> FastlyStatus { + match res { + Ok(()) => FastlyStatus::OK, + Err(e) => FastlyStatus::from(e), + } +} diff --git a/crates/adapter/src/fastly/macros.rs b/crates/adapter/src/fastly/macros.rs new file mode 100644 index 00000000..8047f992 --- /dev/null +++ b/crates/adapter/src/fastly/macros.rs @@ -0,0 +1,23 @@ +#[macro_export] +macro_rules! with_buffer { + ($buf:expr, $len:expr, $alloc:block, |$res:ident| $free:block) => { + crate::State::with::(|state| { + let $res = state.import_alloc.with_buffer($buf, $len, || $alloc)?; + $free; + Ok(()) + }) + }; +} + +#[macro_export] +macro_rules! alloc_result { + ($buf:expr, $len:expr, $nwritten:expr, $block:block) => { + with_buffer!($buf, $len, $block, |res| { + unsafe { + *$nwritten = res.len(); + } + + std::mem::forget(res); + }) + }; +} diff --git a/crates/adapter/src/fastly/mod.rs b/crates/adapter/src/fastly/mod.rs new file mode 100644 index 00000000..34998690 --- /dev/null +++ b/crates/adapter/src/fastly/mod.rs @@ -0,0 +1,11 @@ +mod cache; +mod config_store; +mod core; +mod error; +mod macros; + +pub(crate) use error::*; + +pub use cache::*; +pub use config_store::*; +pub use core::*; diff --git a/crates/adapter/src/lib.rs b/crates/adapter/src/lib.rs new file mode 100644 index 00000000..f096ab06 --- /dev/null +++ b/crates/adapter/src/lib.rs @@ -0,0 +1,1429 @@ +use crate::bindings::wasi::clocks::{monotonic_clock, wall_clock}; +use crate::bindings::wasi::io::poll; +use crate::bindings::wasi::io::streams; +use crate::bindings::wasi::random::random; +use core::cell::{Cell, RefCell, RefMut, UnsafeCell}; +use core::ffi::c_void; +use core::mem::{self, align_of, forget, size_of, MaybeUninit}; +use core::ops::{Deref, DerefMut}; +use core::ptr::{self, null_mut}; +use core::slice; +use poll::Pollable; +use wasi::*; + +// test + +#[macro_use] +mod macros; + +pub mod fastly; + +mod descriptors; +use crate::descriptors::{Descriptor, Descriptors, StreamType}; + +pub mod bindings { + wit_bindgen_rust_macro::generate!({ + path: "../../lib/wit", + world: "fastly:api/compute", + std_feature, + raw_strings, + runtime_path: "crate::bindings::wit_bindgen_rt_shim", + disable_run_ctors_once_workaround: true, + skip: ["poll"], + }); + + pub mod wit_bindgen_rt_shim { + pub use bitflags; + + pub fn maybe_link_cabi_realloc() {} + } + + pub struct ComponentAdapter; + + impl exports::fastly::api::reactor::Guest for ComponentAdapter { + fn serve( + req: fastly::api::http_types::RequestHandle, + body: fastly::api::http_types::BodyHandle, + ) -> Result<(), ()> { + #[link(wasm_import_module = "__main_module__")] + extern "C" { + fn _start(); + } + + let res = crate::State::with::(|state| { + let old = state.request.replace(Some(req)); + assert!(old.is_none()); + let old = state.request_body.replace(Some(body)); + assert!(old.is_none()); + Ok(()) + }); + + unsafe { + _start(); + } + + if res == crate::fastly::FastlyStatus::OK { + Ok(()) + } else { + Err(()) + } + } + } + + export!(ComponentAdapter); +} + +// The unwrap/expect methods in std pull panic when they fail, which pulls +// in unwinding machinery that we can't use in the adapter. Instead, use this +// extension trait to get postfixed upwrap on Option and Result. +trait TrappingUnwrap { + fn trapping_unwrap(self) -> T; +} + +impl TrappingUnwrap for Option { + fn trapping_unwrap(self) -> T { + match self { + Some(t) => t, + None => unreachable!(), + } + } +} + +impl TrappingUnwrap for Result { + fn trapping_unwrap(self) -> T { + match self { + Ok(t) => t, + Err(_) => unreachable!(), + } + } +} + +/// Allocate a file descriptor which will generate an `ERRNO_BADF` if passed to +/// any WASI Preview 1 function implemented by this adapter. +/// +/// This is intended for use by `wasi-libc` during its incremental transition +/// from WASI Preview 1 to Preview 2. It will use this function to reserve +/// descriptors for its own use, valid only for use with libc functions. +#[no_mangle] +pub unsafe extern "C" fn adapter_open_badfd(fd: *mut u32) -> Errno { + State::with::(|state| { + *fd = state.descriptors_mut().open(Descriptor::Bad)?; + Ok(()) + }) +} + +/// Close a descriptor previously opened using `adapter_open_badfd`. +#[no_mangle] +pub unsafe extern "C" fn adapter_close_badfd(fd: u32) -> Errno { + State::with::(|state| state.descriptors_mut().close(fd)) +} + +#[no_mangle] +pub unsafe extern "C" fn reset_adapter_state() { + let state = get_state_ptr(); + if !state.is_null() { + State::init(state) + } +} + +#[no_mangle] +pub unsafe extern "C" fn cabi_import_realloc( + old_ptr: *mut u8, + old_size: usize, + align: usize, + new_size: usize, +) -> *mut u8 { + if !old_ptr.is_null() || old_size != 0 { + unreachable!(); + } + let mut ptr = null_mut::(); + State::with::(|state| { + ptr = state.import_alloc.alloc(align, new_size); + Ok(()) + }); + ptr +} + +/// Bump-allocated memory arena. This is a singleton - the +/// memory will be sized according to `bump_arena_size()`. +pub struct BumpArena { + data: MaybeUninit<[u8; bump_arena_size()]>, + position: Cell, +} + +impl BumpArena { + fn new() -> Self { + BumpArena { + data: MaybeUninit::uninit(), + position: Cell::new(0), + } + } + fn alloc(&self, align: usize, size: usize) -> *mut u8 { + let start = self.data.as_ptr() as usize; + let next = start + self.position.get(); + let alloc = align_to(next, align); + let offset = alloc - start; + if offset + size > bump_arena_size() { + unreachable!("out of memory"); + } + self.position.set(offset + size); + alloc as *mut u8 + } +} +fn align_to(ptr: usize, align: usize) -> usize { + (ptr + (align - 1)) & !(align - 1) +} + +// Invariant: buffer not-null and arena is-some are never true at the same +// time. We did not use an enum to make this invalid behavior unrepresentable +// because we can't use RefCell to borrow() the variants of the enum - only +// Cell provides mutability without pulling in panic machinery - so it would +// make the accessors a lot more awkward to write. +pub struct ImportAlloc { + // When not-null, allocator should use this buffer/len pair at most once + // to satisfy allocations. + buffer: Cell<*mut u8>, + len: Cell, + // When not-empty, allocator should use this arena to satisfy allocations. + arena: Cell>, +} + +impl ImportAlloc { + fn new() -> Self { + ImportAlloc { + buffer: Cell::new(std::ptr::null_mut()), + len: Cell::new(0), + arena: Cell::new(None), + } + } + + /// Expect at most one import allocation during execution of the provided closure. + /// Use the provided buffer to satisfy that import allocation. The user is responsible + /// for making sure allocated imports are not used beyond the lifetime of the buffer. + pub fn with_buffer(&self, buffer: *mut u8, len: usize, f: impl FnOnce() -> T) -> T { + if self.arena.get().is_some() { + unreachable!("arena mode") + } + let prev = self.buffer.replace(buffer); + if !prev.is_null() { + unreachable!("overwrote another buffer") + } + self.len.set(len); + let r = f(); + self.buffer.set(std::ptr::null_mut()); + r + } + + /// To be used by cabi_import_realloc only! + fn alloc(&self, align: usize, size: usize) -> *mut u8 { + if let Some(arena) = self.arena.get() { + arena.alloc(align, size) + } else { + let buffer = self.buffer.get(); + if buffer.is_null() { + unreachable!("buffer not provided, or already used") + } + let buffer = buffer as usize; + let alloc = align_to(buffer, align); + if alloc.checked_add(size).trapping_unwrap() + > buffer.checked_add(self.len.get()).trapping_unwrap() + { + unreachable!("out of memory") + } + self.buffer.set(std::ptr::null_mut()); + alloc as *mut u8 + } + } +} + +/// This allocator is only used for the `run` entrypoint. +/// +/// The implementation here is a bump allocator into `State::long_lived_arena` which +/// traps when it runs out of data. This means that the total size of +/// arguments/env/etc coming into a component is bounded by the current 64k +/// (ish) limit. That's just an implementation limit though which can be lifted +/// by dynamically calling the main module's allocator as necessary for more data. +#[no_mangle] +pub unsafe extern "C" fn cabi_export_realloc( + old_ptr: *mut u8, + old_size: usize, + align: usize, + new_size: usize, +) -> *mut u8 { + if !old_ptr.is_null() || old_size != 0 { + unreachable!(); + } + let mut ret = null_mut::(); + State::with::(|state| { + ret = state.long_lived_arena.alloc(align, new_size); + Ok(()) + }); + ret +} + +/// Read command-line argument data. +/// The size of the array should match that returned by `args_sizes_get` +#[no_mangle] +pub unsafe extern "C" fn args_get(_argv: *mut *mut u8, _argv_buf: *mut u8) -> Errno { + ERRNO_SUCCESS +} + +/// Return command-line argument data sizes. +#[no_mangle] +pub unsafe extern "C" fn args_sizes_get(argc: *mut Size, argv_buf_size: *mut Size) -> Errno { + *argc = 0; + *argv_buf_size = 0; + ERRNO_SUCCESS +} + +/// Read environment variable data. +/// The sizes of the buffers should match that returned by `environ_sizes_get`. +#[no_mangle] +pub unsafe extern "C" fn environ_get(_environ: *mut *mut u8, _nviron_buf: *mut u8) -> Errno { + ERRNO_SUCCESS +} + +/// Return environment variable data sizes. +#[no_mangle] +pub unsafe extern "C" fn environ_sizes_get( + environc: *mut Size, + environ_buf_size: *mut Size, +) -> Errno { + *environc = 0; + *environ_buf_size = 0; + + return ERRNO_SUCCESS; +} + +/// Return the resolution of a clock. +/// Implementations are required to provide a non-zero value for supported clocks. For unsupported clocks, +/// return `errno::inval`. +/// Note: This is similar to `clock_getres` in POSIX. +#[no_mangle] +pub extern "C" fn clock_res_get(id: Clockid, resolution: &mut Timestamp) -> Errno { + match id { + CLOCKID_MONOTONIC => { + *resolution = monotonic_clock::resolution(); + ERRNO_SUCCESS + } + CLOCKID_REALTIME => { + let res = wall_clock::resolution(); + *resolution = match Timestamp::from(res.seconds) + .checked_mul(1_000_000_000) + .and_then(|ns| ns.checked_add(res.nanoseconds.into())) + { + Some(ns) => ns, + None => return ERRNO_OVERFLOW, + }; + ERRNO_SUCCESS + } + _ => ERRNO_BADF, + } +} + +/// Return the time value of a clock. +/// Note: This is similar to `clock_gettime` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn clock_time_get( + id: Clockid, + _precision: Timestamp, + time: &mut Timestamp, +) -> Errno { + match id { + CLOCKID_MONOTONIC => { + *time = monotonic_clock::now(); + ERRNO_SUCCESS + } + CLOCKID_REALTIME => { + let res = wall_clock::now(); + *time = match Timestamp::from(res.seconds) + .checked_mul(1_000_000_000) + .and_then(|ns| ns.checked_add(res.nanoseconds.into())) + { + Some(ns) => ns, + None => return ERRNO_OVERFLOW, + }; + ERRNO_SUCCESS + } + _ => ERRNO_BADF, + } +} + +/// Provide file advisory information on a file descriptor. +/// Note: This is similar to `posix_fadvise` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_advise( + _fd: Fd, + _offset: Filesize, + _len: Filesize, + _advice: Advice, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Force the allocation of space in a file. +/// Note: This is similar to `posix_fallocate` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_allocate(_fd: Fd, _offset: Filesize, _len: Filesize) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Close a file descriptor. +/// Note: This is similar to `close` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_close(fd: Fd) -> Errno { + State::with::(|state| { + if let Descriptor::Bad = state.descriptors().get(fd)? { + return Err(wasi::ERRNO_BADF); + } + + state.descriptors_mut().close(fd)?; + Ok(()) + }) +} + +/// Synchronize the data of a file to disk. +/// Note: This is similar to `fdatasync` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_datasync(_fd: Fd) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Get the attributes of a file descriptor. +/// Note: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_get(_fd: Fd, _stat: *mut Fdstat) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Adjust the flags associated with a file descriptor. +/// Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_set_flags(_fd: Fd, _flags: Fdflags) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Does not do anything if `fd` corresponds to a valid descriptor and returns [`wasi::ERRNO_BADF`] otherwise. +#[no_mangle] +pub unsafe extern "C" fn fd_fdstat_set_rights( + fd: Fd, + _fs_rights_base: Rights, + _fs_rights_inheriting: Rights, +) -> Errno { + State::with::(|state| { + let ds = state.descriptors(); + match ds.get(fd)? { + Descriptor::Streams(..) => Ok(()), + Descriptor::Closed(..) | Descriptor::Bad => Err(wasi::ERRNO_BADF), + } + }) +} + +/// Return the attributes of an open file. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_get(_fd: Fd, _buf: *mut Filestat) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. +/// Note: This is similar to `ftruncate` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_set_size(_fd: Fd, _size: Filesize) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Adjust the timestamps of an open file or directory. +/// Note: This is similar to `futimens` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_filestat_set_times( + _fd: Fd, + _atim: Timestamp, + _mtim: Timestamp, + _fst_flags: Fstflags, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Read from a file descriptor, without using and updating the file descriptor's offset. +/// Note: This is similar to `preadv` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_pread( + _fd: Fd, + _iovs_ptr: *const Iovec, + _iovs_len: usize, + _offset: Filesize, + _nread: *mut Size, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Return a description of the given preopened file descriptor. +#[no_mangle] +pub unsafe extern "C" fn fd_prestat_get(_fd: Fd, _buf: *mut Prestat) -> Errno { + return ERRNO_BADF; +} + +/// Return a description of the given preopened file descriptor. +#[no_mangle] +pub unsafe extern "C" fn fd_prestat_dir_name( + _fd: Fd, + _path: *mut u8, + _path_max_len: Size, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Write to a file descriptor, without using and updating the file descriptor's offset. +/// Note: This is similar to `pwritev` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_pwrite( + _fd: Fd, + _iovs_ptr: *const Ciovec, + _iovs_len: usize, + _offset: Filesize, + _nwritten: *mut Size, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Read from a file descriptor. +/// Note: This is similar to `readv` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_read( + fd: Fd, + mut iovs_ptr: *const Iovec, + mut iovs_len: usize, + nread: *mut Size, +) -> Errno { + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nread = 0; + return ERRNO_SUCCESS; + } + + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + + State::with::(|state| { + let ds = state.descriptors(); + match ds.get(fd)? { + Descriptor::Streams(streams) => { + let blocking_mode = BlockingMode::Blocking; + + let read_len = u64::try_from(len).trapping_unwrap(); + let wasi_stream = streams.get_read_stream()?; + let data = match state + .import_alloc + .with_buffer(ptr, len, || blocking_mode.read(wasi_stream, read_len)) + { + Ok(data) => data, + Err(streams::StreamError::Closed) => { + *nread = 0; + return Ok(()); + } + Err(streams::StreamError::LastOperationFailed(e)) => { + Err(stream_error_to_errno(e))? + } + }; + + assert_eq!(data.as_ptr(), ptr); + assert!(data.len() <= len); + + let len = data.len(); + *nread = len; + forget(data); + Ok(()) + } + Descriptor::Closed(_) | Descriptor::Bad => Err(ERRNO_BADF), + } + }) +} + +fn stream_error_to_errno(_err: streams::Error) -> Errno { + return ERRNO_IO; +} + +/// Read directory entries from a directory. +/// When successful, the contents of the output buffer consist of a sequence of +/// directory entries. Each directory entry consists of a `dirent` object, +/// followed by `dirent::d_namlen` bytes holding the name of the directory +/// entry. +/// This function fills the output buffer as much as possible, potentially +/// truncating the last directory entry. This allows the caller to grow its +/// read buffer size in case it's too small to fit a single large directory +/// entry, or skip the oversized directory entry. +#[no_mangle] +pub unsafe extern "C" fn fd_readdir( + _fd: Fd, + _buf: *mut u8, + _buf_len: Size, + _cookie: Dircookie, + _bufused: *mut Size, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Atomically replace a file descriptor by renumbering another file descriptor. +/// Due to the strong focus on thread safety, this environment does not provide +/// a mechanism to duplicate or renumber a file descriptor to an arbitrary +/// number, like `dup2()`. This would be prone to race conditions, as an actual +/// file descriptor with the same number could be allocated by a different +/// thread at the same time. +/// This function provides a way to atomically renumber file descriptors, which +/// would disappear if `dup2()` were to be removed entirely. +#[no_mangle] +pub unsafe extern "C" fn fd_renumber(fd: Fd, to: Fd) -> Errno { + State::with::(|state| state.descriptors_mut().renumber(fd, to)) +} + +/// Move the offset of a file descriptor. +/// Note: This is similar to `lseek` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_seek( + _fd: Fd, + _offset: Filedelta, + _whence: Whence, + _newoffset: *mut Filesize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Synchronize the data and metadata of a file to disk. +/// Note: This is similar to `fsync` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_sync(_fd: Fd) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Return the current offset of a file descriptor. +/// Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_tell(_fd: Fd, _offset: *mut Filesize) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Write to a file descriptor. +/// Note: This is similar to `writev` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn fd_write( + fd: Fd, + mut iovs_ptr: *const Ciovec, + mut iovs_len: usize, + nwritten: *mut Size, +) -> Errno { + if !matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + *nwritten = 0; + return ERRNO_IO; + } + + // Advance to the first non-empty buffer. + while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { + iovs_ptr = iovs_ptr.add(1); + iovs_len -= 1; + } + if iovs_len == 0 { + *nwritten = 0; + return ERRNO_SUCCESS; + } + + let ptr = (*iovs_ptr).buf; + let len = (*iovs_ptr).buf_len; + let bytes = slice::from_raw_parts(ptr, len); + + State::with::(|state| { + let ds = state.descriptors(); + match ds.get(fd)? { + Descriptor::Streams(streams) => { + let wasi_stream = streams.get_write_stream()?; + + let nbytes = BlockingMode::Blocking.write(wasi_stream, bytes)?; + + *nwritten = nbytes; + Ok(()) + } + Descriptor::Closed(_) | Descriptor::Bad => Err(ERRNO_BADF), + } + }) +} + +/// Create a directory. +/// Note: This is similar to `mkdirat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_create_directory( + _fd: Fd, + _path_ptr: *const u8, + _path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Return the attributes of a file or directory. +/// Note: This is similar to `stat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_filestat_get( + _fd: Fd, + _flags: Lookupflags, + _path_ptr: *const u8, + _path_len: usize, + _buf: *mut Filestat, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Adjust the timestamps of a file or directory. +/// Note: This is similar to `utimensat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_filestat_set_times( + _fd: Fd, + _flags: Lookupflags, + _path_ptr: *const u8, + _path_len: usize, + _atim: Timestamp, + _mtim: Timestamp, + _fst_flags: Fstflags, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Create a hard link. +/// Note: This is similar to `linkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_link( + _old_fd: Fd, + _old_flags: Lookupflags, + _old_path_ptr: *const u8, + _old_path_len: usize, + _new_fd: Fd, + _new_path_ptr: *const u8, + _new_path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Open a file or directory. +/// The returned file descriptor is not guaranteed to be the lowest-numbered +/// file descriptor not currently open; it is randomized to prevent +/// applications from depending on making assumptions about indexes, since this +/// is error-prone in multi-threaded contexts. The returned file descriptor is +/// guaranteed to be less than 2**31. +/// Note: This is similar to `openat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_open( + _fd: Fd, + _dirflags: Lookupflags, + _path_ptr: *const u8, + _path_len: usize, + _oflags: Oflags, + _fs_rights_base: Rights, + _fs_rights_inheriting: Rights, + _fdflags: Fdflags, + _opened_fd: *mut Fd, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Read the contents of a symbolic link. +/// Note: This is similar to `readlinkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_readlink( + _fd: Fd, + _path_ptr: *const u8, + _path_len: usize, + _buf: *mut u8, + _buf_len: Size, + _bufused: *mut Size, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Remove a directory. +/// Return `errno::notempty` if the directory is not empty. +/// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_remove_directory( + _fd: Fd, + _path_ptr: *const u8, + _path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Rename a file or directory. +/// Note: This is similar to `renameat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_rename( + _old_fd: Fd, + _old_path_ptr: *const u8, + _old_path_len: usize, + _new_fd: Fd, + _new_path_ptr: *const u8, + _new_path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Create a symbolic link. +/// Note: This is similar to `symlinkat` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_symlink( + _old_path_ptr: *const u8, + _old_path_len: usize, + _fd: Fd, + _new_path_ptr: *const u8, + _new_path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +/// Unlink a file. +/// Return `errno::isdir` if the path refers to a directory. +/// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn path_unlink_file( + _fd: Fd, + _path_ptr: *const u8, + _path_len: usize, +) -> Errno { + wasi::ERRNO_NOTSUP +} + +struct Pollables { + pointer: *mut Pollable, + index: usize, + length: usize, +} + +impl Pollables { + unsafe fn push(&mut self, pollable: Pollable) { + assert!(self.index < self.length); + // Use `ptr::write` instead of `*... = pollable` because `ptr::write` + // doesn't call drop on the old memory. + self.pointer.add(self.index).write(pollable); + self.index += 1; + } +} + +// We create new pollable handles for each `poll_oneoff` call, so drop them all +// after the call. +impl Drop for Pollables { + fn drop(&mut self) { + while self.index != 0 { + self.index -= 1; + unsafe { + core::ptr::drop_in_place(self.pointer.add(self.index)); + } + } + } +} + +/// Concurrently poll for the occurrence of a set of events. +#[no_mangle] +pub unsafe extern "C" fn poll_oneoff( + r#in: *const Subscription, + out: *mut Event, + nsubscriptions: Size, + nevents: *mut Size, +) -> Errno { + *nevents = 0; + + let subscriptions = slice::from_raw_parts(r#in, nsubscriptions); + + // We're going to split the `nevents` buffer into two non-overlapping + // buffers: one to store the pollable handles, and the other to store + // the bool results. + // + // First, we assert that this is possible: + assert!(align_of::() >= align_of::()); + assert!(align_of::() >= align_of::()); + assert!( + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + >= nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + .checked_add( + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap() + ) + .trapping_unwrap() + ); + // Store the pollable handles at the beginning, and the bool results at the + // end, so that we don't clobber the bool results when writting the events. + let pollables = out as *mut c_void as *mut Pollable; + let results = out.add(nsubscriptions).cast::().sub(nsubscriptions); + + // Indefinite sleeping is not supported in preview1. + if nsubscriptions == 0 { + return ERRNO_INVAL; + } + + State::with::(|state| { + const EVENTTYPE_CLOCK: u8 = wasi::EVENTTYPE_CLOCK.raw(); + const EVENTTYPE_FD_READ: u8 = wasi::EVENTTYPE_FD_READ.raw(); + const EVENTTYPE_FD_WRITE: u8 = wasi::EVENTTYPE_FD_WRITE.raw(); + + let mut pollables = Pollables { + pointer: pollables, + index: 0, + length: nsubscriptions, + }; + + for subscription in subscriptions { + pollables.push(match subscription.u.tag { + EVENTTYPE_CLOCK => { + let clock = &subscription.u.u.clock; + let absolute = (clock.flags & SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) + == SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME; + match clock.id { + CLOCKID_REALTIME => { + let timeout = if absolute { + // Convert `clock.timeout` to `Datetime`. + let mut datetime = wall_clock::Datetime { + seconds: clock.timeout / 1_000_000_000, + nanoseconds: (clock.timeout % 1_000_000_000) as _, + }; + + // Subtract `now`. + let now = wall_clock::now(); + datetime.seconds -= now.seconds; + if datetime.nanoseconds < now.nanoseconds { + datetime.seconds -= 1; + datetime.nanoseconds += 1_000_000_000; + } + datetime.nanoseconds -= now.nanoseconds; + + // Convert to nanoseconds. + let nanos = datetime + .seconds + .checked_mul(1_000_000_000) + .ok_or(ERRNO_OVERFLOW)?; + nanos + .checked_add(datetime.nanoseconds.into()) + .ok_or(ERRNO_OVERFLOW)? + } else { + clock.timeout + }; + + monotonic_clock::subscribe_duration(timeout) + } + + CLOCKID_MONOTONIC => { + if absolute { + monotonic_clock::subscribe_instant(clock.timeout) + } else { + monotonic_clock::subscribe_duration(clock.timeout) + } + } + + _ => return Err(ERRNO_INVAL), + } + } + + EVENTTYPE_FD_READ => state + .descriptors() + .get_read_stream(subscription.u.u.fd_read.file_descriptor) + .map(|stream| stream.subscribe())?, + + EVENTTYPE_FD_WRITE => state + .descriptors() + .get_write_stream(subscription.u.u.fd_write.file_descriptor) + .map(|stream| stream.subscribe())?, + + _ => return Err(ERRNO_INVAL), + }); + } + + #[link(wasm_import_module = "wasi:io/poll@0.2.0")] + #[allow(improper_ctypes)] // FIXME(bytecodealliance/wit-bindgen#684) + extern "C" { + #[link_name = "poll"] + fn poll_import(pollables: *const Pollable, len: usize, rval: *mut ReadyList); + } + let mut ready_list = ReadyList { + base: std::ptr::null(), + len: 0, + }; + + state.import_alloc.with_buffer( + results.cast(), + nsubscriptions + .checked_mul(size_of::()) + .trapping_unwrap(), + || { + poll_import( + pollables.pointer, + pollables.length, + &mut ready_list as *mut _, + ); + }, + ); + + assert!(ready_list.len <= nsubscriptions); + assert_eq!(ready_list.base, results as *const u32); + + drop(pollables); + + let ready = std::slice::from_raw_parts(ready_list.base, ready_list.len); + + let mut count = 0; + + for subscription in ready { + let subscription = *subscriptions.as_ptr().add(*subscription as usize); + + let type_; + + let (error, nbytes, flags) = match subscription.u.tag { + EVENTTYPE_CLOCK => { + type_ = wasi::EVENTTYPE_CLOCK; + (ERRNO_SUCCESS, 0, 0) + } + + EVENTTYPE_FD_READ => { + type_ = wasi::EVENTTYPE_FD_READ; + let ds = state.descriptors(); + let desc = ds + .get(subscription.u.u.fd_read.file_descriptor) + .trapping_unwrap(); + match desc { + Descriptor::Streams(streams) => match &streams.type_ { + StreamType::Stdio(_) => (ERRNO_SUCCESS, 1, 0), + }, + _ => unreachable!(), + } + } + EVENTTYPE_FD_WRITE => { + type_ = wasi::EVENTTYPE_FD_WRITE; + let ds = state.descriptors(); + let desc = ds + .get(subscription.u.u.fd_write.file_descriptor) + .trapping_unwrap(); + match desc { + Descriptor::Streams(streams) => match &streams.type_ { + StreamType::Stdio(_) => (ERRNO_SUCCESS, 1, 0), + }, + _ => unreachable!(), + } + } + + _ => unreachable!(), + }; + + *out.add(count) = Event { + userdata: subscription.userdata, + error, + type_, + fd_readwrite: EventFdReadwrite { nbytes, flags }, + }; + + count += 1; + } + + *nevents = count; + + Ok(()) + }) +} + +/// Terminate the process normally. An exit code of 0 indicates successful +/// termination of the program. The meanings of other values is dependent on +/// the environment. +#[no_mangle] +pub unsafe extern "C" fn proc_exit(_rval: Exitcode) -> ! { + unreachable!("no other implementation available in proxy world"); +} + +/// Send a signal to the process of the calling thread. +/// Note: This is similar to `raise` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn proc_raise(_sig: Signal) -> Errno { + unreachable!() +} + +/// Temporarily yield execution of the calling thread. +/// Note: This is similar to `sched_yield` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sched_yield() -> Errno { + // TODO: This is not yet covered in Preview2. + + ERRNO_SUCCESS +} + +/// Write high-quality random data into a buffer. +/// This function blocks when the implementation is unable to immediately +/// provide sufficient high-quality random data. +/// This function may execute slowly, so when large mounts of random data are +/// required, it's advisable to use this function to seed a pseudo-random +/// number generator, rather than to provide the random data directly. +#[no_mangle] +pub unsafe extern "C" fn random_get(buf: *mut u8, buf_len: Size) -> Errno { + if matches!( + get_allocation_state(), + AllocationState::StackAllocated | AllocationState::StateAllocated + ) { + State::with::(|state| { + assert_eq!(buf_len as u32 as Size, buf_len); + let result = state + .import_alloc + .with_buffer(buf, buf_len, || random::get_random_bytes(buf_len as u64)); + assert_eq!(result.as_ptr(), buf); + + // The returned buffer's memory was allocated in `buf`, so don't separately + // free it. + forget(result); + + Ok(()) + }) + } else { + ERRNO_SUCCESS + } +} + +/// Accept a new incoming connection. +/// Note: This is similar to `accept` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sock_accept(_fd: Fd, _flags: Fdflags, _connection: *mut Fd) -> Errno { + unreachable!() +} + +/// Receive a message from a socket. +/// Note: This is similar to `recv` in POSIX, though it also supports reading +/// the data into multiple buffers in the manner of `readv`. +#[no_mangle] +pub unsafe extern "C" fn sock_recv( + _fd: Fd, + _ri_data_ptr: *const Iovec, + _ri_data_len: usize, + _ri_flags: Riflags, + _ro_datalen: *mut Size, + _ro_flags: *mut Roflags, +) -> Errno { + unreachable!() +} + +/// Send a message on a socket. +/// Note: This is similar to `send` in POSIX, though it also supports writing +/// the data from multiple buffers in the manner of `writev`. +#[no_mangle] +pub unsafe extern "C" fn sock_send( + _fd: Fd, + _si_data_ptr: *const Ciovec, + _si_data_len: usize, + _si_flags: Siflags, + _so_datalen: *mut Size, +) -> Errno { + unreachable!() +} + +/// Shut down socket send and receive channels. +/// Note: This is similar to `shutdown` in POSIX. +#[no_mangle] +pub unsafe extern "C" fn sock_shutdown(_fd: Fd, _how: Sdflags) -> Errno { + unreachable!() +} + +#[derive(Clone, Copy)] +pub enum BlockingMode { + NonBlocking, + Blocking, +} + +impl BlockingMode { + // note: these methods must take self, not &self, to avoid rustc creating a constant + // out of a BlockingMode literal that it places in .romem, creating a data section and + // breaking our fragile linking scheme + fn read( + self, + input_stream: &streams::InputStream, + read_len: u64, + ) -> Result, streams::StreamError> { + match self { + BlockingMode::NonBlocking => input_stream.read(read_len), + BlockingMode::Blocking => input_stream.blocking_read(read_len), + } + } + fn write( + self, + output_stream: &streams::OutputStream, + mut bytes: &[u8], + ) -> Result { + match self { + BlockingMode::Blocking => { + let total = bytes.len(); + while !bytes.is_empty() { + let len = bytes.len().min(4096); + let (chunk, rest) = bytes.split_at(len); + bytes = rest; + match output_stream.blocking_write_and_flush(chunk) { + Ok(()) => {} + Err(streams::StreamError::Closed) => return Err(ERRNO_IO), + Err(streams::StreamError::LastOperationFailed(e)) => { + return Err(stream_error_to_errno(e)) + } + } + } + Ok(total) + } + + BlockingMode::NonBlocking => { + let permit = match output_stream.check_write() { + Ok(n) => n, + Err(streams::StreamError::Closed) => 0, + Err(streams::StreamError::LastOperationFailed(e)) => { + return Err(stream_error_to_errno(e)) + } + }; + + let len = bytes.len().min(permit as usize); + if len == 0 { + return Ok(0); + } + + match output_stream.write(&bytes[..len]) { + Ok(_) => {} + Err(streams::StreamError::Closed) => return Ok(0), + Err(streams::StreamError::LastOperationFailed(e)) => { + return Err(stream_error_to_errno(e)) + } + } + + match output_stream.blocking_flush() { + Ok(_) => {} + Err(streams::StreamError::Closed) => return Ok(0), + Err(streams::StreamError::LastOperationFailed(e)) => { + return Err(stream_error_to_errno(e)) + } + } + + Ok(len) + } + } + } +} + +const PAGE_SIZE: usize = 65536; + +/// A canary value to detect memory corruption within `State`. +const MAGIC: u32 = u32::from_le_bytes(*b"ugh!"); + +#[repr(C)] // used for now to keep magic1 and magic2 at the start and end +pub(crate) struct State { + /// A canary constant value located at the beginning of this structure to + /// try to catch memory corruption coming from the bottom. + magic1: u32, + + /// Used to coordinate allocations of `cabi_import_realloc` + import_alloc: ImportAlloc, + + /// Storage of mapping from preview1 file descriptors to preview2 file + /// descriptors. + /// + /// Do not use this member directly - use State::descriptors() to ensure + /// lazy initialization happens. + descriptors: RefCell>, + + /// Long-lived bump allocated memory arena. + /// + /// This is used for the cabi_export_realloc to allocate data passed to the + /// `run` entrypoint. Allocations in this arena are safe to use for + /// the lifetime of the State struct. It may also be used for import allocations + /// which need to be long-lived, by using `import_alloc.with_arena`. + long_lived_arena: BumpArena, + + /// The incoming request, if the entry-point was through the reactor. + pub(crate) request: Cell>, + + /// The incoming request body, if the entry-point was through the reactor. + pub(crate) request_body: Cell>, + + /// Another canary constant located at the end of the structure to catch + /// memory corruption coming from the bottom. + magic2: u32, +} + +#[repr(C)] +pub struct WasmStr { + ptr: *const u8, + len: usize, +} + +#[repr(C)] +pub struct WasmStrList { + base: *const WasmStr, + len: usize, +} + +#[repr(C)] +pub struct StrTuple { + key: WasmStr, + value: WasmStr, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct StrTupleList { + base: *const StrTuple, + len: usize, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct ReadyList { + base: *const u32, + len: usize, +} + +const fn bump_arena_size() -> usize { + // The total size of the struct should be a page, so start there + let mut start = PAGE_SIZE; + + // Remove big chunks of the struct for its various fields. + start -= size_of::(); + + // Remove miscellaneous metadata also stored in state. + let misc = 11; + start -= misc * size_of::(); + + // Everything else is the `command_data` allocation. + start +} + +// Statically assert that the `State` structure is the size of a wasm page. This +// mostly guarantees that it's not larger than one page which is relied upon +// below. +#[cfg(target_arch = "wasm32")] +const _: () = { + let _size_assert: [(); PAGE_SIZE] = [(); size_of::()]; +}; + +#[allow(unused)] +#[repr(i32)] +enum AllocationState { + StackUnallocated, + StackAllocating, + StackAllocated, + StateAllocating, + StateAllocated, +} + +#[allow(improper_ctypes)] +extern "C" { + fn get_state_ptr() -> *mut State; + fn set_state_ptr(state: *mut State); + fn get_allocation_state() -> AllocationState; + fn set_allocation_state(state: AllocationState); +} + +pub(crate) trait StateError { + const SUCCESS: Self; +} + +impl StateError for Errno { + const SUCCESS: Self = ERRNO_SUCCESS; +} + +impl State { + pub(crate) fn with(f: impl FnOnce(&State) -> Result<(), E>) -> E { + let state_ref = State::ptr(); + assert_eq!(state_ref.magic1, MAGIC); + assert_eq!(state_ref.magic2, MAGIC); + let ret = f(state_ref); + match ret { + Ok(()) => E::SUCCESS, + Err(err) => err, + } + } + + fn ptr() -> &'static State { + unsafe { + let mut ptr = get_state_ptr(); + if ptr.is_null() { + ptr = State::new(); + set_state_ptr(ptr); + } + &*ptr + } + } + + #[cold] + fn new() -> *mut State { + #[link(wasm_import_module = "__main_module__")] + extern "C" { + fn cabi_realloc( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, + ) -> *mut u8; + } + + assert!(matches!( + unsafe { get_allocation_state() }, + AllocationState::StackAllocated + )); + + unsafe { set_allocation_state(AllocationState::StateAllocating) }; + + let ret = unsafe { + cabi_realloc( + ptr::null_mut(), + 0, + mem::align_of::>(), + mem::size_of::>(), + ) as *mut State + }; + + unsafe { set_allocation_state(AllocationState::StateAllocated) }; + + unsafe { + Self::init(ret); + } + + ret + } + + #[cold] + unsafe fn init(state: *mut State) { + state.write(State { + magic1: MAGIC, + magic2: MAGIC, + import_alloc: ImportAlloc::new(), + descriptors: RefCell::new(None), + request: Cell::new(None), + request_body: Cell::new(None), + long_lived_arena: BumpArena::new(), + }); + } + + /// Accessor for the descriptors member that ensures it is properly initialized + fn descriptors<'a>(&'a self) -> impl Deref + 'a { + let mut d = self + .descriptors + .try_borrow_mut() + .unwrap_or_else(|_| unreachable!()); + if d.is_none() { + *d = Some(Descriptors::new(&self.import_alloc, &self.long_lived_arena)); + } + RefMut::map(d, |d| d.as_mut().unwrap_or_else(|| unreachable!())) + } + + /// Mut accessor for the descriptors member that ensures it is properly initialized + fn descriptors_mut<'a>(&'a self) -> impl DerefMut + Deref + 'a { + let mut d = self + .descriptors + .try_borrow_mut() + .unwrap_or_else(|_| unreachable!()); + if d.is_none() { + *d = Some(Descriptors::new(&self.import_alloc, &self.long_lived_arena)); + } + RefMut::map(d, |d| d.as_mut().unwrap_or_else(|| unreachable!())) + } +} diff --git a/crates/adapter/src/macros.rs b/crates/adapter/src/macros.rs new file mode 100644 index 00000000..c1ec4a51 --- /dev/null +++ b/crates/adapter/src/macros.rs @@ -0,0 +1,94 @@ +//! Minimal versions of standard-library panicking and printing macros. +//! +//! We're avoiding static initializers, so we can't have things like string +//! literals. Replace the standard assert macros with simpler implementations. + +use crate::bindings::wasi::cli::stderr::get_stderr; + +#[allow(dead_code)] +#[doc(hidden)] +pub fn print(message: &[u8]) { + let _ = get_stderr().blocking_write_and_flush(message); +} + +/// A minimal `eprint` for debugging. +#[allow(unused_macros)] +macro_rules! eprint { + ($arg:tt) => {{ + // We have to expand string literals into byte arrays to prevent them + // from getting statically initialized. + let message = byte_array_literals::str!($arg); + $crate::macros::print(&message); + }}; +} + +/// A minimal `eprintln` for debugging. +#[allow(unused_macros)] +macro_rules! eprintln { + ($arg:tt) => {{ + // We have to expand string literals into byte arrays to prevent them + // from getting statically initialized. + let message = byte_array_literals::str_nl!($arg); + $crate::macros::print(&message); + }}; +} + +pub(crate) fn eprint_u32(x: u32) { + if x == 0 { + eprint!("0"); + } else { + eprint_u32_impl(x) + } + + fn eprint_u32_impl(x: u32) { + if x != 0 { + eprint_u32_impl(x / 10); + + let digit = [b'0' + ((x % 10) as u8)]; + crate::macros::print(&digit); + } + } +} + +/// A minimal `unreachable`. +macro_rules! unreachable { + () => {{ + eprint!("unreachable executed at adapter line "); + crate::macros::eprint_u32(line!()); + eprint!("\n"); + #[cfg(target_arch = "wasm32")] + core::arch::wasm32::unreachable(); + // This is here to keep rust-analyzer happy when building for native: + #[cfg(not(target_arch = "wasm32"))] + std::process::abort(); + }}; + + ($arg:tt) => {{ + eprint!("unreachable executed at adapter line "); + crate::macros::eprint_u32(line!()); + eprint!(": "); + eprintln!($arg); + eprint!("\n"); + #[cfg(target_arch = "wasm32")] + core::arch::wasm32::unreachable(); + // This is here to keep rust-analyzer happy when building for native: + #[cfg(not(target_arch = "wasm32"))] + std::process::abort(); + }}; +} + +/// A minimal `assert`. +macro_rules! assert { + ($cond:expr $(,)?) => { + if !$cond { + unreachable!("assertion failed") + } + }; +} + +/// A minimal `assert_eq`. +macro_rules! assert_eq { + ($left:expr, $right:expr $(,)?) => { + assert!($left == $right); + }; +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 253c7779..5def6f37 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -21,6 +21,7 @@ include = [ "src/**/*", "wit/**/*", "compute-at-edge-abi/**/*.witx", + "data/*.wasm", ] [dependencies] @@ -55,6 +56,8 @@ tracing = { workspace = true } tracing-futures = { workspace = true } url = { workspace = true } wasmparser = { workspace = true } +wasm-encoder = { workspace = true } +wit-component = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } wasmtime-wasi-nn = { workspace = true } diff --git a/lib/data/viceroy-component-adapter.wasm b/lib/data/viceroy-component-adapter.wasm new file mode 100755 index 00000000..5cf314de Binary files /dev/null and b/lib/data/viceroy-component-adapter.wasm differ diff --git a/lib/src/adapt.rs b/lib/src/adapt.rs new file mode 100644 index 00000000..3dd0d6f7 --- /dev/null +++ b/lib/src/adapt.rs @@ -0,0 +1,78 @@ +const ADAPTER_BYTES: &[u8] = include_bytes!("../data/viceroy-component-adapter.wasm"); + +/// Given bytes that represent a core wasm module, adapt it to a component using the viceroy +/// adapter. +pub fn adapt_bytes(bytes: &[u8]) -> anyhow::Result> { + let module = mangle_imports(bytes)?; + + let component = wit_component::ComponentEncoder::default() + .module(module.as_slice())? + // NOTE: the adapter uses the module name `wasi_snapshot_preview1` as it was originally a + // fork of the wasi_snapshot_preview1 adapter. The wasm has a different name to make the + // codebase make more sense, but plumbing that name all the way through the adapter would + // require adjusting all preview1 functions to have a mangled name, like + // "wasi_snapshot_preview1#args_get". + .adapter("wasi_snapshot_preview1", ADAPTER_BYTES)? + .validate(true) + .encode()?; + + Ok(component) +} + +/// We need to ensure that the imports of the core wasm module are all remapped to the single +/// adapter `wasi_snapshot_preview1`, as that allows us to reuse common infrastructure in the +/// adapter's implementation. To accomplish this, we change imports to all come from the +/// `wasi_snapshot_preview1` module, and mangle the function name to +/// `original_module#original_name`. +fn mangle_imports(bytes: &[u8]) -> anyhow::Result { + let mut module = wasm_encoder::Module::new(); + + for payload in wasmparser::Parser::new(0).parse_all(&bytes) { + let payload = payload?; + match payload { + wasmparser::Payload::Version { + encoding: wasmparser::Encoding::Component, + .. + } => { + anyhow::bail!("Mangling only supports core-wasm modules, not components"); + } + + wasmparser::Payload::ImportSection(section) => { + let mut imports = wasm_encoder::ImportSection::new(); + + for import in section { + let import = import?; + let entity = wasm_encoder::EntityType::try_from(import.ty).map_err(|_| { + anyhow::anyhow!( + "Failed to translate type for import {}:{}", + import.module, + import.name + ) + })?; + + // Leave the existing preview1 imports alone + if import.module == "wasi_snapshot_preview1" { + imports.import(import.module, import.name, entity); + } else { + let module = "wasi_snapshot_preview1"; + let name = format!("{}#{}", import.module, import.name); + imports.import(module, &name, entity); + } + } + + module.section(&imports); + } + + payload => { + if let Some((id, range)) = payload.as_section() { + module.section(&wasm_encoder::RawSection { + id, + data: &bytes[range], + }); + } + } + } + } + + Ok(module) +} diff --git a/lib/src/component/config_store.rs b/lib/src/component/config_store.rs new file mode 100644 index 00000000..d0824e0e --- /dev/null +++ b/lib/src/component/config_store.rs @@ -0,0 +1,40 @@ +use { + super::fastly::api::{config_store, types}, + crate::{error, session::Session}, +}; + +#[async_trait::async_trait] +impl config_store::Host for Session { + async fn open(&mut self, name: String) -> Result { + let handle = self.dictionary_handle(name.as_str())?; + Ok(handle.into()) + } + + async fn get( + &mut self, + store: config_store::Handle, + name: String, + max_len: u64, + ) -> Result, types::Error> { + let dict = self + .dictionary(store.into())? + .contents() + .map_err(|err| error::Error::Other(err.into()))?; + + let item = if let Some(item) = dict.get(&name) { + item + } else { + return Ok(None); + }; + + if item.len() > usize::try_from(max_len).unwrap() { + return Err(error::Error::BufferLengthError { + buf: "item_out", + len: "item_max_len", + } + .into()); + } + + Ok(Some(item.clone())) + } +} diff --git a/lib/src/component/dictionary.rs b/lib/src/component/dictionary.rs index f1a22995..4a371a69 100644 --- a/lib/src/component/dictionary.rs +++ b/lib/src/component/dictionary.rs @@ -21,10 +21,11 @@ impl dictionary::Host for Session { .contents() .map_err(|err| error::Error::Other(err.into()))?; - let key = key.as_str(); - let item = dict - .get(key) - .ok_or_else(|| types::Error::from(types::Error::OptionalNone))?; + let item = if let Some(item) = dict.get(&key) { + item + } else { + return Ok(None); + }; if item.len() > usize::try_from(max_len).unwrap() { return Err(error::Error::BufferLengthError { diff --git a/lib/src/component/mod.rs b/lib/src/component/mod.rs index c1d1d2e6..9991dd52 100644 --- a/lib/src/component/mod.rs +++ b/lib/src/component/mod.rs @@ -57,6 +57,7 @@ pub fn link_host_functions(linker: &mut component::Linker) -> anyh fastly::api::secret_store::add_to_linker(linker, |x| x.session())?; fastly::api::types::add_to_linker(linker, |x| x.session())?; fastly::api::uap::add_to_linker(linker, |x| x.session())?; + fastly::api::config_store::add_to_linker(linker, |x| x.session())?; Ok(()) } @@ -64,6 +65,7 @@ pub fn link_host_functions(linker: &mut component::Linker) -> anyh pub mod async_io; pub mod backend; pub mod cache; +pub mod config_store; pub mod dictionary; pub mod error; pub mod geo; diff --git a/lib/src/execute.rs b/lib/src/execute.rs index c3b1fe19..e1c9b925 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -8,6 +8,7 @@ use crate::config::UnknownImportBehavior; use { crate::{ + adapt, body::Body, component as compute, config::{Backends, DeviceDetection, Dictionaries, ExperimentalModule, Geolocation}, @@ -106,6 +107,7 @@ impl ExecuteCtx { wasi_modules: HashSet, guest_profile_path: Option, unknown_import_behavior: UnknownImportBehavior, + adapt_components: bool, ) -> Result { let input = fs::read(&module_path)?; @@ -126,6 +128,22 @@ impl ExecuteCtx { }) ); + // When the input wasn't a component, but we're automatically adapting, + // apply the component adapter. + let (is_component, input) = if !is_component && adapt_components { + // It's not possible to adapt a component from WAT, we can't continue at this point. + if is_wat { + return Err(Error::Other(anyhow::anyhow!( + "Wasm components may only be adapted from binary wasm components, not wat" + ))); + } + + let input = adapt::adapt_bytes(&input)?; + (true, input) + } else { + (is_component, input) + }; + let config = &configure_wasmtime(is_component, profiling_strategy); let engine = Engine::new(config)?; let instance_pre = if is_component { @@ -323,7 +341,8 @@ impl ExecuteCtx { /// # use viceroy_lib::{Error, ExecuteCtx, ProfilingStrategy, ViceroyService}; /// # async fn f() -> Result<(), Error> { /// # let req = Request::new(Body::from("")); - /// let ctx = ExecuteCtx::new("path/to/a/file.wasm", ProfilingStrategy::None, HashSet::new(), None, Default::default())?; + /// let adapt_core_wasm = false; + /// let ctx = ExecuteCtx::new("path/to/a/file.wasm", ProfilingStrategy::None, HashSet::new(), None, Default::default(), adapt_core_wasm)?; /// let resp = ctx.handle_request(req, "127.0.0.1".parse().unwrap()).await?; /// # Ok(()) /// # } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index db972744..5f03d7ae 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -13,6 +13,7 @@ #![cfg_attr(not(debug_assertions), doc(test(attr(allow(dead_code)))))] #![cfg_attr(not(debug_assertions), doc(test(attr(allow(unused_variables)))))] +pub mod adapt; pub mod body; pub mod config; pub mod error; diff --git a/lib/src/service.rs b/lib/src/service.rs index eb9201be..60411f33 100644 --- a/lib/src/service.rs +++ b/lib/src/service.rs @@ -43,7 +43,8 @@ impl ViceroyService { /// # use std::collections::HashSet; /// use viceroy_lib::{Error, ExecuteCtx, ProfilingStrategy, ViceroyService}; /// # fn f() -> Result<(), Error> { - /// let ctx = ExecuteCtx::new("path/to/a/file.wasm", ProfilingStrategy::None, HashSet::new(), None, Default::default())?; + /// let adapt_core_wasm = false; + /// let ctx = ExecuteCtx::new("path/to/a/file.wasm", ProfilingStrategy::None, HashSet::new(), None, Default::default(), adapt_core_wasm)?; /// let svc = ViceroyService::new(ctx); /// # Ok(()) /// # } diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit index 3367d589..dc5066cf 100644 --- a/lib/wit/deps/fastly/compute.wit +++ b/lib/wit/deps/fastly/compute.wit @@ -1055,6 +1055,22 @@ interface cache { get-hits: func(handle: handle) -> result; } +interface config-store { + use types.{error}; + + type handle = u32; + + /// Attempt to open the named config store. + open: func(name: string) -> result; + + /// Fetch a value from the config store, returning `None` if it doesn't exist. + get: func( + store: handle, + key: string, + max-len: u64, + ) -> result, error>; +} + interface reactor { use http-types.{request-handle, body-handle}; @@ -1089,6 +1105,7 @@ world compute { import kv-store; import purge; import secret-store; + import config-store; import uap; export reactor; diff --git a/test-fixtures/Cargo.lock b/test-fixtures/Cargo.lock index 183a04a7..27106833 100644 --- a/test-fixtures/Cargo.lock +++ b/test-fixtures/Cargo.lock @@ -197,9 +197,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -230,18 +230,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote",