commit 893895a76ccc677caddb7f306e65786b56b67b0f Author: Maofeng Date: Mon Jul 27 04:57:34 2026 +0800 feat(package): add component and WIT package tooling - define the constrained .wasmpkg archive and integrity checks - encode and inspect standard binary WIT packages - resolve exact Registry dependencies with wit.lock and path replace - provide wasmeld pack and wit build/fetch/publish CLI commands diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43314de --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target/ +/dist/ +/var/ +/.idea/ +/components/*/wit/deps/ +/components/*/wit/.deps.* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..be618ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1974 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae05bf9579f45a62e8d0a4e3f52eaa8da518883ac5afa482ec8256c329ecd56" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmeld-package" +version = "0.1.0" +dependencies = [ + "reqwest", + "semver", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror", + "toml", + "wit-component", + "wit-parser", + "zip", +] + +[[package]] +name = "wasmparser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-component" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f9fc53513e461ce51dcf17a3e331752cb829f1d187069e54af5608fc998fe4" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..82d61d5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = [ + "crates/wasmeld-package", +] +default-members = ["crates/wasmeld-package"] +resolver = "2" + +[workspace.package] +edition = "2024" +rust-version = "1.90" +license = "Apache-2.0" + +[workspace.dependencies] +reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +semver = "1.0.28" +sha2 = "0.10.9" +thiserror = "2.0.17" +toml = "0.9.8" +wit-component = "=0.243.0" +wit-parser = "0.243.0" +zip = { version = "2.4.2", default-features = false, features = ["deflate"] } diff --git a/crates/wasmeld-package/Cargo.toml b/crates/wasmeld-package/Cargo.toml new file mode 100644 index 0000000..0c1a354 --- /dev/null +++ b/crates/wasmeld-package/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "wasmeld-package" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +semver.workspace = true +sha2.workspace = true +thiserror.workspace = true +toml.workspace = true +wit-component.workspace = true +wit-parser.workspace = true +zip.workspace = true + +[dev-dependencies] +tempfile = "3.23.0" + +[[bin]] +name = "wasmeld" +path = "src/main.rs" diff --git a/crates/wasmeld-package/src/lib.rs b/crates/wasmeld-package/src/lib.rs new file mode 100644 index 0000000..49e4150 --- /dev/null +++ b/crates/wasmeld-package/src/lib.rs @@ -0,0 +1,356 @@ +//! Component and WIT packaging primitives shared by the Wasmeld CLI, Console, +//! and Runtime. +//! +//! A runnable component is distributed as a constrained `.wasmpkg` ZIP +//! containing exactly [`PACKAGE_MANIFEST_PATH`] and [`COMPONENT_PATH`]. WIT +//! packages use the Component Model binary WIT encoding implemented by the +//! [`wit_package`] module; they are not stored in `.wasmpkg` containers. + +use std::{ + collections::BTreeSet, + io::{self, Cursor, Read, Seek, Write}, +}; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use zip::{CompressionMethod, ZipArchive, ZipWriter, result::ZipError, write::SimpleFileOptions}; + +/// Resolves `wasmeld.toml`, `wit.lock`, Registry dependencies, and path replacements. +pub mod module; +/// Builds and inspects standard binary WIT packages. +pub mod wit_package; + +/// Current `.wasmpkg` manifest schema. +pub const PACKAGE_SCHEMA_VERSION: u32 = 1; +/// Fixed manifest entry name inside a `.wasmpkg` archive. +pub const PACKAGE_MANIFEST_PATH: &str = "package.toml"; +/// Fixed component entry name inside a `.wasmpkg` archive. +pub const COMPONENT_PATH: &str = "component.wasm"; +/// Baseline service world implemented by Wasmeld components. +pub const SERVICE_WORLD: &str = "wasmeld:service/service-component@0.1.0"; +const MAX_MANIFEST_BYTES: usize = 64 * 1024; + +/// Identity and integrity metadata embedded in a `.wasmpkg` archive. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct PackageManifest { + pub schema_version: u32, + pub id: String, + pub revision: String, + pub world: String, + pub component: String, + pub sha256: String, +} + +impl PackageManifest { + /// Creates a manifest and computes the component digest. + pub fn new( + id: impl Into, + revision: impl Into, + world: impl Into, + component: &[u8], + ) -> Self { + Self { + schema_version: PACKAGE_SCHEMA_VERSION, + id: id.into(), + revision: revision.into(), + world: world.into(), + component: COMPONENT_PATH.to_owned(), + sha256: component_sha256(component), + } + } + + /// Validates schema compatibility, identifiers, world syntax, and digest format. + pub fn validate(&self) -> Result<(), PackageError> { + if self.schema_version != PACKAGE_SCHEMA_VERSION { + return Err(PackageError::InvalidPackage(format!( + "unsupported package schema version {}, expected {PACKAGE_SCHEMA_VERSION}", + self.schema_version + ))); + } + validate_identifier("id", &self.id)?; + validate_identifier("revision", &self.revision)?; + validate_world(&self.world)?; + if self.component != COMPONENT_PATH { + return Err(PackageError::InvalidPackage(format!( + "component must be {COMPONENT_PATH:?}" + ))); + } + if self.sha256.len() != 64 + || !self + .sha256 + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(PackageError::InvalidPackage( + "sha256 must be a lowercase SHA-256 digest".to_owned(), + )); + } + Ok(()) + } +} + +/// A validated component package decoded from a `.wasmpkg` archive. +#[derive(Debug)] +pub struct ComponentPackage { + pub manifest: PackageManifest, + pub component: Vec, +} + +/// Errors produced while creating or validating component packages. +#[derive(Debug, Error)] +pub enum PackageError { + #[error("invalid component package: {0}")] + InvalidPackage(String), + + #[error("component exceeds the {limit}-byte uncompressed limit")] + ComponentTooLarge { limit: usize }, + + #[error("component digest mismatch: expected {expected}, found {actual}")] + DigestMismatch { expected: String, actual: String }, + + #[error("failed to read or write package: {0}")] + Io(#[from] io::Error), + + #[error("invalid ZIP container: {0}")] + Zip(#[from] ZipError), + + #[error("invalid package manifest: {0}")] + ManifestParse(#[from] toml::de::Error), + + #[error("failed to serialize package manifest: {0}")] + ManifestSerialize(#[from] toml::ser::Error), +} + +/// Writes a deterministic two-entry `.wasmpkg` archive. +/// +/// The caller owns the destination writer. Runtime limits are intentionally not +/// included because the Console injects platform policy during registration. +pub fn write_package( + writer: W, + id: impl Into, + revision: impl Into, + world: impl Into, + component: &[u8], +) -> Result +where + W: Write + Seek, +{ + if component.is_empty() { + return Err(PackageError::InvalidPackage( + "component must not be empty".to_owned(), + )); + } + + let manifest = PackageManifest::new(id, revision, world, component); + manifest.validate()?; + let manifest_toml = toml::to_string_pretty(&manifest)?; + let options = SimpleFileOptions::default() + .compression_method(CompressionMethod::Deflated) + .unix_permissions(0o644); + let mut archive = ZipWriter::new(writer); + archive.start_file(PACKAGE_MANIFEST_PATH, options)?; + archive.write_all(manifest_toml.as_bytes())?; + archive.start_file(COMPONENT_PATH, options)?; + archive.write_all(component)?; + archive.finish()?; + Ok(manifest) +} + +/// Reads and validates a `.wasmpkg` archive. +/// +/// `max_component_bytes` applies to the uncompressed component so compressed +/// archives cannot bypass the platform's artifact limit. +pub fn read_package( + package_bytes: &[u8], + max_component_bytes: usize, +) -> Result { + let mut archive = ZipArchive::new(Cursor::new(package_bytes))?; + if archive.len() != 2 { + return Err(PackageError::InvalidPackage(format!( + "archive must contain exactly {PACKAGE_MANIFEST_PATH} and {COMPONENT_PATH}" + ))); + } + + let mut names = BTreeSet::new(); + for index in 0..archive.len() { + let entry = archive.by_index(index)?; + let name = entry.name().to_owned(); + if name != PACKAGE_MANIFEST_PATH && name != COMPONENT_PATH { + return Err(PackageError::InvalidPackage(format!( + "unexpected archive entry {name:?}" + ))); + } + if !names.insert(name.clone()) { + return Err(PackageError::InvalidPackage(format!( + "duplicate archive entry {name:?}" + ))); + } + } + if !names.contains(PACKAGE_MANIFEST_PATH) || !names.contains(COMPONENT_PATH) { + return Err(PackageError::InvalidPackage(format!( + "archive must contain {PACKAGE_MANIFEST_PATH} and {COMPONENT_PATH}" + ))); + } + + let manifest_bytes = read_entry(&mut archive, PACKAGE_MANIFEST_PATH, MAX_MANIFEST_BYTES) + .map_err(|error| match error { + PackageError::ComponentTooLarge { .. } => PackageError::InvalidPackage(format!( + "{PACKAGE_MANIFEST_PATH} exceeds the {MAX_MANIFEST_BYTES}-byte limit" + )), + other => other, + })?; + let manifest = toml::from_slice::(&manifest_bytes)?; + manifest.validate()?; + + let component = read_entry(&mut archive, COMPONENT_PATH, max_component_bytes)?; + if component.is_empty() { + return Err(PackageError::InvalidPackage( + "component must not be empty".to_owned(), + )); + } + let actual = component_sha256(&component); + if actual != manifest.sha256 { + return Err(PackageError::DigestMismatch { + expected: manifest.sha256, + actual, + }); + } + + Ok(ComponentPackage { + manifest, + component, + }) +} + +fn read_entry( + archive: &mut ZipArchive, + name: &str, + limit: usize, +) -> Result, PackageError> { + let entry = archive.by_name(name)?; + if entry.size() > limit as u64 { + return Err(PackageError::ComponentTooLarge { limit }); + } + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .take(limit.saturating_add(1) as u64) + .read_to_end(&mut bytes)?; + if bytes.len() > limit { + return Err(PackageError::ComponentTooLarge { limit }); + } + Ok(bytes) +} + +fn component_sha256(component: &[u8]) -> String { + format!("{:x}", Sha256::digest(component)) +} + +fn validate_identifier(name: &str, value: &str) -> Result<(), PackageError> { + let valid = !value.is_empty() + && value.len() <= 128 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')); + if valid { + Ok(()) + } else { + Err(PackageError::InvalidPackage(format!( + "{name} must contain only ASCII letters, digits, '.', '_' or '-' and be at most 128 bytes" + ))) + } +} + +/// Validates the canonical `namespace:package/world@version` world form. +pub fn validate_world(world: &str) -> Result<(), PackageError> { + let valid = !world.is_empty() + && world.len() <= 256 + && world.contains(':') + && world.contains('/') + && world.contains('@') + && world.bytes().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'/' | b'@' | b'-' | b'_' | b'.') + }); + if valid { + Ok(()) + } else { + Err(PackageError::InvalidPackage( + "world must be a versioned WIT world path and be at most 256 bytes".to_owned(), + )) + } +} + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Write}; + + use zip::{ZipWriter, write::SimpleFileOptions}; + + use super::*; + + #[test] + fn package_round_trip() { + let mut bytes = Cursor::new(Vec::new()); + let written = + write_package(&mut bytes, "echo", "0.1.0", SERVICE_WORLD, b"component").unwrap(); + let package = read_package(bytes.get_ref(), 1024).unwrap(); + + assert_eq!(package.manifest, written); + assert_eq!(package.component, b"component"); + } + + #[test] + fn rejects_unexpected_archive_entries() { + let mut bytes = Cursor::new(Vec::new()); + let mut archive = ZipWriter::new(&mut bytes); + let options = SimpleFileOptions::default(); + archive.start_file(PACKAGE_MANIFEST_PATH, options).unwrap(); + archive.write_all(b"invalid").unwrap(); + archive.start_file(COMPONENT_PATH, options).unwrap(); + archive.write_all(b"component").unwrap(); + archive.start_file("../escape", options).unwrap(); + archive.write_all(b"escape").unwrap(); + archive.finish().unwrap(); + + let error = read_package(bytes.get_ref(), 1024).unwrap_err(); + assert!(error.to_string().contains("exactly")); + } + + #[test] + fn enforces_the_uncompressed_component_limit() { + let mut bytes = Cursor::new(Vec::new()); + write_package(&mut bytes, "echo", "0.1.0", SERVICE_WORLD, b"component").unwrap(); + + let error = read_package(bytes.get_ref(), 4).unwrap_err(); + assert!(matches!( + error, + PackageError::ComponentTooLarge { limit: 4 } + )); + } + + #[test] + fn rejects_unsafe_service_identifiers() { + let mut bytes = Cursor::new(Vec::new()); + let error = write_package( + &mut bytes, + "../escape", + "0.1.0", + SERVICE_WORLD, + b"component", + ) + .unwrap_err(); + assert!(error.to_string().contains("ASCII letters")); + } + + #[test] + fn accepts_component_specific_worlds() { + let manifest = PackageManifest::new( + "clock-probe", + "0.1.0", + "component:clock-probe/clock-probe-component@0.1.0", + b"component", + ); + manifest.validate().unwrap(); + } +} diff --git a/crates/wasmeld-package/src/main.rs b/crates/wasmeld-package/src/main.rs new file mode 100644 index 0000000..6c2cedf --- /dev/null +++ b/crates/wasmeld-package/src/main.rs @@ -0,0 +1,454 @@ +//! Command-line entry point for Component packaging and WIT dependency workflows. + +use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, ExitCode}, +}; + +use serde::Deserialize; +use wasmeld_package::{ + PackageManifest, + module::{MODULE_MANIFEST_FILE, ModuleManifest, find_module_manifest, sync_dependencies}, + wit_package::{WitPackageMetadata, build_wit_package}, + write_package, +}; + +const TARGET: &str = "wasm32-wasip2"; + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec, + target_directory: PathBuf, + workspace_root: PathBuf, +} + +#[derive(Debug, Deserialize)] +struct CargoPackage { + manifest_path: PathBuf, + version: String, + metadata: serde_json::Value, + targets: Vec, +} + +#[derive(Debug, Deserialize)] +struct CargoTarget { + name: String, + crate_types: Vec, +} + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("wasmeld: {error}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), Box> { + let mut arguments = env::args().skip(1); + match arguments.next().as_deref() { + Some("pack") => run_pack(arguments.collect()), + Some("wit") => run_wit(arguments.collect()), + _ => Err(usage().into()), + } +} + +fn run_pack(arguments: Vec) -> Result<(), Box> { + let mut arguments = arguments.into_iter(); + let manifest_path = arguments.next().ok_or_else(usage)?; + let mut output = None; + let mut no_build = false; + let mut locked = false; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--output" | "-o" => { + output = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--output requires a path")?, + ); + } + "--no-build" => no_build = true, + "--locked" => locked = true, + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + + let manifest_path = fs::canonicalize(manifest_path)?; + sync_component_dependencies(&manifest_path, locked)?; + let metadata = cargo_metadata(&manifest_path)?; + let package = metadata + .packages + .iter() + .find(|package| package.manifest_path == manifest_path) + .ok_or("Cargo metadata did not contain the requested package")?; + let id = package + .metadata + .get("wasmeld") + .and_then(|metadata| metadata.get("id")) + .and_then(serde_json::Value::as_str) + .ok_or("[package.metadata.wasmeld].id is required")?; + let world = package + .metadata + .get("wasmeld") + .and_then(|metadata| metadata.get("world")) + .and_then(serde_json::Value::as_str) + .ok_or("[package.metadata.wasmeld].world is required")?; + PackageManifest::new(id, &package.version, world, b"identity-validation").validate()?; + let target = package + .targets + .iter() + .find(|target| target.crate_types.iter().any(|kind| kind == "cdylib")) + .ok_or("component package must expose a cdylib target")?; + + if !no_build { + build_component(&manifest_path)?; + } + + let artifact = metadata + .target_directory + .join(TARGET) + .join("release") + .join(format!("{}.wasm", target.name.replace('-', "_"))); + let component = fs::read(&artifact).map_err(|error| { + format!( + "failed to read built component {}: {error}", + artifact.display() + ) + })?; + let output = output.unwrap_or_else(|| { + metadata + .workspace_root + .join("dist") + .join(format!("{id}-{}.wasmpkg", package.version)) + }); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent)?; + } + let file = fs::File::create(&output)?; + let manifest = write_package(file, id, &package.version, world, &component)?; + + println!( + "packed {}@{} -> {}", + manifest.id, + manifest.revision, + output.display() + ); + println!("component: {}", artifact.display()); + println!("sha256: {}", manifest.sha256); + Ok(()) +} + +fn run_wit(arguments: Vec) -> Result<(), Box> { + let mut arguments = arguments.into_iter(); + match arguments.next().as_deref() { + Some("fetch" | "tidy") => { + let options = parse_wit_sync_options(arguments)?; + let manifest = resolve_module_manifest(options.manifest)?; + print_sync_report(&manifest, options.locked) + } + Some("graph") => { + let manifest = resolve_module_manifest(parse_manifest_option(arguments)?)?; + print_dependency_graph(&manifest) + } + Some("replace") => run_wit_replace(arguments), + Some("build") => run_wit_build(arguments), + Some("publish") => run_wit_publish(arguments), + _ => Err(usage().into()), + } +} + +struct WitSyncOptions { + manifest: Option, + locked: bool, +} + +fn parse_wit_sync_options( + mut arguments: impl Iterator, +) -> Result> { + let mut manifest = None; + let mut locked = false; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--manifest" => { + manifest = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--manifest requires a path")?, + ); + } + "--locked" => locked = true, + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + Ok(WitSyncOptions { manifest, locked }) +} + +fn parse_manifest_option( + mut arguments: impl Iterator, +) -> Result, Box> { + let mut manifest = None; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--manifest" => { + manifest = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--manifest requires a path")?, + ); + } + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + Ok(manifest) +} + +fn run_wit_replace( + mut arguments: impl Iterator, +) -> Result<(), Box> { + let package = arguments.next().ok_or("wit replace requires a package")?; + let mut manifest_path = None; + let mut replacement_path = None; + let mut drop = false; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--manifest" => { + manifest_path = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--manifest requires a path")?, + ); + } + "--path" => { + replacement_path = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--path requires a directory")?, + ); + } + "--drop" => drop = true, + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + if drop == replacement_path.is_some() { + return Err("wit replace requires exactly one of --path or --drop".into()); + } + + let manifest_path = resolve_module_manifest(manifest_path)?; + let mut manifest = ModuleManifest::read(&manifest_path)?; + if drop { + if !manifest.drop_replacement(&package) { + return Err(format!("replacement {package:?} does not exist").into()); + } + manifest.write(&manifest_path)?; + println!("dropped replace {package}"); + } else { + let replacement_path = replacement_path.expect("validated above"); + manifest.set_path_replacement(&package, &replacement_path)?; + manifest.write(&manifest_path)?; + println!("replaced {package} => path+{}", replacement_path.display()); + } + println!("manifest: {}", manifest_path.display()); + Ok(()) +} + +fn run_wit_build( + mut arguments: impl Iterator, +) -> Result<(), Box> { + let source = arguments.next().ok_or("wit build requires a source path")?; + let mut output = None; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--output" | "-o" => { + output = Some( + arguments + .next() + .map(PathBuf::from) + .ok_or("--output requires a path")?, + ); + } + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + let package = build_wit_package(&source)?; + let output = output.unwrap_or_else(|| { + let basename = package.metadata.name.replace(':', "-"); + PathBuf::from("dist/wit").join(format!("{basename}-{}.wasm", package.metadata.version)) + }); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&output, &package.bytes)?; + println!( + "built {}@{} -> {}", + package.metadata.name, + package.metadata.version, + output.display() + ); + println!("sha256: {}", package.metadata.sha256); + Ok(()) +} + +fn run_wit_publish( + mut arguments: impl Iterator, +) -> Result<(), Box> { + let source = arguments + .next() + .ok_or("wit publish requires a source path")?; + let mut registry = env::var("WASMELD_REGISTRY").ok(); + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--registry" => { + registry = Some( + arguments + .next() + .ok_or("--registry requires an HTTP(S) URL")?, + ); + } + _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()), + } + } + let registry = registry + .ok_or("wit publish requires --registry or the WASMELD_REGISTRY environment variable")?; + let registry = registry.trim_end_matches('/'); + let package = build_wit_package(&source)?; + let url = format!("{registry}/api/v1/wit/packages"); + let part = reqwest::blocking::multipart::Part::bytes(package.bytes) + .file_name("package.wasm") + .mime_str("application/wasm")?; + let response = reqwest::blocking::Client::builder() + .user_agent(format!("wasmeld/{}", env!("CARGO_PKG_VERSION"))) + .build()? + .post(&url) + .multipart(reqwest::blocking::multipart::Form::new().part("package", part)) + .send()?; + let status = response.status(); + if !status.is_success() { + return Err(format!("registry returned HTTP {status}: {}", response.text()?).into()); + } + let published = response.json::()?; + if published != package.metadata { + return Err(format!( + "registry metadata mismatch: expected {}@{} ({})", + package.metadata.name, package.metadata.version, package.metadata.sha256 + ) + .into()); + } + println!( + "published {}@{} -> {registry}", + published.name, published.version + ); + println!("sha256: {}", published.sha256); + Ok(()) +} + +fn resolve_module_manifest( + manifest: Option, +) -> Result> { + match manifest { + Some(path) => Ok(fs::canonicalize(path)?), + None => Ok(find_module_manifest(env::current_dir()?)?), + } +} + +fn sync_component_dependencies( + cargo_manifest: &Path, + locked: bool, +) -> Result<(), Box> { + let module_manifest = cargo_manifest.with_file_name(MODULE_MANIFEST_FILE); + if module_manifest.is_file() { + print_sync_report(&module_manifest, locked)?; + } + Ok(()) +} + +fn print_sync_report(manifest: &Path, locked: bool) -> Result<(), Box> { + let report = sync_dependencies(manifest, locked)?; + let action = if locked { "verified" } else { "resolved" }; + println!( + "{action} {} WIT package(s) -> {}", + report.packages.len(), + report.wit_root.join("deps").display() + ); + for package in report.packages { + println!( + " {}@{} => {} ({})", + package.name, package.version, package.source, package.sha256 + ); + } + println!("lock: {}", report.lock_path.display()); + Ok(()) +} + +fn print_dependency_graph(manifest_path: &Path) -> Result<(), Box> { + let manifest = ModuleManifest::read(manifest_path)?; + println!("{}", manifest_path.display()); + for (name, version) in &manifest.dependencies { + let exact = format!("{name}@{version}"); + let replacement = manifest + .replacements + .get(&exact) + .or_else(|| manifest.replacements.get(name)); + match replacement { + Some(replacement) => { + println!(" {exact} => path+{}", replacement.path.display()); + } + None => match &manifest.registry.url { + Some(registry) => println!(" {exact} => registry+{registry}"), + None => println!(" {exact} => registry (unconfigured)"), + }, + } + } + Ok(()) +} + +fn cargo_metadata(manifest_path: &Path) -> Result> { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--no-deps"]) + .arg("--manifest-path") + .arg(manifest_path) + .output()?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + Ok(serde_json::from_slice(&output.stdout)?) +} + +fn build_component(manifest_path: &Path) -> Result<(), Box> { + let toolchain = env::var("WASMELD_COMPONENT_TOOLCHAIN").unwrap_or_else(|_| "1.90.0".to_owned()); + let status = Command::new("rustup") + .args(["run", &toolchain, "cargo", "build"]) + .arg("--manifest-path") + .arg(manifest_path) + .args(["--target", TARGET, "--release"]) + .status()?; + if !status.success() { + return Err(format!("component build failed with status {status}").into()); + } + Ok(()) +} + +fn usage() -> String { + "usage: + wasmeld pack [--output ] [--no-build] [--locked] + wasmeld wit fetch [--manifest ] [--locked] + wasmeld wit tidy [--manifest ] [--locked] + wasmeld wit graph [--manifest ] + wasmeld wit replace --path [--manifest ] + wasmeld wit replace --drop [--manifest ] + wasmeld wit build [--output ] + wasmeld wit publish --registry " + .to_owned() +} diff --git a/crates/wasmeld-package/src/module.rs b/crates/wasmeld-package/src/module.rs new file mode 100644 index 0000000..d13226d --- /dev/null +++ b/crates/wasmeld-package/src/module.rs @@ -0,0 +1,1036 @@ +//! Go-module-style dependency management for WIT packages. +//! +//! A component declares exact dependencies in `wasmeld.toml`. Resolution +//! prefers root-level path replacements and otherwise downloads immutable +//! binary packages from the configured Registry. The complete transitive graph +//! is materialized under `wit/deps` and recorded in `wit.lock`. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + io::{self, Read}, + path::{Component, Path, PathBuf}, +}; + +use semver::Version; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use wit_parser::{Resolve, UnresolvedPackageGroup}; + +use crate::wit_package::{WitDependency, inspect_wit_package}; + +/// Component dependency manifest filename. +pub const MODULE_MANIFEST_FILE: &str = "wasmeld.toml"; +/// Resolved dependency lock filename. +pub const MODULE_LOCK_FILE: &str = "wit.lock"; +/// Current `wasmeld.toml` schema. +pub const MODULE_SCHEMA_VERSION: u32 = 1; +/// Current `wit.lock` schema. +pub const LOCK_SCHEMA_VERSION: u32 = 1; +const MAX_FETCHED_WIT_PACKAGE_BYTES: usize = 16 * 1024 * 1024; + +/// Parsed `wasmeld.toml` dependency configuration. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ModuleManifest { + /// Manifest format version. + pub schema_version: u32, + /// Local WIT source and generated dependency layout. + #[serde(default)] + pub wit: WitConfig, + /// Exact root dependencies keyed by `namespace:package`. + #[serde(default)] + pub dependencies: BTreeMap, + /// Registry used for dependencies without a path replacement. + #[serde(default, skip_serializing_if = "RegistryConfig::is_empty")] + pub registry: RegistryConfig, + /// Root-controlled package or package-version path replacements. + #[serde(default, rename = "replace")] + pub replacements: BTreeMap, +} + +/// Local WIT source layout. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WitConfig { + /// WIT root relative to `wasmeld.toml`; defaults to `wit`. + #[serde(default = "default_wit_root")] + pub root: PathBuf, +} + +impl Default for WitConfig { + fn default() -> Self { + Self { + root: default_wit_root(), + } + } +} + +/// Local source override for a Registry package. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct PathReplacement { + /// Replacement package directory relative to `wasmeld.toml`. + pub path: PathBuf, +} + +/// Remote WIT Registry configuration. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct RegistryConfig { + /// Base HTTP(S) URL of a Wasmeld Console instance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +impl RegistryConfig { + fn is_empty(&self) -> bool { + self.url.is_none() + } +} + +/// Complete dependency graph captured in `wit.lock`. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ModuleLock { + /// Lock format version. + pub schema_version: u32, + /// Direct and transitive packages in deterministic identity order. + #[serde(default, rename = "package")] + pub packages: Vec, +} + +/// Reproducibility data for one resolved package. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LockedPackage { + /// Package identity in `namespace:package` form. + pub name: String, + /// Exact semantic version. + pub version: String, + /// `registry+URL` or `path+relative/path`. + pub source: String, + /// Digest of the binary package or normalized local WIT source. + pub sha256: String, + /// Whether the root manifest selected this package through `replace`. + pub replaced: bool, +} + +/// One materialized package returned after dependency synchronization. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResolvedPackage { + /// Package identity in `namespace:package` form. + pub name: String, + /// Exact semantic version. + pub version: String, + /// Effective Registry or path source. + pub source: String, + /// Whether the package came from a root path replacement. + pub replaced: bool, + /// Final location under the component's `wit/deps`. + pub materialized_path: PathBuf, + /// Content digest written to the lock file. + pub sha256: String, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct DependencyId { + name: String, + version: String, +} + +/// Result of resolving and materializing a component's WIT dependencies. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SyncReport { + /// Canonical `wasmeld.toml` path. + pub manifest_path: PathBuf, + /// Adjacent `wit.lock` path. + pub lock_path: PathBuf, + /// Effective local WIT root. + pub wit_root: PathBuf, + /// Complete direct and transitive dependency graph. + pub packages: Vec, +} + +/// Errors raised while parsing, resolving, locking, or materializing WIT dependencies. +#[derive(Debug, Error)] +pub enum ModuleError { + #[error("invalid WIT module: {0}")] + InvalidManifest(String), + + #[error("failed to read {path}: {source}")] + Read { path: PathBuf, source: io::Error }, + + #[error("failed to write {path}: {source}")] + Write { path: PathBuf, source: io::Error }, + + #[error("failed to parse {path}: {source}")] + ParseManifest { + path: PathBuf, + source: toml::de::Error, + }, + + #[error("failed to serialize {path}: {source}")] + SerializeManifest { + path: PathBuf, + source: toml::ser::Error, + }, + + #[error("invalid WIT package at {path}: {message}")] + InvalidWitPackage { path: PathBuf, message: String }, + + #[error("locked dependency graph differs from {path}; run `wasmeld wit tidy`")] + LockMismatch { path: PathBuf }, + + #[error("WIT registry request to {url} failed: {message}")] + Registry { url: String, message: String }, +} + +impl ModuleManifest { + /// Reads and validates a `wasmeld.toml` manifest. + pub fn read(path: impl AsRef) -> Result { + let path = path.as_ref(); + let source = fs::read_to_string(path).map_err(|source| ModuleError::Read { + path: path.to_path_buf(), + source, + })?; + let manifest = + toml::from_str::(&source).map_err(|source| ModuleError::ParseManifest { + path: path.to_path_buf(), + source, + })?; + manifest.validate()?; + Ok(manifest) + } + + /// Validates and writes a canonical TOML manifest. + pub fn write(&self, path: impl AsRef) -> Result<(), ModuleError> { + self.validate()?; + let path = path.as_ref(); + let mut source = + toml::to_string_pretty(self).map_err(|source| ModuleError::SerializeManifest { + path: path.to_path_buf(), + source, + })?; + if !source.ends_with('\n') { + source.push('\n'); + } + fs::write(path, source).map_err(|source| ModuleError::Write { + path: path.to_path_buf(), + source, + }) + } + + /// Validates schema, exact versions, package names, paths, and Registry URL. + pub fn validate(&self) -> Result<(), ModuleError> { + if self.schema_version != MODULE_SCHEMA_VERSION { + return Err(ModuleError::InvalidManifest(format!( + "unsupported wasmeld.toml schema version {}, expected {MODULE_SCHEMA_VERSION}", + self.schema_version + ))); + } + validate_wit_root(&self.wit.root)?; + + for (name, version) in &self.dependencies { + validate_package_name(name)?; + parse_version(version)?; + } + + for (key, replacement) in &self.replacements { + parse_replacement_key(key)?; + validate_replacement_path(&format!("replace.{key}.path"), &replacement.path)?; + } + if let Some(url) = &self.registry.url { + validate_registry_url(url)?; + } + Ok(()) + } + + /// Sets a package-wide or exact-version path replacement. + pub fn set_path_replacement( + &mut self, + package: impl Into, + path: impl Into, + ) -> Result<(), ModuleError> { + let package = package.into(); + let path = path.into(); + parse_replacement_key(&package)?; + validate_replacement_path("replacement path", &path)?; + self.replacements.insert(package, PathReplacement { path }); + Ok(()) + } + + /// Removes a path replacement and reports whether it existed. + pub fn drop_replacement(&mut self, package: &str) -> bool { + self.replacements.remove(package).is_some() + } +} + +impl ModuleLock { + /// Reads a lock file and rejects unsupported lock schemas. + pub fn read(path: impl AsRef) -> Result { + let path = path.as_ref(); + let source = fs::read_to_string(path).map_err(|source| ModuleError::Read { + path: path.to_path_buf(), + source, + })?; + let lock = + toml::from_str::(&source).map_err(|source| ModuleError::ParseManifest { + path: path.to_path_buf(), + source, + })?; + if lock.schema_version != LOCK_SCHEMA_VERSION { + return Err(ModuleError::InvalidManifest(format!( + "unsupported wit.lock schema version {}, expected {LOCK_SCHEMA_VERSION}", + lock.schema_version + ))); + } + Ok(lock) + } + + fn write(&self, path: &Path) -> Result<(), ModuleError> { + let mut source = + toml::to_string_pretty(self).map_err(|source| ModuleError::SerializeManifest { + path: path.to_path_buf(), + source, + })?; + if !source.ends_with('\n') { + source.push('\n'); + } + fs::write(path, source).map_err(|source| ModuleError::Write { + path: path.to_path_buf(), + source, + }) + } +} + +/// Resolves, locks, and materializes all dependencies declared by a component. +/// +/// Resolution is deterministic and recursively follows direct package +/// dependencies embedded in Registry artifacts. With `locked = true`, the +/// computed graph must exactly match the existing `wit.lock`; the function does +/// not update the lock file. +pub fn sync_dependencies( + manifest_path: impl AsRef, + locked: bool, +) -> Result { + let manifest_path = canonical_file(manifest_path.as_ref())?; + let manifest = ModuleManifest::read(&manifest_path)?; + let module_root = manifest_path + .parent() + .expect("a canonical manifest path always has a parent"); + let wit_root = module_root.join(&manifest.wit.root); + if !wit_root.is_dir() { + return Err(ModuleError::InvalidManifest(format!( + "WIT root {} is not a directory", + wit_root.display() + ))); + } + + let mut packages = Vec::new(); + let mut materialized_names = BTreeSet::new(); + let mut pending = manifest + .dependencies + .iter() + .map(|(name, version)| DependencyId { + name: name.clone(), + version: version.clone(), + }) + .collect::>(); + let mut resolved = BTreeSet::new(); + // Resolve into a staging tree so a failed download or validation never + // leaves a partially updated wit/deps directory. + let staging = wit_root.join(format!(".deps.tmp-{}", std::process::id())); + remove_directory_if_present(&staging)?; + create_directory(&staging)?; + let registry_url = manifest + .registry + .url + .as_deref() + .map(normalize_registry_url) + .transpose()?; + let client = reqwest::blocking::Client::builder() + .user_agent(format!("wasmeld/{}", env!("CARGO_PKG_VERSION"))) + .build() + .map_err(|error| ModuleError::Registry { + url: registry_url.clone().unwrap_or_default(), + message: error.to_string(), + })?; + + let result = (|| { + while let Some(dependency) = pending.pop_first() { + if !resolved.insert(dependency.clone()) { + continue; + } + let directory_name = materialized_directory_name(&dependency.name, &dependency.version); + if !materialized_names.insert(directory_name.clone()) { + return Err(ModuleError::InvalidManifest(format!( + "dependencies produce the same materialized directory {directory_name:?}" + ))); + } + + // Only the root manifest controls replacement. Exact-version keys + // take precedence over package-wide keys in select_replacement. + let replacement = select_replacement(&manifest, &dependency.name, &dependency.version); + let (source, replaced, sha256, transitive, materialized_path) = + if let Some(replacement) = replacement { + let source_path = canonical_directory(&module_root.join(&replacement.path))?; + let unresolved = + UnresolvedPackageGroup::parse_path(&source_path).map_err(|error| { + ModuleError::InvalidWitPackage { + path: source_path.clone(), + message: error.to_string(), + } + })?; + let actual_name = format!( + "{}:{}", + unresolved.main.name.namespace, unresolved.main.name.name + ); + let actual_version = unresolved + .main + .name + .version + .as_ref() + .map(ToString::to_string) + .ok_or_else(|| ModuleError::InvalidWitPackage { + path: source_path.clone(), + message: "package must declare an explicit semantic version".to_owned(), + })?; + if actual_name != dependency.name || actual_version != dependency.version { + return Err(ModuleError::InvalidWitPackage { + path: source_path, + message: format!( + "expected {}@{}, found {actual_name}@{actual_version}", + dependency.name, dependency.version + ), + }); + } + let transitive = unresolved + .main + .foreign_deps + .keys() + .map(dependency_from_package_name) + .collect::, _>>()?; + let destination = staging.join(&directory_name); + let sha256 = copy_wit_package(&source_path, &destination)?; + ( + format!("path+{}", replacement.path.display()), + true, + sha256, + transitive, + wit_root.join("deps").join(directory_name), + ) + } else { + let registry_url = registry_url.as_deref().ok_or_else(|| { + ModuleError::InvalidManifest(format!( + "dependency {}@{} has no replace and [registry].url is not configured", + dependency.name, dependency.version + )) + })?; + let (metadata, bytes) = + fetch_registry_package(&client, registry_url, &dependency)?; + let file_name = format!("{directory_name}.wasm"); + let destination = staging.join(&file_name); + fs::write(&destination, bytes).map_err(|source| ModuleError::Write { + path: destination, + source, + })?; + ( + format!("registry+{registry_url}"), + false, + metadata.sha256, + metadata + .dependencies + .iter() + .map(dependency_from_metadata) + .collect::, _>>()?, + wit_root.join("deps").join(file_name), + ) + }; + + for dependency in transitive { + if !resolved.contains(&dependency) { + pending.insert(dependency); + } + } + packages.push(ResolvedPackage { + name: dependency.name, + version: dependency.version, + source, + replaced, + materialized_path, + sha256, + }); + } + + Ok(()) + })(); + + if result.is_err() { + let _ = fs::remove_dir_all(&staging); + } + result?; + + let lock_path = manifest_path.with_file_name(MODULE_LOCK_FILE); + let expected_lock = ModuleLock { + schema_version: LOCK_SCHEMA_VERSION, + packages: packages + .iter() + .map(|package| LockedPackage { + name: package.name.clone(), + version: package.version.clone(), + source: package.source.clone(), + sha256: package.sha256.clone(), + replaced: package.replaced, + }) + .collect(), + }; + + // Check the complete graph before installing staging. A locked build must + // not change either wit.lock or the currently materialized dependencies. + if locked { + let actual_lock = match ModuleLock::read(&lock_path) { + Ok(lock) => lock, + Err(error) => { + remove_directory_if_present(&staging)?; + return Err(error); + } + }; + if actual_lock != expected_lock { + remove_directory_if_present(&staging)?; + return Err(ModuleError::LockMismatch { path: lock_path }); + } + } + + install_and_validate_dependencies(&wit_root, &staging)?; + if !locked { + expected_lock.write(&lock_path)?; + } + + Ok(SyncReport { + manifest_path, + lock_path, + wit_root, + packages, + }) +} + +/// Searches `start` and its ancestors for the nearest `wasmeld.toml`. +pub fn find_module_manifest(start: impl AsRef) -> Result { + let mut current = canonical_directory(start.as_ref())?; + loop { + let candidate = current.join(MODULE_MANIFEST_FILE); + if candidate.is_file() { + return Ok(candidate); + } + if !current.pop() { + return Err(ModuleError::InvalidManifest(format!( + "could not find {MODULE_MANIFEST_FILE} from {}", + start.as_ref().display() + ))); + } + } +} + +fn default_wit_root() -> PathBuf { + PathBuf::from("wit") +} + +fn select_replacement<'a>( + manifest: &'a ModuleManifest, + name: &str, + version: &str, +) -> Option<&'a PathReplacement> { + let exact = format!("{name}@{version}"); + manifest + .replacements + .get(&exact) + .or_else(|| manifest.replacements.get(name)) +} + +fn dependency_from_package_name( + package: &wit_parser::PackageName, +) -> Result { + let name = format!("{}:{}", package.namespace, package.name); + let version = package + .version + .as_ref() + .map(ToString::to_string) + .ok_or_else(|| { + ModuleError::InvalidManifest(format!( + "transitive WIT dependency {name} must declare an exact version" + )) + })?; + Ok(DependencyId { name, version }) +} + +fn dependency_from_metadata(dependency: &WitDependency) -> Result { + validate_package_name(&dependency.name)?; + parse_version(&dependency.version)?; + Ok(DependencyId { + name: dependency.name.clone(), + version: dependency.version.clone(), + }) +} + +fn fetch_registry_package( + client: &reqwest::blocking::Client, + registry_url: &str, + dependency: &DependencyId, +) -> Result<(crate::wit_package::WitPackageMetadata, Vec), ModuleError> { + let (namespace, name) = dependency + .name + .split_once(':') + .expect("validated package names always contain a namespace"); + let url = format!( + "{registry_url}/api/v1/wit/packages/{namespace}/{name}/{}/content", + dependency.version + ); + let mut response = client + .get(&url) + .send() + .map_err(|error| ModuleError::Registry { + url: url.clone(), + message: error.to_string(), + })?; + let status = response.status(); + if !status.is_success() { + let message = response + .text() + .unwrap_or_else(|error| format!("failed to read error response: {error}")); + return Err(ModuleError::Registry { + url, + message: format!("HTTP {status}: {message}"), + }); + } + if response + .content_length() + .is_some_and(|length| length > MAX_FETCHED_WIT_PACKAGE_BYTES as u64) + { + return Err(ModuleError::Registry { + url, + message: format!( + "package exceeds the {MAX_FETCHED_WIT_PACKAGE_BYTES}-byte client limit" + ), + }); + } + let mut bytes = Vec::new(); + response + .by_ref() + .take(MAX_FETCHED_WIT_PACKAGE_BYTES.saturating_add(1) as u64) + .read_to_end(&mut bytes) + .map_err(|error| ModuleError::Registry { + url: url.clone(), + message: error.to_string(), + })?; + if bytes.len() > MAX_FETCHED_WIT_PACKAGE_BYTES { + return Err(ModuleError::Registry { + url, + message: format!( + "package exceeds the {MAX_FETCHED_WIT_PACKAGE_BYTES}-byte client limit" + ), + }); + } + let metadata = inspect_wit_package(&bytes).map_err(|error| ModuleError::Registry { + url: url.clone(), + message: error.to_string(), + })?; + if metadata.name != dependency.name || metadata.version != dependency.version { + return Err(ModuleError::Registry { + url, + message: format!( + "expected {}@{}, received {}@{}", + dependency.name, dependency.version, metadata.name, metadata.version + ), + }); + } + Ok((metadata, bytes)) +} + +fn parse_replacement_key(key: &str) -> Result<(&str, Option<&str>), ModuleError> { + if let Some((name, version)) = key.rsplit_once('@') { + validate_package_name(name)?; + parse_version(version)?; + Ok((name, Some(version))) + } else { + validate_package_name(key)?; + Ok((key, None)) + } +} + +fn validate_package_name(name: &str) -> Result<(), ModuleError> { + let Some((namespace, package)) = name.split_once(':') else { + return Err(ModuleError::InvalidManifest(format!( + "WIT package {name:?} must use namespace:name" + ))); + }; + if package.contains(':') + || !valid_kebab_identifier(namespace) + || !valid_kebab_identifier(package) + { + return Err(ModuleError::InvalidManifest(format!( + "invalid WIT package name {name:?}" + ))); + } + Ok(()) +} + +fn valid_kebab_identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= 128 + && !value.starts_with('-') + && !value.ends_with('-') + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +fn parse_version(version: &str) -> Result { + Version::parse(version).map_err(|error| { + ModuleError::InvalidManifest(format!("invalid exact version {version:?}: {error}")) + }) +} + +fn validate_registry_url(url: &str) -> Result<(), ModuleError> { + normalize_registry_url(url).map(|_| ()) +} + +fn normalize_registry_url(url: &str) -> Result { + let normalized = url.trim_end_matches('/'); + let parsed = reqwest::Url::parse(normalized).map_err(|error| { + ModuleError::InvalidManifest(format!("invalid [registry].url {url:?}: {error}")) + })?; + if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() { + return Err(ModuleError::InvalidManifest(format!( + "[registry].url must be an absolute HTTP(S) URL, found {url:?}" + ))); + } + Ok(normalized.to_owned()) +} + +fn validate_wit_root(path: &Path) -> Result<(), ModuleError> { + if path.as_os_str().is_empty() || path.is_absolute() { + return Err(ModuleError::InvalidManifest( + "wit.root must be a non-empty relative directory inside the component".to_owned(), + )); + } + if path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::Prefix(_) | Component::RootDir + ) + }) { + return Err(ModuleError::InvalidManifest( + "wit.root must stay inside the component directory".to_owned(), + )); + } + Ok(()) +} + +fn validate_replacement_path(name: &str, path: &Path) -> Result<(), ModuleError> { + if path.as_os_str().is_empty() { + Err(ModuleError::InvalidManifest(format!( + "{name} must be a non-empty directory" + ))) + } else { + Ok(()) + } +} + +fn canonical_file(path: &Path) -> Result { + let canonical = fs::canonicalize(path).map_err(|source| ModuleError::Read { + path: path.to_path_buf(), + source, + })?; + if !canonical.is_file() { + return Err(ModuleError::InvalidManifest(format!( + "{} is not a file", + canonical.display() + ))); + } + Ok(canonical) +} + +fn canonical_directory(path: &Path) -> Result { + let canonical = fs::canonicalize(path).map_err(|source| ModuleError::Read { + path: path.to_path_buf(), + source, + })?; + if !canonical.is_dir() { + return Err(ModuleError::InvalidManifest(format!( + "{} is not a directory", + canonical.display() + ))); + } + Ok(canonical) +} + +fn materialized_directory_name(name: &str, version: &str) -> String { + format!("{}-{version}", name.replace(':', "-")) +} + +fn copy_wit_package(source: &Path, destination: &Path) -> Result { + create_directory(destination)?; + let mut files = fs::read_dir(source) + .map_err(|source_error| ModuleError::Read { + path: source.to_path_buf(), + source: source_error, + })? + .collect::, _>>() + .map_err(|source_error| ModuleError::Read { + path: source.to_path_buf(), + source: source_error, + })?; + files.sort_by_key(|entry| entry.file_name()); + + let mut digest = Sha256::new(); + let mut copied = 0_usize; + for entry in files { + let file_type = entry + .file_type() + .map_err(|source_error| ModuleError::Read { + path: entry.path(), + source: source_error, + })?; + let path = entry.path(); + if !file_type.is_file() || path.extension().and_then(|value| value.to_str()) != Some("wit") + { + continue; + } + let name = entry.file_name(); + let bytes = fs::read(&path).map_err(|source_error| ModuleError::Read { + path: path.clone(), + source: source_error, + })?; + let name_bytes = name.to_string_lossy(); + digest.update((name_bytes.len() as u64).to_le_bytes()); + digest.update(name_bytes.as_bytes()); + digest.update((bytes.len() as u64).to_le_bytes()); + digest.update(&bytes); + fs::write(destination.join(&name), bytes).map_err(|source_error| ModuleError::Write { + path: destination.join(&name), + source: source_error, + })?; + copied += 1; + } + if copied == 0 { + return Err(ModuleError::InvalidWitPackage { + path: source.to_path_buf(), + message: "package directory contains no top-level .wit files".to_owned(), + }); + } + Ok(format!("{:x}", digest.finalize())) +} + +fn install_and_validate_dependencies(wit_root: &Path, staging: &Path) -> Result<(), ModuleError> { + let dependencies = wit_root.join("deps"); + let backup = wit_root.join(format!(".deps.backup-{}", std::process::id())); + remove_directory_if_present(&backup)?; + + let had_dependencies = dependencies.exists(); + if had_dependencies { + fs::rename(&dependencies, &backup).map_err(|source| ModuleError::Write { + path: dependencies.clone(), + source, + })?; + } + if let Err(source) = fs::rename(staging, &dependencies) { + if had_dependencies { + let _ = fs::rename(&backup, &dependencies); + } + return Err(ModuleError::Write { + path: dependencies, + source, + }); + } + + let validation = Resolve::default() + .push_path(wit_root) + .map(|_| ()) + .map_err(|error| ModuleError::InvalidWitPackage { + path: wit_root.to_path_buf(), + message: error.to_string(), + }); + if validation.is_err() { + let _ = fs::remove_dir_all(&dependencies); + if had_dependencies { + let _ = fs::rename(&backup, &dependencies); + } + return validation; + } + if had_dependencies { + remove_directory_if_present(&backup)?; + } + Ok(()) +} + +fn create_directory(path: &Path) -> Result<(), ModuleError> { + fs::create_dir_all(path).map_err(|source| ModuleError::Write { + path: path.to_path_buf(), + source, + }) +} + +fn remove_directory_if_present(path: &Path) -> Result<(), ModuleError> { + match fs::remove_dir_all(path) { + Ok(()) => Ok(()), + Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(()), + Err(source) => Err(ModuleError::Write { + path: path.to_path_buf(), + source, + }), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::*; + + #[test] + fn path_replace_materializes_dependencies_and_writes_lock() { + let fixture = fixture(); + + let report = sync_dependencies(&fixture.manifest, false).unwrap(); + + assert_eq!(report.packages.len(), 1); + assert!( + fixture + .root + .join("wit/deps/wasmeld-clock-1.1.0/package.wit") + .is_file() + ); + let lock = ModuleLock::read(fixture.root.join(MODULE_LOCK_FILE)).unwrap(); + assert_eq!(lock.packages[0].name, "wasmeld:clock"); + assert_eq!(lock.packages[0].version, "1.1.0"); + assert_eq!(lock.packages[0].source, "path+../clock"); + assert!(lock.packages[0].replaced); + } + + #[test] + fn locked_sync_rejects_changed_local_source() { + let fixture = fixture(); + sync_dependencies(&fixture.manifest, false).unwrap(); + fs::write( + fixture.clock.join("package.wit"), + "package wasmeld:clock@1.1.0;\ninterface monotonic-clock { ticks: func() -> u64; }\n", + ) + .unwrap(); + + let error = sync_dependencies(&fixture.manifest, true).unwrap_err(); + + assert!(matches!(error, ModuleError::LockMismatch { .. })); + } + + #[test] + fn rejects_a_replacement_with_the_wrong_package_identity() { + let fixture = fixture(); + fs::write( + fixture.clock.join("package.wit"), + "package wasmeld:other@1.1.0;\ninterface monotonic-clock { now: func() -> u64; }\n", + ) + .unwrap(); + + let error = sync_dependencies(&fixture.manifest, false).unwrap_err(); + + assert!( + error + .to_string() + .contains("expected wasmeld:clock@1.1.0, found wasmeld:other@1.1.0") + ); + } + + #[test] + fn exact_replace_takes_priority_over_package_replace() { + let fixture = fixture(); + let alternate = fixture.root.parent().unwrap().join("alternate-clock"); + fs::create_dir(&alternate).unwrap(); + fs::write( + alternate.join("package.wit"), + "package wasmeld:clock@1.1.0;\ninterface monotonic-clock { ticks: func() -> u64; }\n", + ) + .unwrap(); + let mut manifest = ModuleManifest::read(&fixture.manifest).unwrap(); + manifest + .set_path_replacement("wasmeld:clock@1.1.0", PathBuf::from("../alternate-clock")) + .unwrap(); + manifest.write(&fixture.manifest).unwrap(); + + sync_dependencies(&fixture.manifest, false).unwrap(); + + let materialized = fs::read_to_string( + fixture + .root + .join("wit/deps/wasmeld-clock-1.1.0/package.wit"), + ) + .unwrap(); + assert!(materialized.contains("ticks")); + } + + #[test] + fn set_and_drop_path_replacement_round_trip() { + let fixture = fixture(); + let mut manifest = ModuleManifest::read(&fixture.manifest).unwrap(); + assert!(manifest.drop_replacement("wasmeld:clock")); + manifest + .set_path_replacement("wasmeld:clock@1.1.0", "../clock") + .unwrap(); + manifest.write(&fixture.manifest).unwrap(); + + let parsed = ModuleManifest::read(&fixture.manifest).unwrap(); + assert!(!parsed.replacements.contains_key("wasmeld:clock")); + assert_eq!( + parsed.replacements["wasmeld:clock@1.1.0"].path, + PathBuf::from("../clock") + ); + } + + struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, + clock: PathBuf, + manifest: PathBuf, + } + + fn fixture() -> Fixture { + let temp = tempdir().unwrap(); + let root = temp.path().join("component"); + let clock = temp.path().join("clock"); + fs::create_dir_all(root.join("wit")).unwrap(); + fs::create_dir(&clock).unwrap(); + fs::write( + clock.join("package.wit"), + "package wasmeld:clock@1.1.0;\ninterface monotonic-clock { now: func() -> u64; }\n", + ) + .unwrap(); + fs::write( + root.join("wit/world.wit"), + "package example:component@0.1.0;\nworld component {\n import wasmeld:clock/monotonic-clock@1.1.0;\n}\n", + ) + .unwrap(); + let manifest = root.join(MODULE_MANIFEST_FILE); + fs::write( + &manifest, + r#" +schema_version = 1 + +[dependencies] +"wasmeld:clock" = "1.1.0" + +[replace."wasmeld:clock"] +path = "../clock" +"#, + ) + .unwrap(); + Fixture { + _temp: temp, + root, + clock, + manifest, + } + } +} diff --git a/crates/wasmeld-package/src/wit_package.rs b/crates/wasmeld-package/src/wit_package.rs new file mode 100644 index 0000000..de3adb2 --- /dev/null +++ b/crates/wasmeld-package/src/wit_package.rs @@ -0,0 +1,173 @@ +//! Standard binary WIT package encoding and inspection. +//! +//! Package identity is derived from the encoded WIT itself. Registry clients +//! never submit a separate package name, version, or dependency manifest that +//! could disagree with the artifact. + +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use wit_parser::{ + Resolve, + decoding::{DecodedWasm, decode}, +}; + +/// Metadata schema returned by the Wasmeld WIT Registry API. +pub const WIT_PACKAGE_SCHEMA_VERSION: u32 = 1; + +/// One exact, direct WIT package dependency. +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WitDependency { + pub name: String, + pub version: String, +} + +/// Metadata derived from a binary WIT package. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WitPackageMetadata { + pub schema_version: u32, + pub name: String, + pub version: String, + pub sha256: String, + pub dependencies: Vec, +} + +/// Encoded package bytes together with metadata derived from those bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BuiltWitPackage { + pub metadata: WitPackageMetadata, + pub bytes: Vec, +} + +/// Errors raised while parsing WIT source or decoding a binary WIT package. +#[derive(Debug, Error)] +pub enum WitPackageError { + #[error("failed to parse WIT package at {path}: {message}")] + Source { path: PathBuf, message: String }, + + #[error("failed to encode WIT package: {0}")] + Encode(String), + + #[error("invalid binary WIT package: {0}")] + Decode(String), + + #[error("WIT package {0} must declare an explicit semantic version")] + MissingVersion(String), +} + +/// Parses a WIT source path and encodes its main package as binary WIT. +/// +/// The package must declare an explicit semantic version. Dependencies +/// available through the source path are encoded as Component Model package +/// references and reported in [`WitPackageMetadata::dependencies`]. +pub fn build_wit_package(path: impl AsRef) -> Result { + let path = path.as_ref(); + let mut resolve = Resolve::default(); + let (package, _) = resolve + .push_path(path) + .map_err(|error| WitPackageError::Source { + path: path.to_path_buf(), + message: error.to_string(), + })?; + let bytes = wit_component::encode(&resolve, package) + .map_err(|error| WitPackageError::Encode(error.to_string()))?; + let metadata = metadata_from_resolve(&resolve, package, &bytes)?; + Ok(BuiltWitPackage { metadata, bytes }) +} + +/// Decodes binary WIT and derives its identity, direct dependencies, and digest. +/// +/// Ordinary WebAssembly Components are rejected even though both formats use a +/// Component Model binary container. +pub fn inspect_wit_package(bytes: &[u8]) -> Result { + let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?; + let DecodedWasm::WitPackage(resolve, package) = decoded else { + return Err(WitPackageError::Decode( + "artifact is a WebAssembly Component, not a binary WIT package".to_owned(), + )); + }; + metadata_from_resolve(&resolve, package, bytes) +} + +/// Returns the lowercase SHA-256 digest used as the Registry artifact identity. +pub fn wit_package_sha256(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +fn metadata_from_resolve( + resolve: &Resolve, + package: wit_parser::PackageId, + bytes: &[u8], +) -> Result { + let package_name = &resolve.packages[package].name; + let name = format!("{}:{}", package_name.namespace, package_name.name); + let version = package_name + .version + .as_ref() + .map(ToString::to_string) + .ok_or_else(|| WitPackageError::MissingVersion(name.clone()))?; + let mut dependencies = resolve + .package_direct_deps(package) + .map(|dependency| { + let dependency = &resolve.packages[dependency].name; + let dependency_name = format!("{}:{}", dependency.namespace, dependency.name); + let dependency_version = dependency + .version + .as_ref() + .map(ToString::to_string) + .ok_or_else(|| WitPackageError::MissingVersion(dependency_name.clone()))?; + Ok(WitDependency { + name: dependency_name, + version: dependency_version, + }) + }) + .collect::, WitPackageError>>()?; + dependencies.sort(); + dependencies.dedup(); + + Ok(WitPackageMetadata { + schema_version: WIT_PACKAGE_SCHEMA_VERSION, + name, + version, + sha256: wit_package_sha256(bytes), + dependencies, + }) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::*; + + #[test] + fn builds_and_inspects_a_binary_wit_package() { + let source = tempdir().unwrap(); + fs::write( + source.path().join("package.wit"), + "package wasmeld:clock@1.2.3;\ninterface clock { now: func() -> u64; }\n", + ) + .unwrap(); + + let package = build_wit_package(source.path()).unwrap(); + let inspected = inspect_wit_package(&package.bytes).unwrap(); + + assert_eq!(package.metadata, inspected); + assert_eq!(inspected.name, "wasmeld:clock"); + assert_eq!(inspected.version, "1.2.3"); + assert!(inspected.dependencies.is_empty()); + assert_eq!(inspected.sha256.len(), 64); + } + + #[test] + fn rejects_a_component_as_a_wit_package() { + let error = inspect_wit_package(b"not wasm").unwrap_err(); + assert!(matches!(error, WitPackageError::Decode(_))); + } +}