diff --git a/Cargo.lock b/Cargo.lock index 9087355..b9c2778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -35,6 +44,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -46,9 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ar_archive_writer" @@ -56,7 +71,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" dependencies = [ - "object", + "object 0.37.3", ] [[package]] @@ -137,7 +152,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -252,6 +267,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -276,6 +297,19 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +[[package]] +name = "bigdecimal" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa3f3d8cbf4dffcfe4991de61d012bef509a409ecbe9dd41049bfe32b4d4653" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "binrw" version = "0.15.2" @@ -296,7 +330,7 @@ dependencies = [ "either", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -345,6 +379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] @@ -352,6 +387,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytemuck" @@ -384,6 +422,25 @@ dependencies = [ "web-time", ] +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "camino-tempfile" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64308c4c82a5c38679945ddf88738dc1483dcc563bbb5780755ae9f8497d2b20" +dependencies = [ + "camino", + "tempfile", +] + [[package]] name = "candid" version = "0.10.35" @@ -416,7 +473,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -438,7 +495,37 @@ dependencies = [ "pretty", "serde", "thiserror 1.0.69", - "toml", + "toml 0.8.23", +] + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix 1.1.4", ] [[package]] @@ -497,6 +584,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -526,6 +622,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -563,6 +671,21 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -581,6 +704,152 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249a61efca6a54425e027f5704a6f68a0232b4df527e05a3ef947d89dbdcb078" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5336a46e05db2d515440b1ce8a9363e7bc04b608c0f66cc98dddc5492a66fbf" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b3675b13214270fddbd257d5fbf4109be8b02d92fc636fe96e2829514a7235" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c1d67aa02b6e174bd2b165c113e94af9498fcc20e2892d0f4d5ab8c9618d09" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fe6f3ec5344c32602c414e986dcbe699fa501a3e3e7b02f887d9d12b14ae42" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c5e6323d458486d95781defdb5016a1176fc911d8bc8e64b696adbda6e9044" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1260aa93b0bd48e8e55c1c7fd9ff6a260f1a1bc46e1f0151a5055b124e5b15d" + +[[package]] +name = "cranelift-control" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c179632a09850f81f1a1e9116cb09330c734a86689f3ee4c3f4ec81543acbd" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c071188cb1c3b1803e23ae415a8d5f58d741d3a4b26a79b83dd61921112eac" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b85ba0a55d7685117b275fe6f36c819d438862e4223d28f0b97902460ff0ac" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ed364edcaed769da7b2709d540f8f7d0ff257749ba193f475e22dba6a0178b" + +[[package]] +name = "cranelift-native" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d98c3dce08f0d13526692bbd1888bff4c5f45207f5093d168b5a224fbc88c" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f016b9994e39cad68107f33fa5b8dcee8fb7e38332bde3f97690e5e8abe1b10" + [[package]] name = "crc32fast" version = "1.5.0" @@ -599,6 +868,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -668,7 +956,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -715,7 +1003,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -728,7 +1016,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -739,7 +1027,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -750,7 +1038,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -759,6 +1047,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "der" version = "0.7.10" @@ -794,7 +1091,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -804,7 +1101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.117", ] [[package]] @@ -837,6 +1134,17 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", + "zeroize", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", ] [[package]] @@ -889,7 +1197,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -986,6 +1294,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.4" @@ -995,6 +1315,26 @@ dependencies = [ "log", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1069,6 +1409,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1089,6 +1439,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1103,6 +1454,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1112,6 +1469,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix 1.1.4", + "windows-sys 0.52.0", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1174,7 +1542,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1206,6 +1574,20 @@ dependencies = [ "slab", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -1258,6 +1640,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "group" version = "0.13.0" @@ -1318,14 +1718,25 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] [[package]] name = "heck" @@ -1557,12 +1968,55 @@ dependencies = [ ] [[package]] -name = "ic-certification" -version = "3.2.0" +name = "ic-cdk" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754e3037360152de7e5f2bd0ccf51db8864b6a03c5a2b4ffc6a65286c0e2ffa7" +checksum = "818d6d5416a8f0212e1b132703b0da51e36c55f2b96677e96f2bbe7702e1bd85" dependencies = [ - "hex", + "candid", + "ic-cdk-executor", + "ic-cdk-macros", + "ic-error-types", + "ic-management-canister-types 0.5.0", + "ic0", + "pin-project-lite", + "serde", + "serde_bytes", + "slotmap", + "thiserror 2.0.18", +] + +[[package]] +name = "ic-cdk-executor" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33716b730ded33690b8a704bff3533fda87d229e58046823647d28816e9bcee7" +dependencies = [ + "ic0", + "slotmap", + "smallvec", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66dad91a214945cb3605bc9ef6901b87e2ac41e3624284c2cabba49d43aa4f43" +dependencies = [ + "candid", + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ic-certification" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754e3037360152de7e5f2bd0ccf51db8864b6a03c5a2b4ffc6a65286c0e2ffa7" +dependencies = [ + "hex", "serde", "serde_bytes", "sha2 0.10.9", @@ -1585,6 +2039,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ic-error-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" +dependencies = [ + "serde", + "strum 0.26.3", + "strum_macros 0.26.4", +] + +[[package]] +name = "ic-ledger-types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ab0da348a638e01beb5bcc6c0b92e51efbe351950ff99c125d53fff77899d5" +dependencies = [ + "candid", + "crc32fast", + "hex", + "ic-cdk", + "serde", + "serde_bytes", + "sha2 0.10.9", +] + [[package]] name = "ic-management-canister-types" version = "0.5.0" @@ -1596,6 +2076,26 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "ic-management-canister-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdeb533e0ed67b4fd6b7f2c396e6b84cfe0f875e38911bed49a1f527c65fb3" +dependencies = [ + "candid", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic-stable-structures" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d30d4cf17aff1024e13133897048bcba580e063c9000571ab766ca37e2996f4" +dependencies = [ + "ic_principal", +] + [[package]] name = "ic-transport-types" version = "0.45.0" @@ -1646,6 +2146,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "ic0" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05349d5c51ab6cc962c8963f877f826997ee6b737c6be3aafc9779eb8a4dd05" + [[package]] name = "ic_bls12_381" version = "0.10.1" @@ -1675,6 +2181,129 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "icp-canister-interfaces" +version = "1.8.0" +source = "git+https://github.com/dfinity/icp-cli?rev=8f98455d021f1f7dacf91f3244844f324a1c503b#8f98455d021f1f7dacf91f3244844f324a1c503b" +dependencies = [ + "bigdecimal", + "candid", + "ic-management-canister-types 0.9.0", + "serde", +] + +[[package]] +name = "icp-events" +version = "1.8.0" +source = "git+https://github.com/dfinity/icp-cli?rev=8f98455d021f1f7dacf91f3244844f324a1c503b#8f98455d021f1f7dacf91f3244844f324a1c503b" +dependencies = [ + "serde", + "tokio", +] + +[[package]] +name = "icp-project" +version = "1.8.0" +source = "git+https://github.com/dfinity/icp-cli?rev=8f98455d021f1f7dacf91f3244844f324a1c503b#8f98455d021f1f7dacf91f3244844f324a1c503b" +dependencies = [ + "async-trait", + "bigdecimal", + "camino", + "camino-tempfile", + "candid", + "candid_parser", + "cap-std", + "dunce", + "flate2", + "futures", + "glob", + "handlebars", + "hex", + "ic-ledger-types", + "ic-management-canister-types 0.9.0", + "icp-canister-interfaces", + "icp-events", + "icrc-ledger-types", + "indexmap", + "indoc", + "itertools 0.14.0", + "num-bigint", + "num-integer", + "num-traits", + "pathdiff", + "rand 0.10.1", + "schemars 1.2.1", + "semver", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shellwords", + "snafu", + "strum 0.28.0", + "tar", + "time", + "tokio", + "tracing", + "url", + "wasmparser 0.255.0", + "winreg", +] + +[[package]] +name = "icp-sync-plugin" +version = "1.8.0" +source = "git+https://github.com/dfinity/icp-cli?rev=8f98455d021f1f7dacf91f3244844f324a1c503b#8f98455d021f1f7dacf91f3244844f324a1c503b" +dependencies = [ + "async-trait", + "bytes", + "camino", + "candid", + "console", + "icp-events", + "icp-project", + "semver", + "snafu", + "tokio", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "icrc-cbor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90569d2894d9536c5416943556ac6339df249f06611b3c41029196b39e0dd119" +dependencies = [ + "candid", + "minicbor", + "num-bigint", + "num-traits", +] + +[[package]] +name = "icrc-ledger-types" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263acc94fa47008ad00f76b8d3694efe7f8361870c9ec92cddcc588b571518b7" +dependencies = [ + "base32", + "candid", + "crc32fast", + "hex", + "ic-stable-structures", + "icrc-cbor", + "minicbor", + "num-bigint", + "num-traits", + "serde", + "serde_bytes", + "sha2 0.10.9", + "strum 0.26.3", + "strum_macros 0.26.4", + "time", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1847,6 +2476,34 @@ dependencies = [ "wat", ] +[[package]] +name = "imcp2-deploy" +version = "0.6.1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "camino", + "candid", + "flate2", + "hex", + "ic-agent", + "icp-canister-interfaces", + "icp-events", + "icp-project", + "icp-sync-plugin", + "imcp2-core", + "rmcp", + "serde", + "serde_json", + "sha2 0.11.0", + "tar", + "tempfile", + "tokio", + "tracing", + "url", + "wat", +] + [[package]] name = "imcp2-local" version = "0.6.1" @@ -1884,6 +2541,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +dependencies = [ + "rustversion", +] + [[package]] name = "instant" version = "0.1.13" @@ -1893,6 +2559,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + [[package]] name = "ipnet" version = "2.12.0" @@ -1927,12 +2615,41 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "jni" version = "0.22.4" @@ -1960,7 +2677,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.117", ] [[package]] @@ -1979,7 +2696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2035,7 +2752,7 @@ dependencies = [ "ascii-canvas", "bit-set", "ena", - "itertools", + "itertools 0.11.0", "lalrpop-util", "petgraph", "pico-args", @@ -2081,6 +2798,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -2144,7 +2867,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn", + "syn 2.0.117", ] [[package]] @@ -2162,6 +2885,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "matchers" version = "0.2.0" @@ -2177,12 +2906,27 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "merlin" version = "3.0.0" @@ -2211,6 +2955,26 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicbor" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2232,6 +2996,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2295,6 +3065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2306,6 +3077,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2402,6 +3185,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +dependencies = [ + "camino", +] + [[package]] name = "pem" version = "3.0.6" @@ -2457,7 +3249,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2511,6 +3303,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "pocket-ic" version = "15.0.0" @@ -2523,7 +3321,7 @@ dependencies = [ "flate2", "hex", "ic-certification", - "ic-management-canister-types", + "ic-management-canister-types 0.5.0", "ic-transport-types 0.45.0", "reqwest 0.12.28", "schemars 0.8.22", @@ -2533,8 +3331,8 @@ dependencies = [ "serde_json", "sha2 0.10.9", "slog", - "strum", - "strum_macros", + "strum 0.26.3", + "strum_macros 0.26.4", "tempfile", "thiserror 2.0.18", "tokio", @@ -2545,8 +3343,20 @@ dependencies = [ ] [[package]] -name = "potential_utf" -version = "0.1.5" +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ @@ -2592,7 +3402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -2682,6 +3492,29 @@ dependencies = [ "cc", ] +[[package]] +name = "pulley-interpreter" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d74df9e20cba0f6dd07f01c3140535a6b23e40aecccfcacfb921621b41d0acb" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8415513bfa407c06de6f11dbe9437f2352e075a5520ff491a131166d3d90aed1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2822,6 +3655,26 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2870,7 +3723,22 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", ] [[package]] @@ -3053,9 +3921,15 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.117", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3097,6 +3971,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.40" @@ -3223,10 +4107,12 @@ checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "chrono", "dyn-clone", + "indexmap", "ref-cast", "schemars_derive 1.2.1", "serde", "serde_json", + "url", ] [[package]] @@ -3238,7 +4124,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -3250,7 +4136,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -3353,7 +4239,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3364,7 +4250,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3399,7 +4285,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3411,6 +4297,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3423,6 +4318,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.9.9" @@ -3467,6 +4375,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellwords" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e515aa4699a88148ed5ef96413ceef0048ce95b43fbc955a33bde0a70fcae6" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "shlex" version = "2.0.1" @@ -3539,11 +4457,44 @@ dependencies = [ "serde_core", ] +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "snafu" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45cb604038abb7b926b679887b3226d8d0f23874b66623625a0454be425a4b7" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287f59010008f0d7cf5e3b03196d666c1acc46c8d3e9cf34c28a1a7157601e72" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "socket2" @@ -3633,7 +4584,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -3646,7 +4606,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3667,6 +4639,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -3695,9 +4678,26 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -3757,7 +4757,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3768,7 +4768,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3869,7 +4869,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3913,11 +4913,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3936,6 +4951,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3944,7 +4968,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "winnow 0.7.15", ] @@ -3964,18 +4988,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -4071,7 +5095,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4187,6 +5211,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -4203,6 +5233,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4325,7 +5356,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -4338,6 +5369,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-compose" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27dc71f96d21a8b9153da9005c603f4f20d5781b7e6d2675ea0ca5def8713b75" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wat", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -4350,12 +5398,22 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.258.0" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" dependencies = [ "leb128fmt", - "wasmparser 0.258.0", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser 0.262.0", ] [[package]] @@ -4370,6 +5428,18 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-metadata" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -4410,35 +5480,356 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.258.0" +version = "0.255.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" dependencies = [ "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasmtime" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45bf3b13781b9febb0eef42e1e145146c92ef891f345fd918e0e0fa1568c992" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.40.0", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833f00f3dc6128159284c5b71e211f24c97d45e052230a14f767fd830a6fb8b0" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", "indexmap", + "log", + "object 0.40.0", + "postcard", + "rustc-demangle", "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f1bafc5f2237c5fb6f8babc77b5db2eee8128766b8f1cfe52e771ebbcbed89" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml 1.1.8+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d69c34412409072d2e8ec399101524c330e334ff313f77415e6984cc9c6a172" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b168d39f2d6b896d89fe096afa4ccdd7a8e927b01469b590af84f02fca3126a1" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad86a65d12c6a9202584ceb658c9a20cf5b25c32eed61c9be5e5f863f88f0d9" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71f0fd1dd40e4e18d4a20cdba29ff8e0eb2c0f21b12c2eb5869e0fea789dd05" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object 0.40.0", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4dfa39e58735b24ce3d28bfc8c2b3041603cc8a941bb268395351beff90172" +dependencies = [ + "cc", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2eb98d4486c7b0163447d77dea06f3cc87e5e5ba5fc93613f8ec42e27ea181d" +dependencies = [ + "cc", + "object 0.40.0", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852cae234735d79db6c58fe0d04b32f4078ad01fdb3fd6d488ac366132ba9785" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9289378c82b9db7e7cf933d6bb441ad968536ac48f3f38c55b7cf9d3e8d87e1" +dependencies = [ + "cranelift-codegen", + "log", + "object 0.40.0", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5c60c732d99dbcc5d670b71ba6ccf2b015cd16fc828990eaad2c4b8805ebdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77e4fcc7cd22772578458c5e233df47190ebc9b8b65788797d99a02787c9000" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component 0.258.3", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-wasi" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705b90d1fa1084cc73df656868948da77711b97f6b895d145a4949f86bcea674" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand 0.10.1", + "rustix 1.1.4", + "rustix-linux-procfs", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d2b6a255be488639f450b380e7e0923789f946de3463696cd30f2fe8a65cc3" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", ] [[package]] name = "wast" -version = "258.0.0" +version = "262.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" +checksum = "30612a0c29249b5f185888a107e8a519d920226b7d07f9d351fd094e9f75eb23" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.258.0", + "wasm-encoder 0.262.0", ] [[package]] name = "wat" -version = "1.258.0" +version = "1.262.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555c008cca87f2ac58d9f83ccda7e7b44611093ce28eb28f052e7c78024b9bf" +checksum = "e228027f2b07bbaf946980ebbe2437fca73be6f54c40c2a8d4f42f277075f8d1" dependencies = [ - "wast", + "wast 262.0.0", ] [[package]] @@ -4479,6 +5870,46 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wiggle" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ef4f7720340c50b9d28718516f7ab881a169b9021268e7dc67729435d9ccfd" +dependencies = [ + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1692e27a88a6fa5bacd69a645e982a439c5ae835d9180394992627b0124cb705" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "49.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562354c56fa0b83911512f0b60baf33c824e431de67a6012939ce52d224713b9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4531,7 +5962,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4542,7 +5973,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4740,6 +6171,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.52.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4763,7 +6214,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -4776,10 +6227,10 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", - "wasm-metadata", + "syn 2.0.117", + "wasm-metadata 0.244.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.244.0", ] [[package]] @@ -4792,7 +6243,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4811,9 +6262,28 @@ dependencies = [ "serde_derive", "serde_json", "wasm-encoder 0.244.0", - "wasm-metadata", + "wasm-metadata 0.244.0", "wasmparser 0.244.0", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.258.3", + "wasm-metadata 0.258.3", + "wasmparser 0.258.3", + "wit-parser 0.258.3", ] [[package]] @@ -4834,6 +6304,37 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wit-parser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.258.3", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + [[package]] name = "writeable" version = "0.6.3" @@ -4846,6 +6347,16 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04a2ecdf2cc4d33a6a93d71bcfbc00bb1f635cdb8029a2cc0709204a045ec7a3" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "yoke" version = "0.8.3" @@ -4865,7 +6376,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4886,7 +6397,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4906,7 +6417,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4927,7 +6438,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4960,11 +6471,45 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.2+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7d0dab9..86e2670 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ exclude = [ ] [workspace] -members = [".", "crates/imcp2-core", "crates/imcp2-local"] +members = [".", "crates/imcp2-core", "crates/imcp2-deploy", "crates/imcp2-local"] # The hosted server releases to crates.io (publish-crate.yml), never as dist # binaries — dist releases only imcp2-local (see dist-workspace.toml). diff --git a/crates/imcp2-deploy/Cargo.toml b/crates/imcp2-deploy/Cargo.toml new file mode 100644 index 0000000..c260a6d --- /dev/null +++ b/crates/imcp2-deploy/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "imcp2-deploy" +version = "0.6.1" +edition = "2021" +description = "The imcp2 `icp_deploy_bundle` tool: deploys an icp project bundle through the icp CLI's own project library." +license = "Apache-2.0" +repository = "https://github.com/dfinity/imcp2" +# NOT publishable, by construction: the deploy runs on `icp-project` (and its +# siblings) pulled straight from the icp-cli git repository — those crates are +# `publish = false` upstream, and crates.io refuses any crate with a git +# dependency. That is why this is its own crate rather than a module of +# `imcp2-core`, and why only `imcp2-local` (released as dist binaries, never +# published) composes it: `imcp2-core` and `imcp2` keep publishing untouched. +publish = false + +[dependencies] +# The session engine (which Internet Identity session a call acts as, and +# the management identity minted from it) and the `SessionResolver` seam. +imcp2-core = { workspace = true } +rmcp = { workspace = true, features = ["server", "macros"] } +ic-agent = { workspace = true } +candid = { workspace = true } +# `rt-multi-thread`: the sync-plugin runtime runs a wasm component on a +# blocking thread (`block_in_place`), which only a multi-thread runtime has. +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "fs"] } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +base64 = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +url = { workspace = true } +async-trait = "0.1" +# `icp-project` spells every path as a UTF-8 path. +camino = "1" +# A bundle is the `.tar.gz` that `icp project bundle` writes. +tar = "0.4" +flate2 = "1" +# Each deploy extracts its bundle into a scratch directory that is removed +# when the deploy ends, whichever way it ends. +tempfile = "3" + +# The icp CLI's project library and the seams it is driven through, pinned to +# one commit so the four stay in lockstep (they share a workspace version and +# are not released independently). Bump the `rev` on all four together. +# +# The pinned commit is icp-cli's main at the merge of dfinity/icp-cli#873 +# (on #868): #868 has `icp project bundle` write an archive of regular files +# and directories alone, and #873 states the rule for an untrusted project +# (`icp_project::untrusted`, what `--untrusted` on `icp build`, `deploy` and +# `sync` applies): the project is read through a filesystem confined to its +# root by a directory handle and through no symlink, names no recipe, and +# what it runs is held to a list of regular files there. The host composes +# the rule as the CLI does. +# +# * icp-project: manifests, the deploy operation, the id/artifact stores. +# * icp-events: the progress event stream the operations report on. +# * icp-sync-plugin: the wasmtime runtime that runs a canister's sync plugin +# (the asset uploads of a frontend canister). The heavy one — it is what +# brings wasmtime into this workspace. +# * icp-canister-interfaces: the engine-canister registry id a created +# canister's subnet is looked up in. +icp-project = { git = "https://github.com/dfinity/icp-cli", rev = "8f98455d021f1f7dacf91f3244844f324a1c503b" } +icp-events = { git = "https://github.com/dfinity/icp-cli", rev = "8f98455d021f1f7dacf91f3244844f324a1c503b" } +icp-sync-plugin = { git = "https://github.com/dfinity/icp-cli", rev = "8f98455d021f1f7dacf91f3244844f324a1c503b" } +icp-canister-interfaces = { git = "https://github.com/dfinity/icp-cli", rev = "8f98455d021f1f7dacf91f3244844f324a1c503b" } + +[dev-dependencies] +# The tests hand-build small bundles (a manifest plus a wasm) and load them +# through the same host the tool uses. +wat = "1" diff --git a/crates/imcp2-deploy/src/host.rs b/crates/imcp2-deploy/src/host.rs new file mode 100644 index 0000000..f75f2cd --- /dev/null +++ b/crates/imcp2-deploy/src/host.rs @@ -0,0 +1,1630 @@ +//! The [`Host`] a bundle is deployed from: the project library's seams, each +//! implemented for an extracted bundle in a scratch directory. +//! +//! A bundle is the self-contained archive `icp project bundle` writes: the +//! project manifest rewritten so every canister is a pre-built wasm the +//! archive carries, the networks and environments inlined, and each sync +//! plugin's wasm and input files alongside. That shape is what makes the seams +//! small: nothing is built from source, no wasm is fetched over HTTP, no +//! recipe is resolved, and the only network a bundle targets from here is the +//! Internet Computer itself. +//! +//! The archive is untrusted input: it arrives in a tool call, and the agent +//! that passes it may have fetched it from anywhere. So the shape is not +//! assumed but enforced, and the rule it is held to is the library's own: +//! `icp_project::untrusted`, the rule for a project someone else handed over +//! (dfinity/icp-cli#873), the one `--untrusted` on `icp build`, `deploy` and +//! `sync` applies. The host composes it as the CLI does, from the library's +//! three parts: +//! +//! * **The project is read through [`Confined`]**, the host filesystem held +//! to the extraction directory: the root is opened once as a directory +//! handle and every read is made beneath it, so the operating system +//! refuses a path that leaves the root, by `..`, by an absolute path or by +//! a symlink; and on top of that a path through a symlink is refused +//! wherever it leads. A manifest can name only what the archive brought +//! with it. The disk is the byte store because the sync-plugin runtime +//! needs real paths to mount. +//! * **No recipe is fetched** ([`NoRecipes`]): a recipe is build +//! instructions read from a file or fetched from a URL while the project +//! loads, so a project naming one fails to load, naming the recipe. A +//! bundle spells every step out and names none. +//! * **The loaded project is vetted** ([`Vetted`]) before it is handed out: +//! each build step is a pre-built module, each sync step a plugin, and +//! what each names is a regular file the confined filesystem will give; +//! what a plugin step hands its plugin, each `dirs:` and `files:` entry, +//! is a directory or a regular file there too. A project with any other +//! step — a script, a download — or a path that is not such a file fails +//! the load, naming it. The loader is the one gate on the project's +//! shape; the builder and syncer are the library's own, and see no other +//! step. +//! +//! What a consumer of a bundle adds, this crate adds: +//! +//! * **Extraction** writes only regular files and directories, within a total +//! size budget and an object budget, under the scratch directory +//! ([`extract_bundle`]), and refuses the `.icp` state a deploy seeds for +//! itself: the library takes what is under `.icp` as its own earlier +//! writes, and vets none of it. +//! * **The bundle is checked before any phase runs** ([`check_bundle`]): every +//! canister built by one pre-built wasm in the archive that hashes to its +//! recorded checksum, every sync plugin in the archive with the bytes it +//! declares held to a sum, and no network but `ic`. The check's own work +//! is bounded by the archive, not by what the manifest repeats. +//! * **What the load retains is metered**, so a manifest naming large +//! argument files over and over costs no more than [`MAX_PROJECT_BYTES`]. +//! +//! The rule is one, split three ways upstream (dfinity/icp-cli#863): the +//! user's own project may run, fetch and link whatever it likes; `icp +//! project bundle` writes an archive of regular files and directories with +//! every step spelled out (dfinity/icp-cli#868); and a consumer of a bundle +//! it does not trust loads it as untrusted (dfinity/icp-cli#873, on #868). +//! This crate is that consumer, and pins icp-cli's main at the merge of #873. +//! +//! Everything the project library reads or writes — the manifest, the wasms, +//! the `.icp` id and artifact stores, the build scratch — lives under the +//! extracted directory, which is removed when the deploy ends. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, + io::Read, + sync::{Arc, Mutex, PoisonError}, +}; + +use async_trait::async_trait; +use camino::{Utf8Path, Utf8PathBuf}; +use icp_events::StepReporter; +use icp_project::{ + canister::{ + build::Builder, + sync::{ + script::{ScriptInvocation, ScriptRunError, ScriptRunner}, + Syncer, + }, + wasm::{Fetch, FetchError}, + }, + files::{Confined, FileSystem, FsError, Scratch}, + host::{Host, Ignore}, + manifest::{ + plugin::NamedPaths, prebuilt::SourceField, BuildStep, ProjectRootLocate, + ProjectRootLocateImpl, SyncStep, PROJECT_MANIFEST, + }, + network::{ + Access, AccessError, CollectFriendlyDomains, Configuration, NetworkAccess, NetworkUrls, + RootKeySource, RootKeySpec, + }, + prelude::{IC, IC_MAINNET_NETWORK_API_URL, IC_ROOT_KEY}, + random::HostRandom, + store_artifact::ArtifactStore, + store_id::AccessImpl, + untrusted::{NoRecipes, Vetted}, + Canister, Lazy, Network, Project, ProjectLoadImpl, +}; +use sha2::{Digest, Sha256}; + +/// Archives above this are refused before anything is written: a bundle rides +/// in a tool call (or a file the call names), and a runaway input must not +/// fill the disk or the heap. +pub const MAX_BUNDLE_BYTES: usize = 256 * 1024 * 1024; + +/// What an archive may extract to, in total. Gzip expands by up to about a +/// thousand to one, so the archive limit alone bounds nothing on disk; this +/// one does, on the sum of the entries' sizes and on the decompressed stream +/// itself. +pub const MAX_EXTRACTED_BYTES: u64 = 1024 * 1024 * 1024; + +/// The directory under the project root where icp-project keeps its own +/// state (`.icp/data/mappings/.ids.json`, build artifacts). The deploy +/// seeds it from the caller's mapping; the archive may not supply it. +const STATE_DIR: &str = ".icp"; + +/// The most files and directories an archive may extract to, counting the +/// directories its entries' paths imply and extraction creates on the way. +/// The byte budget alone would admit millions of empty files, each a path +/// the loader may walk, or a few files at the bottom of millions of +/// directories, each an inode; a real bundle is a manifest, a few modules and +/// plugins, and a frontend's assets, well inside this. +pub const MAX_BUNDLE_ENTRIES: usize = 250_000; + +/// The longest path an archive entry may have, in bytes: Linux's own `PATH_MAX`, +/// past which no path can be created anyway. Counting the directories a path +/// implies costs a lookup per component, so the path is held to a length first. +pub const MAX_ENTRY_PATH_BYTES: usize = 4096; + +/// What loading the project may read in all: the manifests, and the argument +/// and variable files the loader reads into each canister. Each read is +/// retained, and the loader reads a file once per canister that names it, +/// so the extraction budget bounds none of this: fifty canisters naming one +/// file of 100 MiB would hold 5 GiB. A canister's arguments fit in one +/// ingress message, so a real project reads a few MiB at most. +pub const MAX_PROJECT_BYTES: u64 = 256 * 1024 * 1024; + +/// The most a gzip-compressed module may expand to: 100 MiB, the IC's own +/// ceiling on a module. The library gunzips a module in memory to read its +/// Candid interface, with no bound of its own, so the bound is applied here +/// before a deploy runs. +pub const MAX_WASM_BYTES: u64 = 100 * 1024 * 1024; + +/// What the gzip-compressed modules of one bundle may expand to in all, each +/// distinct module counted once: the extraction budget again, so that +/// compressing a module buys nothing against it. [`check_bundle`] expands +/// each module once to hold it to [`MAX_WASM_BYTES`]; without a sum, an +/// archive of ten thousand kilobyte-sized gzip files each expanding to that +/// limit would have the check decompress a terabyte before refusing anything. +pub const MAX_WASM_BYTES_IN_ALL: u64 = MAX_EXTRACTED_BYTES; + +/// What one sync step's `files:` entries may hold in all, an entry counted +/// each time it is declared: the sync-plugin runtime reads every declared +/// file into a string of its own before the plugin runs, so a file declared +/// a hundred times is held a hundred times over. A plugin's file inputs are +/// its configuration, a few KiB; the directories it declares are mounted, +/// not read, and cost nothing here. +pub const MAX_PLUGIN_INPUT_BYTES: u64 = 64 * 1024 * 1024; + +/// A plain error for the seams below: each carries one sentence, and the +/// library wraps it whole at its own boundary (`AccessError::new` and the +/// like), so the sentence is what the user reads. +#[derive(Debug)] +pub struct Unsupported(pub String); + +impl fmt::Display for Unsupported { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for Unsupported {} + +// ── The network ─────────────────────────────────────────────────────────── + +/// Network resolution for a bundle: the Internet Computer, and nothing else. +/// +/// A bundle's manifest inlines its networks, and a manifest may declare any: +/// a managed one the icp CLI would launch, a connected one with a root key to +/// fetch or a key spelled out — or one that claims the mainnet root key over +/// an endpoint of its choosing, which would send the user's signed calls +/// wherever it points. This server deploys to mainnet only, so the one +/// network honoured is the reserved `ic`: the project loader supplies it with +/// the mainnet endpoint and the pinned root key, and refuses a manifest that +/// redeclares the name. [`check_bundle`] refuses the rest before any phase +/// runs; this is the seam's own refusal, for the library's own calls. +pub struct IcMainnet; + +impl IcMainnet { + /// The connected configuration of `network`, if it is the reserved `ic` + /// network as the loader supplies it: that name, the mainnet endpoint and + /// the mainnet root key. + pub fn mainnet(network: &Network) -> Result<&icp_project::network::Connected, Unsupported> { + let refuse = || { + Unsupported(format!( + "network '{}' is not the Internet Computer; this server deploys to the `ic` \ + network only. Deploy to the `ic` environment, or deploy elsewhere with `icp \ + deploy`", + network.name + )) + }; + if network.name != IC { + return Err(refuse()); + } + match &network.configuration { + Configuration::Connected { connected } + if matches!(connected.root_key, RootKeySpec::Mainnet) + && connected.api_url.as_str().trim_end_matches('/') + == IC_MAINNET_NETWORK_API_URL => + { + Ok(connected) + } + _ => Err(refuse()), + } + } +} + +#[async_trait] +impl Access for IcMainnet { + async fn access(&self, network: &Network) -> Result { + let connected = Self::mainnet(network).map_err(AccessError::new)?; + Ok(NetworkAccess { + root_key: IC_ROOT_KEY.to_vec(), + root_key_source: RootKeySource::Mainnet, + api_url: connected.api_url.clone(), + http_gateway_url: connected.http_gateway_url.clone(), + use_friendly_domains: false, + }) + } + + async fn urls(&self, network: &Network) -> Result { + let connected = Self::mainnet(network).map_err(AccessError::new)?; + Ok(NetworkUrls { + api_url: connected.api_url.clone(), + http_gateway_url: connected.http_gateway_url.clone(), + }) + } + + /// Friendly domains are a managed-network convenience; there is no + /// managed network here to publish them to. + async fn publish_friendly_domains( + &self, + _network: &Network, + _collect: &CollectFriendlyDomains<'_>, + ) { + } +} + +// ── The filesystem ──────────────────────────────────────────────────────── + +/// The extracted bundle, as the filesystem the project library reads +/// through: the library's own [`Confined`], rooted at the scratch directory. +/// +/// Every path the library asks about is opened beneath that root's directory +/// handle, and refused if it passes through a symlink, so a manifest's +/// `path:` — absolute, climbing, or plain — can name only what the archive +/// brought with it; what is not there is "not in the bundle", whatever the +/// machine holds at that spelling or behind a link. A +/// scratch directory the library asks for (a pre-built build step copies its +/// module there and the operation reads it back) is made under the root too. +pub type BundleFs = Confined; + +/// Where a file in the bundle is on disk, for a runtime that needs a real +/// path, or why there is none: the path leaves the root, or nothing is there. +pub fn locate(fs: &BundleFs, path: &Utf8Path) -> Result { + let on_disk = fs.resolve(path).map_err(|e| Unsupported(e.to_string()))?; + if on_disk.is_file() { + Ok(on_disk) + } else { + Err(Unsupported(format!( + "'{}' is not in the bundle; a bundle carries every file its manifest names (`icp \ + project bundle` writes one that does)", + on_disk.strip_prefix(fs.root()).unwrap_or(&on_disk) + ))) + } +} + +/// The bytes of a file in the bundle, read where [`locate`] says. +fn read_bytes(fs: &BundleFs, path: &Utf8Path) -> Result, Unsupported> { + let on_disk = locate(fs, path)?; + std::fs::read(&on_disk).map_err(|e| Unsupported(format!("'{on_disk}': {e}"))) +} + +/// [`BundleFs`] with a budget on what is read through it: the filesystem +/// the project loader is given, so what it retains is bounded. +/// +/// The loader reads a manifest, an argument file or a variable file into the +/// canister that names it, once per canister, and keeps it for the life of +/// the project. Every read is charged by the file's size before it is made; +/// the rest of the trait passes through. +struct Metered { + inner: BundleFs, + left: Mutex, +} + +impl Metered { + fn new(inner: BundleFs, budget: u64) -> Self { + Self { inner, left: Mutex::new(budget) } + } + + /// Charge `path`'s size to the budget, or refuse the read. + fn charge(&self, path: &Utf8Path) -> Result<(), FsError> { + let on_disk = locate(&self.inner, path).map_err(FsError::new)?; + let size = std::fs::metadata(&on_disk) + .map_err(|e| FsError::new(Unsupported(format!("'{on_disk}': {e}"))))? + .len(); + let mut left = self.left.lock().unwrap_or_else(PoisonError::into_inner); + if size > *left { + return Err(FsError::new(Unsupported(format!( + "loading the project reads more than {} bytes of manifests, argument files and \ + variable files in all, more than a project ever holds; a canister's arguments \ + fit in one message", + MAX_PROJECT_BYTES + )))); + } + *left -= size; + Ok(()) + } +} + +#[async_trait] +impl FileSystem for Metered { + async fn read(&self, path: &Utf8Path) -> Result, FsError> { + self.charge(path)?; + self.inner.read(path).await + } + + async fn read_to_string(&self, path: &Utf8Path) -> Result { + self.charge(path)?; + self.inner.read_to_string(path).await + } + + async fn write(&self, path: &Utf8Path, contents: &[u8]) -> Result<(), FsError> { + self.inner.write(path, contents).await + } + + async fn create_dir_all(&self, path: &Utf8Path) -> Result<(), FsError> { + self.inner.create_dir_all(path).await + } + + async fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), FsError> { + self.inner.copy(from, to).await + } + + async fn exists(&self, path: &Utf8Path) -> bool { + self.inner.exists(path).await + } + + async fn is_file(&self, path: &Utf8Path) -> bool { + self.inner.is_file(path).await + } + + async fn is_dir(&self, path: &Utf8Path) -> bool { + self.inner.is_dir(path).await + } + + async fn read_dir(&self, path: &Utf8Path) -> Result, FsError> { + self.inner.read_dir(path).await + } + + async fn canonicalize(&self, path: &Utf8Path) -> Option { + self.inner.canonicalize(path).await + } + + async fn scratch_dir(&self) -> Result, FsError> { + self.inner.scratch_dir().await + } +} + +// ── Wasms, recipes, scripts ─────────────────────────────────────────────── + +/// Wasm resolution for a bundle: the archive carries every module it names, +/// so a source is a path inside the bundle (verified against its recorded +/// checksum) and a remote one is a bundle this tool does not understand. +pub struct BundledWasm(pub BundleFs); + +#[async_trait] +impl Fetch for BundledWasm { + async fn wasm( + &self, + source: &SourceField, + base_dir: &Utf8Path, + sha256: Option<&str>, + reporter: &StepReporter, + ) -> Result { + match source { + SourceField::Local(local) => { + let path = base_dir.join(&local.path); + let on_disk = locate(&self.0, &path).map_err(FetchError::new)?; + if let Some(expected) = sha256 { + reporter.info(format!("Reading wasm: {}", local.path)); + let bytes = read_bytes(&self.0, &path).map_err(FetchError::new)?; + reporter.info("Verifying checksum"); + verify_checksum(&local.path, expected, &bytes).map_err(FetchError::new)?; + } + Ok(on_disk) + } + SourceField::Remote(_) => Err(FetchError::new(Unsupported( + "a bundle carries every wasm it names, so a remote wasm source is not \ + expected here; rebuild the bundle with `icp project bundle`" + .into(), + ))), + } + } +} + +fn verify_checksum(path: &Utf8Path, expected: &str, bytes: &[u8]) -> Result<(), Unsupported> { + checksum_matches(path, expected, &hex::encode(Sha256::digest(bytes))) +} + +fn checksum_matches(path: &Utf8Path, expected: &str, actual: &str) -> Result<(), Unsupported> { + if actual.eq_ignore_ascii_case(expected) { + Ok(()) + } else { + Err(Unsupported(format!( + "checksum mismatch for '{path}': the manifest records {expected}, the file hashes \ + to {actual}" + ))) + } +} + +/// Hold a gzip-compressed module to `limit` bytes expanded, by streaming it +/// through a bounded decoder into nothing, and say what it expanded to. A +/// module that is not gzip passes as it is: its size is its own, and it +/// expands to nothing. +fn gunzipped_within(path: &Utf8Path, bytes: &[u8], limit: u64) -> Result { + if !bytes.starts_with(&[0x1f, 0x8b]) { + return Ok(0); + } + let mut decoder = Limited { inner: flate2::read::GzDecoder::new(bytes), left: limit }; + match std::io::copy(&mut decoder, &mut std::io::sink()) { + Ok(expanded) => Ok(expanded), + Err(_) => Err(Unsupported(format!( + "the gzip-compressed wasm '{path}' expands past {limit} bytes, the most a module \ + can be, or is not valid gzip" + ))), + } +} + +/// The script runner the syncer is composed with. A bundle has no script +/// step for it to run — the vetted loader refuses a project with one — so +/// this spawns nothing; it is here because the syncer takes a runner, not +/// as a second gate. +pub struct NoScripts; + +#[async_trait] +impl ScriptRunner for NoScripts { + async fn run_script( + &self, + _invocation: ScriptInvocation, + _reporter: &StepReporter, + ) -> Result, ScriptRunError> { + Err(ScriptRunError { + source: Box::new(Unsupported( + "a script sync step reached the runner, which the vetted loader admits none of" + .into(), + )), + }) + } +} + +// ── The check, the host, the extraction ─────────────────────────────────── + +/// Hold an environment's canisters to the shape a bundle has — before any +/// phase runs, so a bundle that would fail at its build or sync phase, or +/// be refused there, does not first spend cycles creating canisters. +/// +/// Each canister is built by exactly one pre-built step naming a wasm in the +/// archive that hashes to the checksum the manifest records; each sync step +/// is a plugin in the archive (hashing to its checksum, if one is recorded). +/// The environment's network is the Internet Computer. That no step is a +/// script and no module or plugin is remote, the loader has already seen +/// to ([`Vetted`]): the check reads what the loaded project names. +/// +/// Canister names must also differ by more than letter case: the library's +/// artifact store keys a canister's build output by its name on disk, and +/// a case-insensitive volume (macOS and Windows by default) would hand the +/// last-built module to both. +/// +/// What a plugin declares under `dirs:` and `files:` is held to the bundle +/// the way the runtime will hold it at sync, and the files to what the +/// runtime will read of them ([`check_declared`]). +/// +/// The work the check does is bounded by the archive, not by the manifest: +/// a file is read, hashed and expanded once however many canisters name it +/// ([`Verified`]), and the expansions are summed ([`MAX_WASM_BYTES_IN_ALL`]). +/// +/// Where each canister lives is the loader's business: it finds a canister +/// directory through the confined filesystem, so one outside the bundle root +/// — which the sync-plugin runtime would take as its own sandbox root — is +/// not found and the project does not load. +pub fn check_bundle<'a>( + fs: &BundleFs, + network: &Network, + canisters: impl IntoIterator, +) -> Result<(), String> { + check_bundle_within(fs, network, canisters, MAX_WASM_BYTES_IN_ALL, MAX_PLUGIN_INPUT_BYTES) +} + +/// [`check_bundle`] with the two sums as parameters, so a test can reach +/// them without a gigabyte of modules. +fn check_bundle_within<'a>( + fs: &BundleFs, + network: &Network, + canisters: impl IntoIterator, + wasm_in_all: u64, + plugin_input: u64, +) -> Result<(), String> { + IcMainnet::mainnet(network).map_err(|e| e.0)?; + let canisters: Vec<_> = canisters.into_iter().collect(); + let mut by_folded: BTreeMap = BTreeMap::new(); + for (_, canister) in &canisters { + if let Some(other) = by_folded.insert(canister.name.to_lowercase(), &canister.name) { + if other != canister.name { + return Err(format!( + "canisters '{other}' and '{}' differ only in letter case; the build artifact \ + store keys files by canister name, which a case-insensitive disk would merge", + canister.name + )); + } + } + } + let mut verified = Verified { hashes: BTreeMap::new(), wasm_left: wasm_in_all }; + for (dir, canister) in canisters { + let name = &canister.name; + let at = |e: Unsupported| format!("canister '{name}': {e}"); + + let [BuildStep::Prebuilt(adapter)] = canister.build.steps.as_slice() else { + return Err(at(Unsupported(format!( + "built by {} steps where a bundle has exactly one pre-built step; rebuild the \ + bundle with `icp project bundle`", + canister.build.steps.len() + )))); + }; + let SourceField::Local(local) = &adapter.source else { + return Err(at(Unsupported( + "built from a remote wasm source, which a bundle never is; rebuild the bundle \ + with `icp project bundle`" + .into(), + ))); + }; + let actual = verified.sha256(fs, dir, &local.path).map_err(at)?; + let Some(expected) = adapter.sha256.as_deref() else { + return Err(at(Unsupported(format!( + "the wasm '{}' carries no sha256, which a bundle always records; rebuild the \ + bundle with `icp project bundle`", + local.path + )))); + }; + checksum_matches(&local.path, expected, &actual).map_err(at)?; + + for step in &canister.sync.steps { + let SyncStep::Plugin(plugin) = step else { continue }; + let SourceField::Local(local) = &plugin.source else { + return Err(at(Unsupported( + "synced by a remote plugin, which a bundle never is; rebuild the bundle \ + with `icp project bundle`" + .into(), + ))); + }; + let actual = verified.sha256(fs, dir, &local.path).map_err(at)?; + if let Some(expected) = plugin.sha256.as_deref() { + checksum_matches(&local.path, expected, &actual).map_err(at)?; + } + let files = plugin.files.iter().flat_map(NamedPaths::entries).map(|entry| entry.path); + check_declared(fs, dir, files, plugin_input).map_err(at)?; + } + } + Ok(()) +} + +/// What [`check_bundle`] has read so far, by where each file lies on disk: +/// a module or plugin that several canisters name — or one canister names +/// under several spellings — is read, hashed and expanded once, so the work +/// the check does is bounded by what the archive holds, not by what the +/// manifest repeats. +struct Verified { + /// Each file's sha256, in hex, by its on-disk path. + hashes: BTreeMap, + /// What the gzip-compressed files seen so far may still expand to, in all. + wasm_left: u64, +} + +impl Verified { + /// The sha256 of the file at `path` under `dir`, read and hashed on first + /// sight — when a gzip-compressed one is also held to [`MAX_WASM_BYTES`] + /// expanded, and its expansion counted against the sum. + fn sha256( + &mut self, + fs: &BundleFs, + dir: &Utf8Path, + path: &Utf8Path, + ) -> Result { + let on_disk = locate(fs, &dir.join(path))?; + if let Some(hash) = self.hashes.get(&on_disk) { + return Ok(hash.clone()); + } + let bytes = + std::fs::read(&on_disk).map_err(|e| Unsupported(format!("'{on_disk}': {e}")))?; + let expanded = gunzipped_within(path, &bytes, MAX_WASM_BYTES)?; + if expanded > self.wasm_left { + return Err(Unsupported(format!( + "the bundle's gzip-compressed modules expand to more than \ + {MAX_WASM_BYTES_IN_ALL} bytes in all, more than a bundle extracts to" + ))); + } + self.wasm_left -= expanded; + let hash = hex::encode(Sha256::digest(&bytes)); + self.hashes.insert(on_disk, hash.clone()); + Ok(hash) + } +} + +/// Hold the bytes a sync step's `files:` declare to `budget`, counting a +/// file each time it is declared: the runtime reads every declared file +/// into memory before the plugin runs. That each `dirs:` and `files:` entry +/// is in the bundle — under the root, through no symlink, a directory or a +/// regular file — the vet settled at the load; which of the two settings +/// the plugin's interface accepts, and whether an entry carries a name, is +/// the plugin's to say and stays the runtime's to tell. +fn check_declared<'a>( + fs: &BundleFs, + dir: &Utf8Path, + files: impl IntoIterator, + budget: u64, +) -> Result<(), Unsupported> { + let mut left = budget; + for path in files { + // Where the vet saw it; a path the vet did not pass never gets here. + let not_there = || { + Unsupported(format!( + "its sync step declares '{path}' under `files:`, which is not in the bundle" + )) + }; + let on_disk = fs.resolve(&dir.join(path)).map_err(|_| not_there())?; + let meta = std::fs::metadata(&on_disk).map_err(|_| not_there())?; + if meta.is_file() { + if meta.len() > left { + return Err(Unsupported(format!( + "its sync step's `files:` hold more than {budget} bytes in all, counting a \ + file each time it is declared, more than a plugin's inputs ever do; the \ + runtime reads every declared file into memory before the plugin runs" + ))); + } + left -= meta.len(); + } + } + Ok(()) +} + +/// The host for the bundle extracted into `fs`: its manifest is the project, +/// its `.icp` directory the id and artifact stores, and nothing outside it is +/// reachable. +/// +/// The project is loaded through a [`Metered`] view of the bundle, so what +/// the loader reads and retains — once per canister that names a file — stays +/// within [`MAX_PROJECT_BYTES`]; the phases read the bundle itself. +/// +/// What is not bounded here: the sync-plugin runtime (`icp-sync-plugin`'s +/// wasmtime runner) holds a plugin to a compute-time budget and a stack +/// limit, and each of its linear memories to the wasm32 address space, but +/// sets no store limiter, so a plugin may allocate up to that space before +/// its time runs out. The runner builds its own engine and store, so the +/// limiter belongs there, upstream; until it has one, a bundle's plugins run +/// with the memory bound the icp CLI gives them. Nor is the number of +/// canisters or steps a manifest declares: the phases do their work once per +/// canister, and each canister costs the caller the cycles that create it. +pub fn host_for(fs: BundleFs) -> Host { + host_within(fs, MAX_PROJECT_BYTES) +} + +/// [`host_for`] with the project-loading budget as a parameter, so a test can +/// reach it without a quarter of a gigabyte of argument files. +fn host_within(fs: BundleFs, project_bytes: u64) -> Host { + // The directory IS the project root: no climbing to a workspace above it. + let root = fs.root().to_path_buf(); + let locate: Arc = + Arc::new(ProjectRootLocateImpl::new(root.clone(), Some(root))); + let files: Arc = Arc::new(fs.clone()); + let wasm: Arc = Arc::new(BundledWasm(fs.clone())); + // The untrusted rule, composed as `icp --untrusted` composes it: the + // project is read through the bundle's own filesystem, no recipe is + // fetched, and the project is vetted as it loads, each path a step + // names asked of that filesystem. The loader is the one gate on the + // project's shape, so the builder is the library's own. + let project = Lazy::<_, Project>::new(Vetted::new( + ProjectLoadImpl { + project_root_locate: locate.clone(), + recipe: Arc::new(NoRecipes), + files: Arc::new(Metered::new(fs, project_bytes)), + }, + files.clone(), + )); + Host { + project: Arc::new(project), + files: files.clone(), + ids: Arc::new(AccessImpl::new(locate.clone())), + artifacts: Arc::new(ArtifactStore::new(locate)), + builder: Arc::new(Builder::new(wasm.clone(), files)), + syncer: Arc::new(Syncer::new( + Arc::new(NoScripts), + wasm.clone(), + Arc::new(icp_sync_plugin::Wasmtime), + )), + wasm, + network: Arc::new(IcMainnet), + random: Arc::new(HostRandom), + observer: Arc::new(Ignore), + } +} + +/// Extract a bundle archive into `into`, which must exist and be empty, and +/// return it as the filesystem the deploy reads through, confined to it. +/// +/// Only regular files and directories are written — `icp project bundle` +/// writes nothing else, and a symlink or a device node is how an archive +/// reaches past its directory — and their sizes may not add up to more than +/// [`MAX_EXTRACTED_BYTES`], nor their number, with the directories their +/// paths imply, to more than [`MAX_BUNDLE_ENTRIES`]. `tar` refuses entry +/// paths that would land outside `into`. What it extracts is then required +/// to look like a bundle, i.e. to carry a project manifest at its root. +pub fn extract_bundle(bytes: &[u8], into: &Utf8Path) -> Result { + extract_bundle_within(bytes, into, MAX_BUNDLE_ENTRIES) +} + +/// [`extract_bundle`] with the files-and-directories cap as a parameter, so a +/// test can reach it without writing a quarter of a million files. +fn extract_bundle_within( + bytes: &[u8], + into: &Utf8Path, + max_entries: usize, +) -> Result { + if bytes.len() > MAX_BUNDLE_BYTES { + return Err(format!( + "the bundle is {} bytes, above the {} byte limit", + bytes.len(), + MAX_BUNDLE_BYTES + )); + } + // A link is refused on the way in, so nothing under the root leads out + // of it; should one appear later, the filesystem refuses any path + // through it. The root is held as spelled: it and everything above it + // are this crate's own (macOS keeps its temporary directories under + // `/var`, a link to `/private/var`), and only what lies below is held + // to shape. + let fs = Confined::new(into).map_err(|e| { + let mut why = e.to_string(); + if let Some(cause) = std::error::Error::source(&e) { + why.push_str(": "); + why.push_str(&cause.to_string()); + } + format!("the extraction directory cannot be confined to: {why}") + })?; + let unreadable = + |e: std::io::Error| format!("the bundle is not a readable .tar.gz archive: {e}"); + let decoder = Limited { inner: flate2::read::GzDecoder::new(bytes), left: MAX_EXTRACTED_BYTES }; + let mut archive = tar::Archive::new(decoder); + let mut total: u64 = 0; + // Files and directories written so far, counting each directory an + // entry's path implies: `unpack_in` creates the missing parents of every + // entry, so a few files at the bottom of long, distinct directory chains + // would otherwise cost inodes the entry count never sees. + let mut objects: usize = 0; + let too_many = || { + format!( + "the bundle holds more than {max_entries} files and directories, more than a bundle \ + ever has" + ) + }; + // The directories that exist so far, written as entries or implied by + // the paths of earlier ones, relative to the root. + let mut dirs: BTreeSet = BTreeSet::new(); + for entry in archive.entries().map_err(unreadable)? { + let mut entry = entry.map_err(unreadable)?; + objects += 1; + if objects > max_entries { + return Err(too_many()); + } + let kind = entry.header().entry_type(); + let path = entry.path().map_err(unreadable)?.into_owned(); + let path = Utf8PathBuf::from_path_buf(path) + .map_err(|p| format!("the archive entry '{}' is not UTF-8", p.display()))?; + if path.as_str().len() > MAX_ENTRY_PATH_BYTES { + return Err(format!( + "an archive entry's path is {} bytes long, more than the {MAX_ENTRY_PATH_BYTES} \ + a path can be", + path.as_str().len() + )); + } + if !(kind.is_file() || kind.is_dir()) { + return Err(format!( + "the archive entry '{path}' is not a regular file or directory ({kind:?}), which \ + a bundle never carries; rebuild it with `icp project bundle`" + )); + } + // Where the entry lands, relative to the root: `tar` refuses a path + // that would leave `into`, so resolving it against the root is only + // a matter of spelling (`./x` is `x`). + let rel = match fs.resolve(&path) { + Ok(resolved) => resolved.strip_prefix(fs.root()).unwrap_or(&resolved).to_path_buf(), + Err(_) => { + return Err(format!( + "the archive entry '{path}' would land outside the extraction directory" + )) + } + }; + // `.icp` is the library's own state under the project root: the id + // mappings a deploy reads to decide what exists, and the artifact + // store. A bundle never carries it (`icp project bundle` writes + // manifests, modules, plugins, argument files and images), and one + // that did could point a deploy at canisters the caller never named. + // Compared without regard to case: on a case-insensitive disk `.ICP` + // is the same directory. + if rel.components().next().is_some_and(|c| c.as_str().eq_ignore_ascii_case(STATE_DIR)) { + return Err(format!( + "the archive entry '{path}' is under `{STATE_DIR}/`, the project state a bundle \ + never carries; rebuild it with `icp project bundle`" + )); + } + // The parents this entry implies that nothing before it did. A known + // directory's own parents are known too, so the walk up stops at the + // first one. + let implied: Vec = rel + .ancestors() + .skip(1) + .filter(|dir| !dir.as_str().is_empty()) + .take_while(|dir| !dirs.contains(*dir)) + .map(Utf8Path::to_path_buf) + .collect(); + objects = objects.saturating_add(implied.len()); + if objects > max_entries { + return Err(too_many()); + } + total = total.saturating_add(entry.size()); + if total > MAX_EXTRACTED_BYTES { + return Err(too_large()); + } + if entry.unpack_in(fs.root().as_std_path()).map_err(unreadable)? { + dirs.extend(implied); + if kind.is_dir() { + dirs.insert(rel); + } + } + } + if !fs.root().join(PROJECT_MANIFEST).is_file() { + return Err(format!( + "the archive carries no `{PROJECT_MANIFEST}` at its root, so it is not an icp \ + project bundle (`icp project bundle` writes one)" + )); + } + Ok(fs) +} + +fn too_large() -> String { + format!("the bundle extracts to more than the {MAX_EXTRACTED_BYTES} byte limit") +} + +/// A reader that fails once more than `left` bytes have passed through it: +/// the decompressed stream's own bound, independent of what the entry +/// headers claim. +struct Limited { + inner: R, + left: u64, +} + +impl Read for Limited { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let n = self.inner.read(buf)?; + if n as u64 > self.left { + return Err(std::io::Error::other(too_large())); + } + self.left -= n as u64; + Ok(n) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use icp_project::{ + host::EnvironmentSelection, + network::{Connected, Managed}, + Environment, + }; + use std::io::Write; + + fn managed(name: &str) -> Network { + Network { + name: name.into(), + configuration: Configuration::Managed { managed: Managed::default() }, + } + } + + fn connected(name: &str, root_key: RootKeySpec) -> Network { + Network { + name: name.into(), + configuration: Configuration::Connected { + connected: Connected { + api_url: "https://icp-api.io".parse().unwrap(), + http_gateway_url: Some("https://icp.net".parse().unwrap()), + root_key, + }, + }, + } + } + + /// A gzipped tar of the given entries, each a regular file. + fn archive(entries: &[(&str, &[u8])]) -> Vec { + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + for (path, data) in entries { + let mut header = tar::Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + archive.append_data(&mut header, path, *data).unwrap(); + } + archive.into_inner().unwrap().finish().unwrap() + } + + fn scratch() -> (tempfile::TempDir, Utf8PathBuf) { + let dir = tempfile::tempdir().unwrap(); + let path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + (dir, path) + } + + // Only the Internet Computer: a bundle built for `local` names a managed + // network, a testnet a connected one with a fetched key; both are refused + // with the way out, not deep inside the deploy. + #[tokio::test] + async fn only_the_ic_network_is_served() { + // A manifest may declare a network of any name, and it may claim the + // mainnet root key over an endpoint of its own: refused, like a + // network to be launched or one whose key would be fetched. So is + // the reserved name over another endpoint, should a loader ever let + // that through. + let mut elsewhere = connected("mainnet2", RootKeySpec::Mainnet); + let Configuration::Connected { connected: config } = &mut elsewhere.configuration else { + unreachable!() + }; + config.api_url = "http://127.0.0.1:4943".parse().unwrap(); + let mut ic_elsewhere = elsewhere.clone(); + ic_elsewhere.name = "ic".into(); + for network in [ + managed("local"), + connected("testnet", RootKeySpec::Fetch), + connected("mainnet2", RootKeySpec::Mainnet), + elsewhere, + ic_elsewhere, + ] { + let err = match IcMainnet.urls(&network).await { + Err(e) => e.to_string(), + Ok(_) => panic!("{} must be refused", network.name), + }; + assert!(err.contains("not the Internet Computer"), "{err}"); + assert!(err.contains("`ic`"), "{err}"); + assert!(IcMainnet.access(&network).await.is_err()); + } + } + + // The mainnet root key is pinned, never fetched: a bundle for `ic` + // resolves without touching the network. + #[tokio::test] + async fn the_ic_network_resolves_to_the_pinned_root_key() { + let access = IcMainnet.access(&connected("ic", RootKeySpec::Mainnet)).await.unwrap(); + assert_eq!(access.root_key, IC_ROOT_KEY.to_vec()); + assert_eq!(access.root_key_source, RootKeySource::Mainnet); + assert_eq!(access.api_url.as_str(), "https://icp-api.io/"); + } + + #[test] + fn an_archive_without_a_manifest_is_not_a_bundle() { + let (_dir, into) = scratch(); + let err = extract_bundle(&archive(&[("README", b"hello")]), &into).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + } + + #[test] + fn garbage_is_not_an_archive() { + let (_dir, into) = scratch(); + let err = extract_bundle(b"not a tarball", &into).unwrap_err(); + assert!(err.contains("not a readable"), "{err}"); + } + + #[test] + fn oversized_bundles_are_refused_before_extraction() { + let (_dir, into) = scratch(); + let big = vec![0u8; MAX_BUNDLE_BYTES + 1]; + let err = extract_bundle(&big, &into).unwrap_err(); + assert!(err.contains("limit"), "{err}"); + } + + // A small archive can claim a huge file: the entry's size is what tar + // writes (as a hole, if the data is not there), so the budget is checked + // against the sizes before anything is written. + #[test] + fn an_archive_that_extracts_past_the_budget_is_refused() { + let (_dir, into) = scratch(); + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + let mut header = tar::Header::new_gnu(); + header.set_path("huge").unwrap(); + header.set_size(MAX_EXTRACTED_BYTES + 1); + header.set_mode(0o644); + header.set_cksum(); + // Only the header is appended: the claimed data never follows. + archive.get_mut().write_all(header.as_bytes()).unwrap(); + let bytes = archive.into_inner().unwrap().finish().unwrap(); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("extracts to more than"), "{err}"); + assert!(!into.join("huge").exists()); + } + + // A symlink is how an archive reaches past its directory; a bundle + // carries none, so one is refused rather than written. Should one appear + // later, the filesystem refuses any path through it (the test after + // this), so neither gate stands alone. + #[test] + fn an_archive_with_a_symlink_is_refused() { + let (_dir, into) = scratch(); + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Symlink); + header.set_size(0); + header.set_mode(0o777); + header.set_cksum(); + archive.append_link(&mut header, "icp.yaml", "/etc/passwd").unwrap(); + let bytes = archive.into_inner().unwrap().finish().unwrap(); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("not a regular file or directory"), "{err}"); + assert!(!into.join("icp.yaml").exists()); + } + + // Should a link appear under the root after extraction, the filesystem + // refuses every path through it, wherever it leads: the vet sees a + // module named through one as not a regular file of the project, and a + // read through one is refused naming the link. The untrusted rule's own + // test, run through this host. + #[cfg(unix)] + #[tokio::test] + async fn a_module_named_through_a_link_fails_the_load() { + use std::os::unix::fs::symlink; + let manifest = |path: &str| { + format!( + "canisters:\n - name: backend\n build:\n steps:\n - type: \ + pre-built\n path: {path}\n" + ) + }; + let elsewhere = tempfile::tempdir().unwrap(); + let secret = elsewhere.path().join("secret.wasm"); + std::fs::write(&secret, b"\0asm").unwrap(); + + // Out: refused at the load, naming the path. + let (_dir, into) = scratch(); + let bytes = archive(&[("icp.yaml", manifest("out.wasm").as_bytes())]); + let fs = extract_bundle(&bytes, &into).unwrap(); + symlink(&secret, into.join("out.wasm")).unwrap(); + let err = chain(&host_for(fs.clone()).project.load().await.expect_err("a link out")); + assert!(err.contains("not a regular file in the project"), "{err}"); + assert!(err.contains("out.wasm"), "{err}"); + // And a read through it is refused naming the link, not followed. + let err = chain(&fs.read(Utf8Path::new("out.wasm")).await.unwrap_err()); + assert!(err.contains("passes through") && err.contains("out.wasm"), "{err}"); + + // Within: refused all the same; the rule is no link, not no link out. + let (_dir, into) = scratch(); + let bytes = + archive(&[("icp.yaml", manifest("in.wasm").as_bytes()), ("backend.wasm", b"\0asm")]); + let fs = extract_bundle(&bytes, &into).unwrap(); + symlink("backend.wasm", into.join("in.wasm")).unwrap(); + let err = chain(&host_for(fs.clone()).project.load().await.expect_err("a link within")); + assert!(err.contains("not a regular file in the project"), "{err}"); + assert!(err.contains("in.wasm"), "{err}"); + assert!(fs.read(Utf8Path::new("in.wasm")).await.is_err()); + assert_eq!(fs.read(Utf8Path::new("backend.wasm")).await.unwrap(), b"\0asm"); + + // Dangling: a link is a link, wherever it leads or fails to. + let (_dir, into) = scratch(); + let bytes = archive(&[("icp.yaml", manifest("gone.wasm").as_bytes())]); + let fs = extract_bundle(&bytes, &into).unwrap(); + symlink(into.join("nowhere.wasm"), into.join("gone.wasm")).unwrap(); + let err = chain(&host_for(fs).project.load().await.expect_err("a dangling link")); + assert!(err.contains("not a regular file in the project"), "{err}"); + assert!(err.contains("gone.wasm"), "{err}"); + } + + // What a step names must be a regular file: a directory of the bundle + // named as a module is refused at the load, naming what was found. (A + // pipe cannot arrive in an archive: extraction writes files and + // directories alone.) + #[tokio::test] + async fn a_directory_named_as_a_module_fails_the_load() { + let manifest = "canisters:\n - name: backend\n build:\n steps:\n \ + - type: pre-built\n path: assets\n"; + let (_dir, into) = scratch(); + let bytes = archive_with_dirs(&[("icp.yaml", manifest.as_bytes())], &["assets"]); + let fs = extract_bundle(&bytes, &into).unwrap(); + let err = chain(&host_for(fs).project.load().await.expect_err("a directory")); + assert!(err.contains("not a regular file"), "{err}"); + assert!(err.contains("assets"), "{err}"); + } + + // The root is held as spelled: named through a link, the extraction + // lands in the directory the link leads to, the filesystem answers to + // the spelling it was given, and the project in it loads — the root and + // everything above it are the caller's, and only what lies below is + // held to shape. + #[cfg(unix)] + #[tokio::test] + async fn a_root_named_through_a_link_holds_the_project() { + let manifest = "canisters:\n - name: backend\n build:\n steps:\n \ + - type: pre-built\n path: backend.wasm\n"; + let (dir, into) = scratch(); + let alias = Utf8PathBuf::from_path_buf(dir.path().with_extension("alias")).unwrap(); + std::os::unix::fs::symlink(&into, &alias).unwrap(); + let bytes = archive(&[("icp.yaml", manifest.as_bytes()), ("backend.wasm", b"\0asm")]); + let fs = extract_bundle(&bytes, &alias).unwrap(); + assert_eq!(fs.root(), alias); + assert!(into.join("backend.wasm").is_file()); + let project = host_for(fs.clone()).project.load().await.unwrap(); + assert_eq!(project.canisters.len(), 1); + assert_eq!(fs.read(Utf8Path::new("backend.wasm")).await.unwrap(), b"\0asm"); + std::fs::remove_file(&alias).unwrap(); + } + + /// An error and its causes, outermost first. + fn chain(err: &(dyn std::error::Error + 'static)) -> String { + let mut text = err.to_string(); + let mut source = err.source(); + while let Some(cause) = source { + text.push_str(": "); + text.push_str(&cause.to_string()); + source = cause.source(); + } + text + } + + // The rule for a project someone else handed over is the library's own, + // and it holds at the load: a script step or a module fetched from a URL + // fails the load, before any phase could act on it. So does a recipe: + // the library would read or fetch it while loading, and an untrusted + // project may name none. + #[tokio::test] + async fn a_project_asking_more_than_an_untrusted_one_may_fails_to_load() { + for (manifest, refused) in [ + ( + "canisters:\n - name: backend\n build:\n steps:\n - type: script\n command: touch /tmp/pwned\n", + "not a pre-built module, which is all an untrusted project may have", + ), + ( + "canisters:\n - name: backend\n build:\n steps:\n - type: pre-built\n url: https://example.com/backend.wasm\n sha256: abc\n", + "not a pre-built module, which is all an untrusted project may have", + ), + ( + "canisters:\n - name: backend\n recipe:\n type: \"@dfinity/rust@v1\"\n", + "recipe '@dfinity/rust@v1' is refused: an untrusted project may not use a recipe", + ), + ] { + let (_dir, into) = scratch(); + let fs = extract_bundle(&archive(&[("icp.yaml", manifest.as_bytes())]), &into).unwrap(); + let err = host_for(fs).project.load().await.expect_err(refused); + let err = chain(&err); + assert!(err.contains(refused), "{refused}: {err}"); + // The refusal names the step, so the reader sees what was asked. + let named = ["touch /tmp/pwned", "example.com/backend.wasm", "@dfinity/rust@v1"]; + assert!(named.iter().any(|n| err.contains(n)), "{err}"); + } + } + + #[test] + fn an_archive_carrying_project_state_is_refused() { + let (_dir, into) = scratch(); + let bytes = archive(&[ + ("icp.yaml", b"canisters: []"), + (".icp/data/mappings/ic.ids.json", b"{\"backend\": \"aaaaa-aa\"}"), + ]); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("`.icp/`"), "{err}"); + assert!(!into.join(".icp").exists()); + + // Spelled `.ICP/`: the same directory on a case-insensitive disk. + let (_dir, into) = scratch(); + let bytes = archive(&[ + ("icp.yaml", b"canisters: []"), + (".ICP/data/mappings/ic.ids.json", b"{\"backend\": \"aaaaa-aa\"}"), + ]); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("`.icp/`"), "{err}"); + assert!(!into.join(".ICP").exists()); + + // The same entry spelled `./.icp/...`: the archive path's first + // component is `.`, extraction writes it into `.icp/` all the same. + let (_dir, into) = scratch(); + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + let data: &[u8] = b"{\"backend\": \"aaaaa-aa\"}"; + let mut header = tar::Header::new_gnu(); + header.set_path("./.icp/data/mappings/ic.ids.json").unwrap(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + archive.append(&header, data).unwrap(); + let bytes = archive.into_inner().unwrap().finish().unwrap(); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("`.icp/`"), "{err}"); + assert!(!into.join(".icp").exists()); + } + + // The cap counts files and directories alike, the directories by what + // the paths imply: nine files under one directory are ten objects. + #[test] + fn an_archive_with_too_many_entries_is_refused() { + let (_dir, into) = scratch(); + let paths: Vec = (0..=8).map(|i| format!("f/{i}")).collect(); + let bytes = empty_files(&paths); + let err = extract_bundle_within(&bytes, &into, 9).unwrap_err(); + assert!(err.contains("more than 9 files and directories"), "{err}"); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 10).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + } + + // An entry's path implies the directories above it, which extraction + // creates whether or not the archive lists them: they count once each, + // however many entries share them, and an entry written as a directory + // counts as itself. + #[test] + fn the_directories_an_archive_implies_count_against_its_cap() { + // Two files at the bottom of one chain: 2 files + 3 directories. + let bytes = empty_files(&["a/b/c/x".into(), "a/b/c/y".into()]); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 4).unwrap_err(); + assert!(err.contains("more than 4 files and directories"), "{err}"); + assert!(!into.join("a/b/c/y").exists(), "the refused entry is not written"); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 5).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + + // Two files under distinct chains share nothing: 2 files + 6 dirs. + let bytes = empty_files(&["a/b/c/x".into(), "d/e/f/y".into()]); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 7).unwrap_err(); + assert!(err.contains("more than 7 files and directories"), "{err}"); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 8).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + + // One file far down a chain is refused on its own, before it is + // written: the directories it would create are the cost. + let deep = (0..64).map(|_| "d").collect::>().join("/") + "/x"; + let bytes = empty_files(&[deep]); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 32).unwrap_err(); + assert!(err.contains("more than 32 files and directories"), "{err}"); + assert!(!into.join("d").exists()); + + // A directory entry counts as itself; a file under it then implies + // nothing new: 1 dir entry + 1 file. + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_cksum(); + archive.append_data(&mut header, "a/", &b""[..]).unwrap(); + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_mode(0o644); + header.set_cksum(); + archive.append_data(&mut header, "a/x", &b""[..]).unwrap(); + let bytes = archive.into_inner().unwrap().finish().unwrap(); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 2).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + assert!(into.join("a/x").exists()); + } + + // Counting what a path implies costs a lookup per component, so the + // path is held to a length first — one no filesystem would take anyway. + #[test] + fn an_entry_path_past_the_length_limit_is_refused() { + let long = (0..=MAX_ENTRY_PATH_BYTES / 2).map(|_| "a").collect::>().join("/"); + assert!(long.len() > MAX_ENTRY_PATH_BYTES); + let bytes = empty_files(&[long]); + let (_dir, into) = scratch(); + let err = extract_bundle(&bytes, &into).unwrap_err(); + assert!(err.contains("bytes long"), "{err}"); + assert!(!into.join("a").exists()); + } + + /// A gzipped tar of empty regular files at the given paths. + fn empty_files(paths: &[String]) -> Vec { + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + for path in paths { + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_mode(0o644); + header.set_cksum(); + archive.append_data(&mut header, path, &b""[..]).unwrap(); + } + archive.into_inner().unwrap().finish().unwrap() + } + + /// A gzipped tar of the given regular files, then directory entries at + /// the given paths. + fn archive_with_dirs(files: &[(&str, &[u8])], dirs: &[&str]) -> Vec { + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::fast(), + )); + for (path, data) in files { + let mut header = tar::Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + archive.append_data(&mut header, path, *data).unwrap(); + } + for path in dirs { + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_cksum(); + archive.append_data(&mut header, path, &b""[..]).unwrap(); + } + archive.into_inner().unwrap().finish().unwrap() + } + + // A directory entry makes its parents known like a file does: two + // sibling directory entries under one chain are four directories, the + // shared parents counted once. + #[test] + fn the_parents_a_directory_entry_implies_are_counted_once() { + let bytes = archive_with_dirs(&[], &["a/b/c/", "a/b/d/"]); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 3).unwrap_err(); + assert!(err.contains("more than 3 files and directories"), "{err}"); + let (_dir, into) = scratch(); + let err = extract_bundle_within(&bytes, &into, 4).unwrap_err(); + assert!(err.contains(PROJECT_MANIFEST), "{err}"); + assert!(into.join("a/b/d").is_dir()); + } + + // A directory the archive lists as an entry of its own, with nothing + // under it, is a directory of the bundle: it is on disk, so it is found + // and listed like any other. + #[tokio::test] + async fn an_empty_directory_entry_is_a_directory_of_the_bundle() { + let (_dir, into) = scratch(); + let bytes = archive_with_dirs(&[("icp.yaml", b"canisters: []")], &["assets/"]); + let fs = extract_bundle(&bytes, &into).unwrap(); + let root = fs.root().to_path_buf(); + assert!(fs.is_dir(Utf8Path::new("assets")).await); + assert!(fs.exists(Utf8Path::new("assets")).await); + assert_eq!( + fs.read_dir(&root).await.unwrap(), + vec![root.join("assets"), root.join("icp.yaml")] + ); + assert!(fs.read_dir(Utf8Path::new("assets")).await.unwrap().is_empty()); + } + + // A gzip-compressed module is bounded by what it expands to, read + // through a bounded decoder; a plain module passes as it is. + #[test] + fn a_gzipped_module_is_held_to_its_limit() { + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + encoder.write_all(&[0u8; 4096]).unwrap(); + let gz = encoder.finish().unwrap(); + let path = Utf8Path::new("backend.wasm.gz"); + assert_eq!(gunzipped_within(path, &gz, 8192).unwrap(), 4096); + let err = gunzipped_within(path, &gz, 1024).unwrap_err(); + assert!(err.0.contains("expands past 1024"), "{err}"); + assert_eq!(gunzipped_within(path, b"\0asm plain", 1).unwrap(), 0); + } + + // The confined filesystem serves what the archive carried, by any spelling + // that resolves to it, and refuses what resolves outside the root — an + // absolute path elsewhere, a climb above the root — whatever the machine + // holds at that spelling. (The filesystem's own tests, upstream, cover + // the spellings; this is the bundle seen through it.) + #[tokio::test] + async fn the_bundle_serves_what_it_carries_and_nothing_outside_it() { + let (_dir, into) = scratch(); + let fs = extract_bundle( + &archive(&[("icp.yaml", b"canisters: []"), ("./canisters/a.wasm", b"wasm")]), + &into, + ) + .unwrap(); + let root = fs.root().to_path_buf(); + + for spelling in ["canisters/a.wasm", "./canisters/a.wasm", "canisters/../canisters/a.wasm"] + { + assert_eq!(fs.read(Utf8Path::new(spelling)).await.unwrap(), b"wasm", "{spelling}"); + assert_eq!( + locate(&fs, Utf8Path::new(spelling)).unwrap(), + root.join("canisters/a.wasm") + ); + } + // The absolute paths the library derives from the root resolve too. + assert_eq!(fs.read(&root.join("canisters/a.wasm")).await.unwrap(), b"wasm"); + assert!(fs.is_dir(Utf8Path::new("canisters")).await); + assert!(fs.is_dir(&root).await); + assert_eq!( + fs.read_dir(&root).await.unwrap(), + vec![root.join("canisters"), root.join("icp.yaml")] + ); + + for escape in ["/etc/hostname", "../../../../etc/hostname", "canisters/../../x"] { + let err = fs.read(Utf8Path::new(escape)).await.unwrap_err().to_string(); + assert!(err.contains("is outside"), "{escape}: {err}"); + assert!(!fs.exists(Utf8Path::new(escape)).await, "{escape}"); + let err = locate(&fs, Utf8Path::new(escape)).unwrap_err().to_string(); + assert!(err.contains("is outside"), "{escape}: {err}"); + } + // Inside the root but not there: not in the bundle. + let err = locate(&fs, Utf8Path::new("canisters/b.wasm")).unwrap_err().to_string(); + assert!(err.contains("'canisters/b.wasm' is not in the bundle"), "{err}"); + } + + // The loader reads an argument file into every canister that names it, + // so two canisters naming one file read it twice, and the budget counts + // both: what the project retains is bounded, not what the archive holds. + #[tokio::test] + async fn loading_the_project_is_held_to_its_read_budget() { + let manifest = "canisters:\n - name: a\n init_args:\n path: args.bin\n \ + format: bin\n build:\n steps:\n - type: pre-built\n \ + path: a.wasm\n - name: b\n init_args:\n path: args.bin\n \ + format: bin\n build:\n steps:\n - type: pre-built\n \ + path: a.wasm\n"; + let args = [7u8; 1000]; + // The module is in the archive, as the vet asks; what the loader + // reads of it is nothing. + let bytes = archive(&[ + ("icp.yaml", manifest.as_bytes()), + ("args.bin", &args), + ("a.wasm", b"\0asm"), + ]); + + // Room for the manifest and one read of the file, not two. + let (_dir, into) = scratch(); + let fs = extract_bundle(&bytes, &into).unwrap(); + let host = host_within(fs, manifest.len() as u64 + 1500); + let err = host.project.load().await.expect_err("the second read is over budget"); + let chain = std::iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()) + .map(ToString::to_string) + .collect::>() + .join(": "); + assert!(chain.contains("canister 'b'"), "{chain}"); + assert!(chain.contains("loading the project reads more than"), "{chain}"); + + // Room for both. + let (_dir, into) = scratch(); + let fs = extract_bundle(&bytes, &into).unwrap(); + let host = host_within(fs, manifest.len() as u64 + 2000); + let project = host.project.load().await.unwrap(); + assert_eq!(project.canisters.len(), 2); + } + + // The budget is the loader's alone: the phases read the bundle itself, + // where a module is read as many times as the deploy needs it. + #[tokio::test] + async fn the_read_budget_meters_reads_and_nothing_else() { + let (_dir, into) = scratch(); + let fs = extract_bundle( + &archive(&[("icp.yaml", b"canisters: []"), ("big", &[0u8; 100])]), + &into, + ) + .unwrap(); + let metered = Metered::new(fs.clone(), 150); + assert_eq!(metered.read(Utf8Path::new("big")).await.unwrap().len(), 100); + assert!(metered.is_file(Utf8Path::new("big")).await); + assert!(metered.exists(Utf8Path::new("big")).await); + let err = metered.read(Utf8Path::new("big")).await.unwrap_err().to_string(); + assert!(err.contains("loading the project reads more than"), "{err}"); + // A file the bundle does not hold is refused as such, not charged. + let err = metered.read(Utf8Path::new("missing")).await.unwrap_err().to_string(); + assert!(err.contains("not in the bundle"), "{err}"); + assert_eq!(fs.read(Utf8Path::new("big")).await.unwrap().len(), 100); + } + + /// The `ic` environment of the bundle `bytes`, as a deploy would load + /// it: extracted, then read through the host. + async fn environment(bytes: &[u8]) -> (tempfile::TempDir, BundleFs, Environment) { + let (dir, into) = scratch(); + let fs = extract_bundle(bytes, &into).unwrap(); + let env = host_for(fs.clone()) + .get_environment(&EnvironmentSelection::Named("ic".into())) + .await + .unwrap(); + (dir, fs, env) + } + + fn check( + fs: &BundleFs, + env: &Environment, + wasm_in_all: u64, + plugin_input: u64, + ) -> Result<(), String> { + check_bundle_within( + fs, + &env.network, + env.canisters.values().map(|(dir, canister)| (dir.as_path(), canister)), + wasm_in_all, + plugin_input, + ) + } + + // A module is read, hashed and expanded once however many canisters name + // it, by whatever spelling: the sum of the expansions is a sum over the + // archive's files, so a manifest cannot multiply the work by repeating + // itself. + #[tokio::test] + async fn a_module_is_checked_once_however_many_canisters_name_it() { + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + encoder.write_all(&[0u8; 4096]).unwrap(); + let gz = encoder.finish().unwrap(); + let sha = hex::encode(Sha256::digest(&gz)); + let canister = |name: &str, path: &str| { + format!( + " - name: {name}\n build:\n steps:\n - type: pre-built\n \ + path: {path}\n sha256: {sha}\n" + ) + }; + + // Three canisters, one module, two spellings: expanded once. + let manifest = format!( + "canisters:\n{}{}{}", + canister("a", "m.wasm.gz"), + canister("b", "./m.wasm.gz"), + canister("c", "m.wasm.gz") + ); + let bytes = archive(&[("icp.yaml", manifest.as_bytes()), ("m.wasm.gz", &gz)]); + let (_dir, fs, env) = environment(&bytes).await; + check(&fs, &env, 4096, MAX_PLUGIN_INPUT_BYTES).unwrap(); + + // The same bytes at two paths are two modules: expanded twice, and + // the sum holds. + let manifest = + format!("canisters:\n{}{}", canister("a", "m.wasm.gz"), canister("b", "n.wasm.gz")); + let bytes = + archive(&[("icp.yaml", manifest.as_bytes()), ("m.wasm.gz", &gz), ("n.wasm.gz", &gz)]); + let (_dir, fs, env) = environment(&bytes).await; + let err = check(&fs, &env, 4096, MAX_PLUGIN_INPUT_BYTES).unwrap_err(); + assert!(err.contains("canister 'b'") && err.contains("in all"), "{err}"); + check(&fs, &env, 8192, MAX_PLUGIN_INPUT_BYTES).unwrap(); + } + + // What a sync step hands its plugin is held to the bundle at the load, + // by the vet — each entry under the root, through no link, present, of + // the kind declared — and the files the runtime will read are held to a + // sum here, counting a file each time it is declared. + #[tokio::test] + async fn a_sync_step_declares_what_the_bundle_holds() { + let wasm = wat::parse_str("(module)").unwrap(); + let sha = hex::encode(Sha256::digest(&wasm)); + let bundle = |declared: &str| { + let canister = format!( + "name: backend\nbuild:\n steps:\n - type: pre-built\n path: backend.wasm\n \ + sha256: {sha}\nsync:\n steps:\n - type: plugin\n path: plugin.wasm\n{declared}" + ); + archive(&[ + ("icp.yaml", b"canisters:\n - canisters/backend\n"), + ("canisters/backend/canister.yaml", canister.as_bytes()), + ("canisters/backend/backend.wasm", &wasm), + ("canisters/backend/plugin.wasm", &wasm), + ("canisters/backend/config.txt", b"0123456789"), + ("canisters/backend/assets/index.html", b""), + ("shared/common.txt", b"abc"), + ]) + }; + let all = (MAX_WASM_BYTES_IN_ALL, MAX_PLUGIN_INPUT_BYTES); + + let accepted = [ + " files:\n config: config.txt\n seed: assets\n", + " files:\n shared: ../../shared/common.txt\n", + " dirs:\n - assets\n files:\n - config.txt\n", + ]; + for declared in accepted { + let (_dir, fs, env) = environment(&bundle(declared)).await; + check(&fs, &env, all.0, all.1).unwrap_or_else(|e| panic!("{declared}: {e}")); + } + + // A file that is not there, a file where a directory is declared, an + // absolute path, a path climbing out: each fails the load, before + // the check could read anything. + let refused = [ + (" files:\n config: missing.txt\n", "missing.txt"), + (" dirs:\n - config.txt\n", "config.txt"), + (" files:\n etc: /etc/passwd\n", "/etc/passwd"), + (" files:\n up: ../../../x\n", "x"), + ]; + for (declared, named) in refused { + let (_dir, into) = scratch(); + let fs = extract_bundle(&bundle(declared), &into).unwrap(); + let err = chain(&host_for(fs).project.load().await.expect_err(declared)); + assert!( + err.contains("gives its sync plugin") && err.contains(named), + "{declared}: {err}" + ); + } + + // Ten bytes declared twice are twenty; a directory costs nothing. + let declared = + " files:\n a: config.txt\n b: config.txt\n seed: assets\n"; + let (_dir, fs, env) = environment(&bundle(declared)).await; + let err = check(&fs, &env, all.0, 19).unwrap_err(); + assert!(err.contains("`files:` hold more than 19 bytes in all"), "{err}"); + check(&fs, &env, all.0, 20).unwrap(); + } +} diff --git a/crates/imcp2-deploy/src/lib.rs b/crates/imcp2-deploy/src/lib.rs new file mode 100644 index 0000000..0eb4390 --- /dev/null +++ b/crates/imcp2-deploy/src/lib.rs @@ -0,0 +1,19 @@ +//! imcp2-deploy — deploying an icp project bundle to the Internet Computer +//! through the icp CLI's own project library (`icp-project`, the code behind +//! `icp deploy`). +//! +//! This crate is composed into the local server only. It depends on +//! `icp-project` straight from the icp-cli repository, which crates.io does +//! not allow, so it cannot be part of the published `imcp2-core`/`imcp2` +//! crates (see `Cargo.toml`). +//! +//! [`host`] is the `icp_project::Host` for a bundle: the archive `icp project +//! bundle` writes, treated as untrusted input. The rule it is held to is the +//! library's own, `icp_project::untrusted`, the one `icp --untrusted` +//! applies (dfinity/icp-cli#873, on #868 for what `icp project bundle` +//! writes; issue #865): this crate pins icp-cli's main at their merge, +//! composes the rule as the CLI does, and adds the bounds on the archive +//! itself and the checks on the bundle's shape. The deploy pipeline over it +//! and the MCP tools that expose it follow in their own changes. + +pub mod host;