diff --git a/Cargo.lock b/Cargo.lock index a3d060d..c673184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -176,6 +191,74 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix", + "winx", +] + [[package]] name = "cc" version = "1.2.63" @@ -200,6 +283,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "cmake" version = "0.1.58" @@ -292,6 +386,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cranelift-assembler-x64" version = "0.132.0" @@ -659,6 +762,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -813,6 +927,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -987,6 +1102,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1121,6 +1260,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.12.0" @@ -1233,6 +1388,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1319,6 +1480,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.1" @@ -1560,7 +1727,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -1614,7 +1781,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -1624,7 +1802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1636,6 +1814,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.12.0" @@ -1805,6 +1989,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + [[package]] name = "rustls" version = "0.23.40" @@ -2031,7 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2995,6 +3189,58 @@ dependencies = [ "wit-parser 0.248.0", ] +[[package]] +name = "wasmtime-wasi" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e92a304eaafd672718011c69084041db74fa0fcc3532c5920f492c557b721" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-std", + "cap-time-ext", + "cfg-if", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rand 0.10.1", + "rustix", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0013e1f37d2e0e1b030fa186972f6f5819f69814bb07d3b6d3cab0c40b50e2" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + [[package]] name = "wast" version = "251.0.0" @@ -3014,7 +3260,7 @@ version = "1.251.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b1086c9e85b95bd6a229a928bc6c6d0662e42af0250c88d067b418831ea4d4" dependencies = [ - "wast", + "wast 251.0.0", ] [[package]] @@ -3074,6 +3320,47 @@ dependencies = [ "tower", "tracing", "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wiggle" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c048e5d47058ff8b60cef771dd6276f2cf4508bc7f604b93c8d5d643ad41fc" +dependencies = [ + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b867ae624c2006976985444321d429ee2d0c6784ca5c6e45bc140c48141bb0c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463d6d4c0c100180fdfc586d555ed1c22376114944841629cff0d746666e30a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", ] [[package]] @@ -3126,6 +3413,41 @@ dependencies = [ "wasmtime-internal-cranelift", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3329,6 +3651,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.52.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3527,6 +3859,18 @@ dependencies = [ "wasmparser 0.248.0", ] +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 097a314..21a92e5 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -16,6 +16,7 @@ thiserror = "2" tokio = { version = "1", features = ["full"] } tracing = "0.1" wasmtime = { version = "45", features = ["component-model"] } +wasmtime-wasi = "45" [dev-dependencies] http-body-util = "0.1" diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 39397d0..e392828 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -3,3 +3,4 @@ pub mod fetch; pub mod http; pub mod model; pub mod service; +pub mod wasm; diff --git a/crates/server/src/wasm.rs b/crates/server/src/wasm.rs new file mode 100644 index 0000000..8c9f405 --- /dev/null +++ b/crates/server/src/wasm.rs @@ -0,0 +1,182 @@ +use std::path::Path; + +use wasmtime::component::{Component, Linker}; +use wasmtime::{Config, Engine, Store}; +use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +use crate::error::HostError; +use crate::model::{Comment, Entry, FetchedResponse, ParseOutcome}; +use crate::service::ProviderHandle; + +wasmtime::component::bindgen!({ + world: "provider", + path: "../../wit", +}); + +use exports::whoareyou::provider::lookup::{ + LookupError as WitLookupError, Response as WitResponse, +}; + +/// How many epoch ticks a guest call may run. The epoch thread ticks every +/// 100 ms → 50 ticks ≈ 5 s budget per call. +const EPOCH_DEADLINE_TICKS: u64 = 50; +pub const EPOCH_TICK: std::time::Duration = std::time::Duration::from_millis(100); + +pub struct HostState { + ctx: WasiCtx, + table: ResourceTable, +} + +impl WasiView for HostState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.ctx, + table: &mut self.table, + } + } +} + +pub fn engine() -> Result { + let mut config = Config::new(); + + config.epoch_interruption(true); + + Ok(Engine::new(&config)?) +} + +pub fn linker(engine: &Engine) -> Result, HostError> { + let mut linker = Linker::new(engine); + + wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?; + + Ok(linker) +} + +/// Spawn the thread that advances the engine epoch so runaway guest calls +/// trap instead of hanging the service. Call once at startup. +pub fn spawn_epoch_thread(engine: &Engine) { + let engine = engine.clone(); + + std::thread::spawn(move || { + loop { + std::thread::sleep(EPOCH_TICK); + engine.increment_epoch(); + } + }); +} + +pub struct WasmProvider { + name: String, + version: String, + engine: Engine, + pre: ProviderPre, +} + +impl WasmProvider { + /// Compile a component from disk and read its metadata once. + /// Fails fast if the component does not satisfy the provider world. + pub fn load( + engine: &Engine, + linker: &Linker, + path: &Path, + ) -> Result { + let component = Component::from_file(engine, path)?; + let pre = ProviderPre::new(linker.instantiate_pre(&component)?)?; + + let mut provider = Self { + name: String::new(), + version: String::new(), + engine: engine.clone(), + pre, + }; + + let mut store = provider.new_store(); + let instance = provider.pre.instantiate(&mut store)?; + let info = instance + .whoareyou_provider_lookup() + .call_metadata(&mut store)?; + + provider.name = info.name; + provider.version = info.version; + + Ok(provider) + } + + pub fn version(&self) -> &str { + &self.version + } + + fn new_store(&self) -> Store { + // No preopens, no env, no inherited stdio — fully sandboxed guest. + let ctx = WasiCtxBuilder::new().build(); + + let mut store = Store::new( + &self.engine, + HostState { + ctx, + table: ResourceTable::new(), + }, + ); + + store.set_epoch_deadline(EPOCH_DEADLINE_TICKS); + + store + } +} + +impl ProviderHandle for WasmProvider { + fn name(&self) -> &str { + &self.name + } + + fn requests(&self, number: &str) -> Result, HostError> { + let mut store = self.new_store(); + let instance = self.pre.instantiate(&mut store)?; + + let requests = instance + .whoareyou_provider_lookup() + .call_requests(&mut store, number)?; + + Ok(requests.into_iter().map(|r| r.url).collect()) + } + + fn parse(&self, number: &str, responses: &[FetchedResponse]) -> ParseOutcome { + let wit_responses: Vec = responses + .iter() + .map(|r| WitResponse { + status: r.status, + body: r.body.clone(), + }) + .collect(); + + let mut store = self.new_store(); + + let result: Result, wasmtime::Error> = (|| { + let instance = self.pre.instantiate(&mut store)?; + + instance + .whoareyou_provider_lookup() + .call_parse(&mut store, number, &wit_responses) + })(); + + match result { + Ok(Ok(entry)) => ParseOutcome::Ok(Entry { + messages: entry.messages, + history: entry.history, + comments: entry + .comments + .into_iter() + .map(|c| Comment { + timestamp: c.timestamp, + title: c.title, + message: c.message, + }) + .collect(), + }), + Ok(Err(WitLookupError::NoData)) => ParseOutcome::NoData, + Ok(Err(WitLookupError::ParseFailed(message))) => ParseOutcome::Failed(message), + // Trap (incl. epoch deadline exceeded) or instantiation failure. + Err(error) => ParseOutcome::Failed(format!("component error: {error}")), + } + } +}