diff --git a/Cargo.lock b/Cargo.lock index c1df85e8a..bef522472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -467,7 +476,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.0.8", + "rustix 1.1.4", "slab", "windows-sys 0.60.2", ] @@ -507,7 +516,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix 1.0.8", + "rustix 1.1.4", ] [[package]] @@ -533,7 +542,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.0.8", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.60.2", @@ -618,6 +627,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.7" @@ -1050,7 +1081,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -1120,6 +1151,24 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -1556,6 +1605,20 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -1771,6 +1834,12 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -2005,7 +2074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -2041,6 +2110,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -2076,14 +2155,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] @@ -2250,101 +2327,1009 @@ dependencies = [ ] [[package]] -name = "futures-sink" -version = "0.3.31" +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasi 0.14.4+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gix" +version = "0.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce52001b946a6249d5d0d3011df0a042ac3f8a4d013460db6476577b0b9c567" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-merge", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-actor" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-archive" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a20ec244b733338d4cb60e5e05eac700dab7fcc689647b1d1daa9396b119342" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe17c5a1c0b6f2ef1476aa1d3222ea50cdff67608016613a58bfc3e078046000" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dab9a942ab54a9661ded7397c3bf927274e7afa94494db0d75cfcbde02ca0a" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-chunk" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86335306511abe43d75c866d4b1f3d90932fe202edcd43e1314036333e7384d8" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3b5aa0f24e19028c261d229aeeedafcaaa52ebd71021cc15184620fc9d32eb" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c01848aebd21c67f6ba41f1de8efd46ae96df21f001954a3c9e1517e514d410" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b39ed39ee4c10a3b157f9fb94bac8098d9f8e56201f0cf7dee6c187416c4b2" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-credentials" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ca11598b70811d7b16ff90945a6e57dfe521e85b744e51636965fe39cc8f60" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-date" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", + "smallvec", +] + +[[package]] +name = "gix-diff" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc08e0fa1a91ff5f24affeab052f198056645e1de004910bde7b82b50ea5982a" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-dir" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a0fc06e9e1e430cbf0a313666976d90f822f461a6525320427aa9b8af5236c" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-discover" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17852e6a501e688a1702b24ebe5b3761d4719455bc869fd29f38b0b859bcad34" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f" +dependencies = [ + "bytes", + "crc32fast", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "prodash", + "thiserror 2.0.18", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac917dbe9653c9b615d248db91907a365bd779750c9e1b457a9d9fdeece3a08" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-fs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1967daac9848757c47c2aef0c57bcadc1a897347f559778249bf286a536c86" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-glob" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bf29249a069bf2507f5964f80997f37b134d320ea348d66527726b9be2c38c" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-hashtable" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b" +dependencies = [ + "gix-hash", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb13fbbeeafee943e52b61fcc88dfddf6a452fcaf0c4d0cdc8f218fa25bbec5" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82" +dependencies = [ + "bstr", + "hashbrown 0.15.5", +] + +[[package]] +name = "gix-index" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c3ef97ad08121e4327a6226bd63fed6b9e3c6b976d48bddd4356d9d41191db" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.16.1", + "itoa", + "libc", + "memmap2", + "rustix 1.1.4", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-lock" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3bc074e5723027b482dcd9ab99d95804a53742f6de812d0172fbba4a186c1" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-merge" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bbcdcc52b70a32f0a151b024dff9d0fcf56ee48f00d9503e735af9d99ea881" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-negotiate" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103d42bfade1b8a96ca5005933127bdad461ce588d92422b2c2daa3ff20d780c" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38075a95d7cc5df8afd38e72c617026c1456952207a4120a7f55a3fbf93b4d7" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-odb" +version = "0.80.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeeda12a9663120418735ecdc1250d06eeab0be75700e47b3402a981331716ba" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "memmap2", + "parking_lot", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pack" +version = "0.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf02e6f5c8f07a069c9ea5245f40d9b14856ada4086091dc99941b49002b4fa" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "memmap2", + "parking_lot", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-packetline" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a6059e8a4c1b7f406e24716499cefa3926e060876fb1959ef225efeee346e" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pathspec" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a84a4f083dd70fb49f4377e13afa6d90df2daaa1c705c49d6ff1331fc7e8855" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-prompt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041a626c64cb69e4117fcdf80da8d0e454fba3b1f420412792d191f52251aee" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix 1.1.4", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-protocol" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4bee82db63ec635996b96efae71cf467c155fa3f34a556184373224a26c4fd" +dependencies = [ + "bstr", + "gix-credentials", + "gix-date", + "gix-features", + "gix-hash", + "gix-lock", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-quote" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba9cc15f558b274c99349b83130f5ec83459660828fde9718bbbb43a726167" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-refspec" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61755b27d57edc8940a1b1593c8c61548ca8e4c02da1ed8d5bfeda9eb2a6b761" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-revision" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb5288fac706d3ea3e4e2ba9ec38b78743b8c02f422e18cb342299cfd6ab7e8" +dependencies = [ + "bitflags", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313813706b073a12ff7f9b2896bf3e6504cdac7cfbc97b1920114724705069f0" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-sec" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3a2d3e504a238136751e646a6c028252286a0ea64ea9974bf0498633407c6" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29187305521bfacf4aefd284ab28dbfa9fb74abd39a5e63dd313b1baa5808c27" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-status" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c6d2a8c521ffa205fe7e268c82e6d1378ba37cd826ca10ab6129fdc29a4b65" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-submodule" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd5fc8692890bd71a596e540fd4c364f8460eaa82c4eaaedebde6e1e3eb4d91" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-tempfile" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" [[package]] -name = "futures-task" -version = "0.3.31" +name = "gix-transport" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "ffd6a5c676b92d4ead5f5a2b2935024415dec69edc997b6090ca9cac010a3018" +dependencies = [ + "base64 0.22.1", + "bstr", + "gix-command", + "gix-credentials", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "reqwest 0.13.3", + "thiserror 2.0.18", +] [[package]] -name = "futures-util" -version = "0.3.31" +name = "gix-traverse" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "a14b7052c0786676c03e71fcfde7d7f0f8e8316e642b5cec6bb3998719b2ce5c" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.18", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gix-url" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "35842d099e813f6f6bba529e88d4670572149c3df79b7a412952259887721ece" dependencies = [ - "typenum", - "version_check", - "zeroize", + "bstr", + "gix-path", + "percent-encoding", + "thiserror 2.0.18", ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "gix-utils" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "bstr", + "fastrand", + "unicode-normalization", ] [[package]] -name = "getrandom" -version = "0.3.3" +name = "gix-validate" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasi 0.14.4+wasi-0.2.4", - "wasm-bindgen", + "bstr", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "gix-worktree" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "d69955eb5e2910832f88d041964b809eee01dadd579237e0b55efec58fd406fd" dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", ] [[package]] -name = "ghash" -version = "0.5.1" +name = "gix-worktree-state" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "8a96dccbcf9e8fe0291c55f06e08da93ebb2e691c1311276f541eefcc6d70800" dependencies = [ - "opaque-debug", - "polyval", + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror 2.0.18", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "gix-worktree-stream" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "9a8444b8ed4662e1a0c97f3eceda29630001a1bbb2632201e50312623e594213" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] [[package]] name = "glob" @@ -2433,6 +3418,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -2445,6 +3436,17 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "heapless" version = "0.8.0" @@ -3003,6 +4005,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -3020,16 +4032,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -3093,34 +4095,51 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", + "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", + "windows-sys 0.60.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -3221,7 +4240,7 @@ dependencies = [ "jsonrpsee-core", "jsonrpsee-types", "rustls", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "serde", "serde_json", "thiserror 2.0.18", @@ -3281,6 +4300,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -3384,9 +4412,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -3431,9 +4459,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3501,12 +4529,32 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-async" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -3608,6 +4656,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -4099,7 +5153,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.8", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -4237,6 +5291,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "parking_lot", +] + [[package]] name = "proptest" version = "1.11.0" @@ -4310,6 +5373,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.3", "lru-slab", @@ -4560,6 +5624,46 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier 0.6.1", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -4702,14 +5806,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys 0.12.1", "windows-sys 0.60.2", ] @@ -4719,6 +5823,7 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4771,6 +5876,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.4.0", + "security-framework-sys", + "webpki-root-certs 1.0.7", + "windows-sys 0.52.0", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -4783,6 +5909,7 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5210,6 +6337,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest 0.10.7", + "sha1", +] + [[package]] name = "sha2" version = "0.9.9" @@ -5403,6 +6540,7 @@ dependencies = [ "fqdn", "futures", "futures-util", + "gix", "glob", "heck 0.5.0", "hex", @@ -5419,7 +6557,7 @@ dependencies = [ "predicates", "rand 0.8.5", "regex", - "reqwest", + "reqwest 0.12.23", "rpassword", "rust-embed", "rustc_version", @@ -5447,6 +6585,7 @@ dependencies = [ "strsim", "strum 0.17.1", "strum_macros 0.17.1", + "tar", "tempfile", "termcolor", "termcolor_output", @@ -5691,13 +6830,15 @@ dependencies = [ "assert_cmd", "assert_fs", "ed25519-dalek", + "flate2", "fs_extra", + "gix", "hex", "home", "httpmock", "markdown", "predicates", - "reqwest", + "reqwest 0.12.23", "sep5", "serde_json", "sha2 0.10.9", @@ -5710,6 +6851,7 @@ dependencies = [ "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.15", + "tar", "test-case", "testcontainers", "thiserror 1.0.69", @@ -5824,7 +6966,7 @@ dependencies = [ "once_cell", "phf", "pretty_assertions", - "reqwest", + "reqwest 0.12.23", "serde", "serde_derive", "serde_json", @@ -6096,6 +7238,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[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 = "temp-dir" version = "0.1.16" @@ -6104,14 +7257,14 @@ checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", - "rustix 1.0.8", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -6607,20 +7760,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -6748,6 +7901,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-id" version = "0.3.5" @@ -7295,7 +8454,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -7347,9 +8506,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" @@ -7427,11 +8586,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -7802,7 +8961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.4", ] [[package]] @@ -8044,6 +9203,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zvariant" version = "4.2.0" diff --git a/Cargo.toml b/Cargo.toml index e43666a0a..daff22952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,9 @@ hex = "0.4.3" itertools = "0.10.0" async-trait = "0.1.76" bollard = "0.20.2" +gix = { version = "0.83.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "worktree-mutation", "sha1"] } +tar = "0.4.46" +flate2 = "1.0.30" serde-aux = "4.1.2" serde_json = "1.0.82" serde = "1.0.82" diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index c55d770ca..7429b70f2 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -97,6 +97,7 @@ Tools for smart contract developers - `optimize` — ⚠️ Deprecated, use `build --optimize`. Optimize a WASM file - `read` — Print the current value of a contract-data ledger entry - `restore` — Restore an evicted value for a contract-data legder entry +- `verify` — Verify that a contract's WASM reproduces from the build metadata it records, per SEP-58. Either pass a contract id/alias via `--id` (the WASM is fetched from the network) or a local file via `--wasm` ## `stellar contract asset` @@ -1104,6 +1105,31 @@ If no keys are specificed the contract itself is restored. - `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided - `--build-only` — Build the transaction and only write the base64 xdr to stdout +## `stellar contract verify` + +Verify that a contract's WASM reproduces from the build metadata it records, per SEP-58. Either pass a contract id/alias via `--id` (the WASM is fetched from the network) or a local file via `--wasm` + +**Usage:** `stellar contract verify [OPTIONS]` + +###### **Global Options:** + +- `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings + +###### **Options:** + +- `--id ` — Contract id or alias to fetch the WASM from the network +- `--wasm ` — Local WASM file to verify, instead of fetching from the network +- `--tarball-url ` — Local tarball file or http(s) URL to use as the source when the WASM's recorded SEP-58 metadata has only `tarball_sha256` (no `tarball_url`). Accepts http(s) URLs or local file paths +- `--trust` — Bypass interactive confirmation when the WASM's bldimg is not in the default trust list, or when the source is a tarball (tarballs are never default-trusted) +- `-d`, `--docker-host ` — Override the default docker host used by the rebuild + +###### **RPC Options:** + +- `--rpc-url ` — RPC server endpoint +- `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times +- `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +- `-n`, `--network ` — Name of network to use from config + ## `stellar doctor` Diagnose and troubleshoot CLI and network issues diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index f89fc7066..d150ea6a9 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -55,6 +55,9 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" httpmock = { workspace = true } reqwest = { workspace = true } +gix = { workspace = true } +tar = { workspace = true } +flate2 = { workspace = true } [features] default = [] diff --git a/cmd/crates/soroban-test/tests/it/integration/contract/mod.rs b/cmd/crates/soroban-test/tests/it/integration/contract/mod.rs index 5e5f4b7a0..a8ba22da2 100644 --- a/cmd/crates/soroban-test/tests/it/integration/contract/mod.rs +++ b/cmd/crates/soroban-test/tests/it/integration/contract/mod.rs @@ -1,2 +1,3 @@ mod fetch; mod info_hash; +mod verify; diff --git a/cmd/crates/soroban-test/tests/it/integration/contract/verify.rs b/cmd/crates/soroban-test/tests/it/integration/contract/verify.rs new file mode 100644 index 000000000..556a7a69d --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/contract/verify.rs @@ -0,0 +1,255 @@ +//! End-to-end tests for `stellar contract verify`. +//! +//! These exercise the full pipeline: build a contract verifiably against a +//! pinned bldimg + pinned source_repo, then verify the resulting wasm matches. +//! The "happy path" tests require docker + network access to GitHub + the +//! pinned bldimg pullable from Docker Hub. They are always-run by convention +//! (per the project's "no #[ignore]" rule) — failures there flag a regression +//! or pinned-resource drift loudly. +//! +//! Fixture pins: +//! - bldimg: `docker.io/fnando/stellar-cli-experimental@sha256:85e76e…`. +//! TODO: swap to `docker.io/stellar/stellar-cli@sha256:<…>` once +//! `stellar/stellar-cli-docker` publishes a canonical tag matching the +//! cli version under test. +//! - source_repo + source_rev: a specific commit on +//! `stellar/soroban-examples`. The `hello_world` contract there is the +//! smallest, most-stable example; we build just that with `--package`. + +use gix::progress::Discard; +use predicates::prelude::{predicate, PredicateBooleanExt}; +use sha2::{Digest, Sha256}; +use soroban_test::TestEnv; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; + +const PINNED_BLDIMG: &str = + "docker.io/fnando/stellar-cli-experimental@sha256:85e76eae8bf9f47ba94391214b76f8fa2b9d7b28171774dfafaf5b8d613a74d3"; +const PINNED_SOURCE_REPO: &str = "github:stellar/soroban-examples"; +const PINNED_SOURCE_REV: &str = "7b168174ae1268dab91a0190d80a94ab7ff41b59"; +/// `soroban-examples` has no root `Cargo.toml` — each example is its own +/// crate in a subdirectory. The cli's source-root resolver anchors the +/// bind-mount + the recorded bldopt to the clone root, so the manifest-path +/// stays portable as `hello_world/Cargo.toml` regardless of where the user +/// invoked from. +const PINNED_MANIFEST_PATH: &str = "hello_world/Cargo.toml"; + +/// Build a verifiable wasm for the pinned hello-world example and write it to +/// `/out/soroban_hello_world_contract.wasm`. Returns the on-disk path. +fn build_verifiable_hello_world(sandbox: &TestEnv) -> PathBuf { + let out_dir = sandbox.dir().join("out"); + std::fs::create_dir_all(&out_dir).unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("build") + .arg("--verifiable") + .arg("--image") + .arg(PINNED_BLDIMG) + .arg("--source-repo") + .arg(PINNED_SOURCE_REPO) + .arg("--source-rev") + .arg(PINNED_SOURCE_REV) + .arg("--manifest-path") + .arg(PINNED_MANIFEST_PATH) + .arg("--out-dir") + .arg(&out_dir) + .current_dir(prepared_source_tree(sandbox)) + .assert() + .success(); + out_dir.join("soroban_hello_world_contract.wasm") +} + +/// Materialize the pinned `stellar/soroban-examples` source tree at `/soroban-examples` +/// so the verifiable build has a workspace_root to bind-mount into the +/// container. The host's source tree is what the bldimg actually compiles; +/// `source_repo` + `source_rev` recorded into the wasm only tell a future +/// verifier where to fetch from. We clone via gix to stay shell-free. +fn prepared_source_tree(sandbox: &TestEnv) -> PathBuf { + let dir = sandbox.dir().join("soroban-examples"); + if dir.exists() { + return dir; + } + // Mirror what the cli's `verify::clone_git_source` does — same gix call + // sequence, same flags — so the test exercises the production code path + // a third-party verifier would hit. + let interrupt = AtomicBool::new(false); + let mut prepare = gix::prepare_clone_bare("https://github.com/stellar/soroban-examples", &dir) + .expect("prepare_clone_bare"); + let (repo, _) = prepare.fetch_only(Discard, &interrupt).expect("fetch_only"); + let oid = gix::ObjectId::from_hex(PINNED_SOURCE_REV.as_bytes()).expect("rev hex"); + let object = repo.find_object(oid).expect("find_object"); + let commit = object.peel_to_commit().expect("peel_to_commit"); + let tree_id = commit.tree_id().expect("tree_id"); + let index = gix::index::State::from_tree( + &tree_id, + &repo.objects, + gix::validate::path::component::Options::default(), + ) + .expect("from_tree"); + let mut index_file = gix::index::File::from_state(index, dir.join(".git").join("index")); + gix::worktree::state::checkout( + &mut index_file, + &dir, + repo.objects.clone().into_arc().expect("into_arc"), + &Discard, + &Discard, + &interrupt, + gix::worktree::state::checkout::Options { + destination_is_initially_empty: true, + overwrite_existing: true, + ..Default::default() + }, + ) + .expect("checkout"); + dir +} + +/// Happy path: build a verifiable wasm, then verify it from the local file. +/// Asserts the cli prints `Verified:` on stdout (or stderr; we accept either +/// via `predicates`). +#[test] +fn verify_wasm_succeeds_for_freshly_built_verifiable_wasm() { + let sandbox = TestEnv::default(); + let wasm = build_verifiable_hello_world(&sandbox); + + sandbox + .new_assert_cmd("contract") + .arg("verify") + .arg("--wasm") + .arg(&wasm) + .arg("--trust") + .assert() + .success() + .stderr(predicate::str::contains("Verified:")); +} + +/// Build verifiable → upload to local network → verify by --id. Exercises +/// the wasm::fetch_from_contract path through the verify command. +#[tokio::test] +async fn verify_id_succeeds_after_upload() { + let sandbox = TestEnv::new(); + let wasm = build_verifiable_hello_world(&sandbox); + let wasm_str = wasm.to_string_lossy().to_string(); + + // Upload (cheaper than full deploy; verify only needs the wasm bytes, which + // upload puts on-ledger under a known hash). `--id` accepts a contract id + // OR an alias OR (via wasm_hash) any thing the network can resolve to wasm. + // The deploy path is what gives us a contract id we can pass to --id. + let id = sandbox + .new_assert_cmd("contract") + .arg("deploy") + .arg("--wasm") + .arg(&wasm_str) + .arg("--alias") + .arg("verify_e2e") + .arg("--ignore-checks") + .assert() + .success() + .stdout(predicate::str::is_empty().not()) + .get_output() + .stdout + .clone(); + let id = String::from_utf8(id).unwrap().trim().to_string(); + + sandbox + .new_assert_cmd("contract") + .arg("verify") + .arg("--id") + .arg(&id) + .arg("--trust") + .assert() + .success() + .stderr(predicate::str::contains("Verified:")); +} + +/// Build verifiable with `--tarball-sha256` (no `tarball_url` recorded), then +/// verify by handing the cli a local file path for `--tarball-url`. Exercises +/// the local-file branch of `fetch_tarball_bytes` and the SHA-256 check in +/// `verify_tarball_sha256`. +#[test] +fn verify_wasm_succeeds_via_tarball_sha256_with_local_url() { + let sandbox = TestEnv::default(); + let source = prepared_source_tree(&sandbox); + + // Pack the source tree into a gzipped tarball next to the sandbox and + // record its sha256 — that becomes the `--tarball-sha256` we hand to + // build, and the same file path we hand to verify as `--tarball-url`. + let tar_path = sandbox.dir().join("source.tar.gz"); + pack_source_tarball(&source, &tar_path); + let sha = format!("{:x}", Sha256::digest(std::fs::read(&tar_path).unwrap())); + + let out_dir = sandbox.dir().join("out"); + std::fs::create_dir_all(&out_dir).unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("build") + .arg("--verifiable") + .arg("--image") + .arg(PINNED_BLDIMG) + .arg("--tarball-sha256") + .arg(&sha) + .arg("--manifest-path") + .arg(PINNED_MANIFEST_PATH) + .arg("--out-dir") + .arg(&out_dir) + .current_dir(&source) + .assert() + .success(); + let wasm = out_dir.join("soroban_hello_world_contract.wasm"); + + sandbox + .new_assert_cmd("contract") + .arg("verify") + .arg("--wasm") + .arg(&wasm) + .arg("--tarball-url") + .arg(&tar_path) + .arg("--trust") + .assert() + .success() + .stderr(predicate::str::contains("Verified:")); +} + +fn pack_source_tarball(source: &Path, out: &Path) { + use flate2::write::GzEncoder; + use flate2::Compression; + + let file = std::fs::File::create(out).expect("create tarball"); + let mut gz = GzEncoder::new(file, Compression::default()); + let mut builder = tar::Builder::new(&mut gz); + builder.follow_symlinks(false); + builder + .append_dir_all(".", source) + .expect("append source tree to tarball"); + builder.finish().expect("finish tarball"); + drop(builder); + gz.finish().expect("finish gzip"); +} + +/// Flip a byte in a verifiable wasm and confirm `contract verify` reports the +/// mismatch (different hashes). +#[test] +fn verify_wasm_fails_on_tampered_bytes() { + let sandbox = TestEnv::default(); + let wasm = build_verifiable_hello_world(&sandbox); + + // Tamper: corrupt a byte somewhere in the middle of the WASM. The custom + // section that holds contractmetav0 is near the end; flipping a code byte + // changes the bytes-under-comparison without invalidating the WASM enough + // to break the cli's metadata parse. + let mut bytes = std::fs::read(&wasm).unwrap(); + let mid = bytes.len() / 2; + bytes[mid] = bytes[mid].wrapping_add(1); + let tampered = sandbox.dir().join("tampered.wasm"); + std::fs::write(&tampered, &bytes).unwrap(); + + sandbox + .new_assert_cmd("contract") + .arg("verify") + .arg("--wasm") + .arg(&tampered) + .arg("--trust") + .assert() + .failure() + .stderr(predicate::str::contains("verification failed")); +} diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index c4baaeedb..5e33a3231 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -111,7 +111,9 @@ bollard = { workspace = true } futures-util = "0.3.30" futures = "0.3.30" home = "0.5.9" -flate2 = "1.0.30" +flate2 = { workspace = true } +tar = { workspace = true } +gix = { workspace = true } bytesize = "1.3.0" humantime = "2.1.0" phf = { version = "0.11.2", features = ["macros"] } diff --git a/cmd/soroban-cli/src/commands/contract/build/verifiable.rs b/cmd/soroban-cli/src/commands/contract/build/verifiable.rs index f9240fdd8..c5f18a365 100644 --- a/cmd/soroban-cli/src/commands/contract/build/verifiable.rs +++ b/cmd/soroban-cli/src/commands/contract/build/verifiable.rs @@ -337,24 +337,24 @@ fn cross_check_source_rev_against_git(workspace_root: &Path, claimed: &str) -> R Ok(()) } -fn bldimg_regex() -> Regex { +pub(crate) fn bldimg_regex() -> Regex { Regex::new(r"^(?:localhost(?::\d+)?|[^\s@/]*[.:][^\s@/]*)/[^\s@]+@sha256:[0-9a-f]{64}$") .unwrap() } -fn source_rev_regex() -> Regex { +pub(crate) fn source_rev_regex() -> Regex { Regex::new(r"^[0-9a-f]{40}$").unwrap() } -fn source_repo_regex() -> Regex { +pub(crate) fn source_repo_regex() -> Regex { Regex::new(r"^(https?://\S+|github:[^/\s]+/[^/\s]+)$").unwrap() } -fn tarball_url_regex() -> Regex { +pub(crate) fn tarball_url_regex() -> Regex { Regex::new(r"^https?://\S+$").unwrap() } -fn tarball_sha256_regex() -> Regex { +pub(crate) fn tarball_sha256_regex() -> Regex { Regex::new(r"^[0-9a-f]{64}$").unwrap() } @@ -455,7 +455,7 @@ fn build_metadata_args(image_ref: &str, ids: &SourceIds, bldopts: &[String]) -> out } -fn compose_container_args(forwarded: &[String], metadata: &[String]) -> Vec { +pub(crate) fn compose_container_args(forwarded: &[String], metadata: &[String]) -> Vec { let mut args = vec!["contract".to_string(), "build".to_string()]; args.extend_from_slice(forwarded); args.extend_from_slice(metadata); @@ -511,7 +511,7 @@ pub async fn resolve_image(cmd: &Cmd, docker: &Docker, print: &Print) -> Result< Ok(digest) } -async fn pull_image( +pub(crate) async fn pull_image( docker: &Docker, tag: &str, print: &Print, @@ -705,7 +705,7 @@ async fn probe_cli_version(image_ref: &str, docker: &Docker) -> Result read.run().await?, Cmd::Restore(restore) => restore.run(global_args).await?, Cmd::SpecVerify(spec_verify) => spec_verify.run(global_args)?, + Cmd::Verify(verify) => verify.run(global_args).await?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/verify.rs b/cmd/soroban-cli/src/commands/contract/verify.rs new file mode 100644 index 000000000..b8a21e73e --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/verify.rs @@ -0,0 +1,1335 @@ +use std::io::{IsTerminal, Write}; +use std::path::{Path, PathBuf}; + +use clap::Parser; +use regex::Regex; +use sha2::{Digest, Sha256}; +use soroban_spec_tools::contract::Spec; +use stellar_xdr::curr::{ScMetaEntry, ScMetaV0}; + +use crate::{ + commands::{ + container, + contract::build::verifiable::{ + self, bldimg_regex, source_repo_regex, source_rev_regex, tarball_sha256_regex, + tarball_url_regex, + }, + global, + }, + config::{self, locator, network}, + print::Print, + wasm, +}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + /// Contract id or alias to fetch the WASM from the network. + #[arg(long = "id", env = "STELLAR_CONTRACT_ID", conflicts_with = "wasm")] + pub contract_id: Option, + + /// Local WASM file to verify, instead of fetching from the network. + #[arg(long)] + pub wasm: Option, + + /// Local tarball file or http(s) URL to use as the source when the WASM's + /// recorded SEP-58 metadata has only `tarball_sha256` (no `tarball_url`). + /// Accepts http(s) URLs or local file paths. + #[arg(long)] + pub tarball_url: Option, + + /// Bypass interactive confirmation when the WASM's bldimg is not in the + /// default trust list, or when the source is a tarball (tarballs are + /// never default-trusted). + #[arg(long)] + pub trust: bool, + + /// Override the default docker host used by the rebuild. + #[arg(short = 'd', long, env = "DOCKER_HOST")] + pub docker_host: Option, + + #[command(flatten)] + pub locator: locator::Args, + + #[command(flatten)] + pub network: network::Args, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("must pass exactly one of --id or --wasm")] + MissingInput, + + #[error("reading wasm {0}: {1}")] + ReadWasm(PathBuf, std::io::Error), + + #[error(transparent)] + Network(#[from] network::Error), + + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Wasm(#[from] wasm::Error), + + #[error(transparent)] + SpecTools(#[from] soroban_spec_tools::contract::Error), + + #[error("the WASM has no contractmetav0 custom section")] + NoMeta, + + #[error("the WASM's contractmetav0 does not record a `bldimg` entry; cannot verify")] + MissingBldimg, + + #[error("the WASM's contractmetav0 does not record any SEP-58 source-identification entry (source_repo+source_rev, tarball_url, or tarball_sha256); cannot verify")] + MissingSourceId, + + #[error( + "the WASM's `{field}` value {value:?} does not match the SEP-58 format regex `{regex}`" + )] + MetaFormat { + field: &'static str, + value: String, + regex: &'static str, + }, + + #[error("the WASM records `source_rev` but not `source_repo`; SEP-58 requires both together")] + SourceRevWithoutRepo, + + #[error("{kind} {value:?} is not in the default trust list, and stdin is not a terminal so we can't ask. Re-run with --trust to proceed.")] + TrustRequired { kind: TrustKind, value: String }, + + #[error("user declined to trust the {kind}; aborting")] + TrustDeclined { kind: TrustKind }, + + #[error("reading stdin: {0}")] + Stdin(std::io::Error), + + #[error("the WASM records only `tarball_sha256` (no `tarball_url`). Pass `--tarball-url URL_OR_PATH` to provide retrieval.")] + TarballUrlRequired, + + #[error("downloading {url}: {source}")] + TarballDownload { url: String, source: reqwest::Error }, + + #[error("reading local tarball {path}: {source}")] + TarballRead { + path: PathBuf, + source: std::io::Error, + }, + + #[error("tarball sha256 mismatch: expected {expected}, got {actual}")] + TarballHashMismatch { expected: String, actual: String }, + + #[error("extracting tarball into {path}: {source}")] + TarballExtract { + path: PathBuf, + source: std::io::Error, + }, + + #[error("cloning {repo} failed: {detail}")] + GixClone { repo: String, detail: String }, + + #[error("checking out {rev} failed: {detail}")] + GixCheckout { rev: String, detail: String }, + + #[error("creating tempdir: {0}")] + TempDir(std::io::Error), + + #[error("hardening permissions on {path}: {source}")] + ChmodMaterialized { + path: PathBuf, + source: std::io::Error, + }, + + #[error(transparent)] + Verifiable(#[from] verifiable::Error), + + #[error(transparent)] + Bollard(#[from] bollard::errors::Error), + + #[error(transparent)] + DockerConnection(#[from] container::shared::Error), + + #[error("could not find a rebuilt WASM under {target}")] + NoRebuiltWasm { target: PathBuf }, + + #[error("multiple rebuilt WASMs under {target}; pass --package=... in the bldopt entries to disambiguate. Found: {found}")] + AmbiguousRebuiltWasm { target: PathBuf, found: String }, + + #[error("reading rebuilt wasm {path}: {source}")] + ReadRebuilt { + path: PathBuf, + source: std::io::Error, + }, + + #[error("verification failed: rebuilt bytes do not match the original.\n original: {original_size} bytes, sha256={original_hash}\n rebuilt: {rebuilt_size} bytes, sha256={rebuilt_hash}")] + VerificationMismatch { + original_hash: String, + original_size: usize, + rebuilt_hash: String, + rebuilt_size: usize, + }, +} + +/// What kind of source is being trust-checked. Affects the default-trust +/// decision and shapes the prompt + error wording. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TrustKind { + Bldimg, + Tarball, +} + +impl std::fmt::Display for TrustKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TrustKind::Bldimg => write!(f, "bldimg"), + TrustKind::Tarball => write!(f, "tarball"), + } + } +} + +/// Resolution of a single trust check before any I/O happens. Pure function of +/// the input — the run() side decides what to do with each variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TrustDecision { + /// The value matches the default trust list for its kind. Proceed silently. + Trusted, + /// The value is not trusted by default, but `--trust` was passed. Proceed + /// (and the caller may want to log). + Overridden, + /// Not trusted; the caller must prompt (TTY) or fail (non-TTY). + NeedsConfirmation, +} + +/// SEP-58 places no defaults on which images are trustworthy; we hardcode the +/// canonical `docker.io/stellar/stellar-cli` repo (digest-pinned) as the only +/// default-trusted image. Any other image — including mirrors and forks — +/// requires explicit confirmation. +const TRUSTED_BLDIMG_REGEX_STR: &str = r"^docker\.io/stellar/stellar-cli@sha256:[0-9a-f]{64}$"; + +fn trusted_bldimg_regex() -> Regex { + Regex::new(TRUSTED_BLDIMG_REGEX_STR).unwrap() +} + +/// Pure trust decision; no I/O. Tarball sources are never default-trusted. +pub fn trust_decision(value: &str, kind: TrustKind, trust_flag: bool) -> TrustDecision { + let default_trusted = match kind { + TrustKind::Bldimg => trusted_bldimg_regex().is_match(value), + TrustKind::Tarball => false, + }; + if default_trusted { + TrustDecision::Trusted + } else if trust_flag { + TrustDecision::Overridden + } else { + TrustDecision::NeedsConfirmation + } +} + +/// SEP-58 metadata extracted from a contract's `contractmetav0` section. +/// +/// `cliver` is intentionally not captured: the rebuild container re-injects it, +/// so verify's job is to ensure the rebuild's cliver matches the original's +/// (which it will when `bldimg` resolves to the same container). +#[derive(Debug, Clone)] +pub struct ExtractedMetadata { + pub bldimg: String, + pub source_repo: Option, + pub source_rev: Option, + pub tarball_url: Option, + pub tarball_sha256: Option, + pub bldopts: Vec, +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + + let wasm_bytes = self.fetch_wasm().await?; + let meta = extract_metadata(&wasm_bytes)?; + + print.infoln(format!("Build image: {}", meta.bldimg)); + if let Some(v) = &meta.source_repo { + print.infoln(format!("Source repo: {v}")); + } + if let Some(v) = &meta.source_rev { + print.infoln(format!("Source rev: {v}")); + } + if let Some(v) = &meta.tarball_url { + print.infoln(format!("Tarball URL: {v}")); + } + if let Some(v) = &meta.tarball_sha256 { + print.infoln(format!("Tarball SHA-256: {v}")); + } + if !meta.bldopts.is_empty() { + print.infoln(format!("Build options ({}):", meta.bldopts.len())); + for o in &meta.bldopts { + print.blankln(format!(" • {o}")); + } + } + + // Catch the no-retrieval-channel case before any trust prompts so a + // doomed run errors immediately instead of asking the user to trust + // an image we won't end up using. + let has_git_source = meta.source_repo.is_some() && meta.source_rev.is_some(); + let has_tarball_url = self.tarball_url.is_some() || meta.tarball_url.is_some(); + if !has_git_source && !has_tarball_url { + return Err(Error::TarballUrlRequired); + } + + // bldimg trust check is always required. + require_trust(self.trust, TrustKind::Bldimg, &meta.bldimg, &print)?; + + // Tarball source: trust the URL we will actually fetch from (either the + // value the WASM recorded, or the user's `--tarball-url` override). + if let Some(url) = self.effective_tarball_url(&meta) { + require_trust(self.trust, TrustKind::Tarball, &url, &print)?; + } + + // Materialize the recorded source into a tempdir so the rebuild can + // bind-mount it. TempDir lives across the rebuild + comparison and + // cleans up on drop. + let workdir = tempfile::TempDir::new().map_err(Error::TempDir)?; + materialize_source(&meta, self.tarball_url.as_deref(), workdir.path(), &print).await?; + print.checkln(format!( + "Source materialized at {}", + workdir.path().display() + )); + + // Rebuild in the recorded bldimg. + let docker_args = container::shared::Args { + docker_host: self.docker_host.clone(), + }; + let docker = docker_args.connect_to_docker(&print).await?; + verifiable::pull_image(&docker, &meta.bldimg, &print).await?; + let container_cmd = build_container_command(&meta); + verifiable::run_in_container( + &meta.bldimg, + workdir.path(), + &container_cmd, + &docker, + &print, + global_args.verbose || global_args.very_verbose, + ) + .await?; + + // Locate the rebuilt WASM. The cargo target dir lives under the bind- + // mounted /source, which we mapped to `workdir`. + let rebuilt_path = find_rebuilt_wasm(workdir.path(), &meta)?; + let rebuilt = std::fs::read(&rebuilt_path).map_err(|e| Error::ReadRebuilt { + path: rebuilt_path.clone(), + source: e, + })?; + + // Compare. The final result is always shown, even under `--quiet`, + // via a dedicated Print that ignores the quiet flag. + let result_print = Print::new(false); + let original_hash = format!("{:x}", Sha256::digest(&wasm_bytes)); + let rebuilt_hash = format!("{:x}", Sha256::digest(&rebuilt)); + if original_hash == rebuilt_hash && wasm_bytes.len() == rebuilt.len() { + result_print.checkln(format!( + "Verified: {} bytes, sha256={original_hash}", + wasm_bytes.len() + )); + Ok(()) + } else { + Err(Error::VerificationMismatch { + original_hash, + original_size: wasm_bytes.len(), + rebuilt_hash, + rebuilt_size: rebuilt.len(), + }) + } + } + + /// The tarball URL we'll actually retrieve from: the cli override if set, + /// otherwise the value recorded in the WASM. Returns `None` for git-source + /// builds (which aren't trust-checked here). + fn effective_tarball_url(&self, meta: &ExtractedMetadata) -> Option { + self.tarball_url + .clone() + .or_else(|| meta.tarball_url.clone()) + } + + async fn fetch_wasm(&self) -> Result, Error> { + match (&self.contract_id, &self.wasm) { + (Some(id), None) => { + let network = self.network.get(&self.locator)?; + let resolved = + id.resolve_contract_id(&self.locator, &network.network_passphrase)?; + Ok(wasm::fetch_from_contract(&resolved, &network).await?) + } + (None, Some(path)) => std::fs::read(path).map_err(|e| Error::ReadWasm(path.clone(), e)), + _ => Err(Error::MissingInput), + } + } +} + +/// Walk the WASM's `contractmetav0` entries and pull out the SEP-58 fields we +/// need to drive a rebuild. Errors when `bldimg` is absent or when no source +/// identification is recorded, since neither has a sensible default. +pub fn extract_metadata(wasm: &[u8]) -> Result { + let spec = Spec::new(wasm)?; + if spec.meta.is_empty() { + return Err(Error::NoMeta); + } + + let mut bldimg: Option = None; + let mut source_repo: Option = None; + let mut source_rev: Option = None; + let mut tarball_url: Option = None; + let mut tarball_sha256: Option = None; + let mut bldopts: Vec = Vec::new(); + + for entry in &spec.meta { + let ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) = entry; + let k = key.to_string(); + let v = val.to_string(); + match k.as_str() { + "bldimg" => bldimg = Some(v), + "source_repo" => source_repo = Some(v), + "source_rev" => source_rev = Some(v), + "tarball_url" => tarball_url = Some(v), + "tarball_sha256" => tarball_sha256 = Some(v), + "bldopt" => bldopts.push(v), + _ => {} // cliver and any user --meta are intentionally ignored + } + } + + let bldimg = bldimg.ok_or(Error::MissingBldimg)?; + if !bldimg_regex().is_match(&bldimg) { + return Err(Error::MetaFormat { + field: "bldimg", + value: bldimg, + regex: BLDIMG_REGEX_STR, + }); + } + + if let Some(v) = &source_rev { + if !source_rev_regex().is_match(v) { + return Err(Error::MetaFormat { + field: "source_rev", + value: v.clone(), + regex: SOURCE_REV_REGEX_STR, + }); + } + } + if let Some(v) = &source_repo { + if !source_repo_regex().is_match(v) { + return Err(Error::MetaFormat { + field: "source_repo", + value: v.clone(), + regex: SOURCE_REPO_REGEX_STR, + }); + } + } + if let Some(v) = &tarball_url { + if !tarball_url_regex().is_match(v) { + return Err(Error::MetaFormat { + field: "tarball_url", + value: v.clone(), + regex: TARBALL_URL_REGEX_STR, + }); + } + } + if let Some(v) = &tarball_sha256 { + if !tarball_sha256_regex().is_match(v) { + return Err(Error::MetaFormat { + field: "tarball_sha256", + value: v.clone(), + regex: TARBALL_SHA256_REGEX_STR, + }); + } + } + + // SEP-58 lists `source_repo+source_rev` as a conformant combination. We + // refuse `source_rev` without `source_repo` here so the user sees a + // pointed error rather than a downstream "can't clone repo" surprise. + if source_rev.is_some() && source_repo.is_none() { + return Err(Error::SourceRevWithoutRepo); + } + + if source_repo.is_none() + && source_rev.is_none() + && tarball_url.is_none() + && tarball_sha256.is_none() + { + return Err(Error::MissingSourceId); + } + + Ok(ExtractedMetadata { + bldimg, + source_repo, + source_rev, + tarball_url, + tarball_sha256, + bldopts, + }) +} + +/// Apply the trust decision: silent-OK, log-and-OK on override, or +/// prompt-vs-fail on `NeedsConfirmation` depending on whether stdin is a TTY. +fn require_trust( + trust_flag: bool, + kind: TrustKind, + value: &str, + print: &Print, +) -> Result<(), Error> { + match trust_decision(value, kind, trust_flag) { + TrustDecision::Trusted => Ok(()), + TrustDecision::Overridden => { + print.warnln(format!( + "Trusting {kind} {value} because --trust was passed" + )); + Ok(()) + } + TrustDecision::NeedsConfirmation => { + if !std::io::stdin().is_terminal() { + return Err(Error::TrustRequired { + kind, + value: value.to_string(), + }); + } + confirm_interactively(kind, value) + } + } +} + +fn confirm_interactively(kind: TrustKind, value: &str) -> Result<(), Error> { + // Trust prompts must be visible even under `--quiet` so the user can see + // what they're agreeing to. Use a dedicated Print that ignores the flag. + let print = Print::new(false); + let context = match kind { + TrustKind::Bldimg => format!( + "Image {value} is not in the default trust list (only docker.io/stellar/stellar-cli is trusted by default)." + ), + TrustKind::Tarball => format!( + "Tarball source {value} is not trusted by default. Tarballs always require confirmation." + ), + }; + print.warnln(context); + print.question(format!("Trust this {kind} and continue? [y/N] ")); + std::io::stderr().flush().ok(); + let mut line = String::new(); + std::io::stdin() + .read_line(&mut line) + .map_err(Error::Stdin)?; + if parse_yes(&line) { + Ok(()) + } else { + Err(Error::TrustDeclined { kind }) + } +} + +/// Accepts y / Y / yes / YES / Yes (case-insensitive). Anything else, including +/// the empty string, is "no" — trust prompts default to declined. +pub fn parse_yes(answer: &str) -> bool { + let a = answer.trim(); + a.eq_ignore_ascii_case("y") || a.eq_ignore_ascii_case("yes") +} + +/// Materialize the recorded source tree into `target`. Picks the path based on +/// what the WASM recorded: +/// - source_repo + source_rev → `git clone` + `git checkout` +/// - tarball_url (with optional sha256) → download/read, optional sha-check, +/// extract via `tar` +/// - tarball_sha256 only → require `--tarball-url` on the cli and use it as +/// the retrieval channel +/// +/// `tarball_url_override` is the cli's `--tarball-url` flag value; when set, it +/// wins over whatever the WASM recorded, and may be an http(s) URL or a local +/// file path. +async fn materialize_source( + meta: &ExtractedMetadata, + tarball_url_override: Option<&str>, + target: &Path, + print: &Print, +) -> Result<(), Error> { + if let (Some(repo), Some(rev)) = (&meta.source_repo, &meta.source_rev) { + print.infoln(format!("Cloning {repo} at {rev}")); + clone_git_source(repo, rev, target)?; + } else { + let tarball_source = tarball_url_override + .map(str::to_string) + .or_else(|| meta.tarball_url.clone()); + let Some(source) = tarball_source else { + // No tarball_url anywhere and no git pair — only tarball_sha256 is set. + return Err(Error::TarballUrlRequired); + }; + + print.infoln(format!("Fetching tarball from {source}")); + let bytes = fetch_tarball_bytes(&source).await?; + if let Some(expected) = &meta.tarball_sha256 { + verify_tarball_sha256(&bytes, expected)?; + print.checkln("Tarball SHA-256 matches"); + } + extract_tarball(&bytes, target)?; + } + + // Tighten the freshly materialized tree to 0o700 / 0o600 before docker + // sees it. Uses the same per-path helper the cli already applies to its + // config dirs (one source of truth for what "hardened" means). + crate::config::locator::enforce_hardened_tree(target).map_err(|e| { + Error::ChmodMaterialized { + path: target.to_path_buf(), + source: e, + } + })?; + Ok(()) +} + +fn clone_git_source(repo: &str, rev: &str, target: &Path) -> Result<(), Error> { + // SEP-58 allows `github:user/repo` as a shorthand source_repo value, but + // gix expects an HTTPS URL. Expand before cloning. + let clone_url = expand_source_repo(repo); + + let clone_err = |e: &dyn std::fmt::Display| Error::GixClone { + repo: clone_url.clone(), + detail: e.to_string(), + }; + let checkout_err = |e: &dyn std::fmt::Display| Error::GixCheckout { + rev: rev.to_string(), + detail: e.to_string(), + }; + + // Stage 1: fetch the full set of refs into a fresh bare repo at `target`. + // We don't ask gix to materialize a worktree at HEAD because we overwrite + // it with the recorded source_rev right after. + let interrupt = std::sync::atomic::AtomicBool::new(false); + let mut prepare = + gix::prepare_clone_bare(clone_url.as_str(), target).map_err(|e| clone_err(&e))?; + let (gix_repo, _) = prepare + .fetch_only(gix::progress::Discard, &interrupt) + .map_err(|e| clone_err(&e))?; + + // Stage 2: resolve the recorded commit by SHA, build an in-memory index + // from its tree, and write that index out to the target directory. This + // is gix's equivalent of `git checkout ` against an empty worktree. + let oid = gix::ObjectId::from_hex(rev.as_bytes()).map_err(|e| checkout_err(&e))?; + let object = gix_repo.find_object(oid).map_err(|e| checkout_err(&e))?; + let commit = object.peel_to_commit().map_err(|e| checkout_err(&e))?; + let tree_id = commit.tree_id().map_err(|e| checkout_err(&e))?; + + let index = gix::index::State::from_tree( + &tree_id, + &gix_repo.objects, + gix::validate::path::component::Options::default(), + ) + .map_err(|e| checkout_err(&e))?; + let mut index_file = gix::index::File::from_state(index, target.join(".git").join("index")); + + let opts = gix::worktree::state::checkout::Options { + destination_is_initially_empty: true, + overwrite_existing: true, + ..Default::default() + }; + + gix::worktree::state::checkout( + &mut index_file, + target, + gix_repo + .objects + .clone() + .into_arc() + .map_err(|e| checkout_err(&e))?, + &gix::progress::Discard, + &gix::progress::Discard, + &interrupt, + opts, + ) + .map_err(|e| checkout_err(&e))?; + + Ok(()) +} + +fn github_shorthand_regex() -> Regex { + Regex::new(r"^github:([^/\s]+)/([^/\s]+)$").unwrap() +} + +/// Expand SEP-58's `github:user/repo` shorthand to an HTTPS URL git can +/// clone. Other values are returned verbatim. +fn expand_source_repo(repo: &str) -> String { + if let Some(caps) = github_shorthand_regex().captures(repo) { + format!("https://github.com/{}/{}", &caps[1], &caps[2]) + } else { + repo.to_string() + } +} + +/// Retrieve the tarball bytes. `source` is either an `http(s)://` URL or a +/// local file path. The split is by prefix, not by attempting both — keeps +/// behavior predictable. +async fn fetch_tarball_bytes(source: &str) -> Result, Error> { + if source.starts_with("http://") || source.starts_with("https://") { + let resp = reqwest::get(source) + .await + .map_err(|e| Error::TarballDownload { + url: source.to_string(), + source: e, + })?; + let bytes = resp + .error_for_status() + .map_err(|e| Error::TarballDownload { + url: source.to_string(), + source: e, + })? + .bytes() + .await + .map_err(|e| Error::TarballDownload { + url: source.to_string(), + source: e, + })?; + Ok(bytes.to_vec()) + } else { + std::fs::read(source).map_err(|e| Error::TarballRead { + path: PathBuf::from(source), + source: e, + }) + } +} + +fn verify_tarball_sha256(bytes: &[u8], expected: &str) -> Result<(), Error> { + let actual = format!("{:x}", Sha256::digest(bytes)); + if actual.eq_ignore_ascii_case(expected) { + Ok(()) + } else { + Err(Error::TarballHashMismatch { + expected: expected.to_string(), + actual, + }) + } +} + +fn extract_tarball(bytes: &[u8], target: &Path) -> Result<(), Error> { + let gz = flate2::read::GzDecoder::new(bytes); + let mut archive = tar::Archive::new(gz); + archive.unpack(target).map_err(|e| Error::TarballExtract { + path: target.to_path_buf(), + source: e, + }) +} + +/// Compose the argv we hand to the container's `stellar contract build` so +/// that: +/// - the bldopts from the original build become flags (each entry is one +/// token, ready for clap), AND +/// - bldimg / source-ids / bldopt are re-recorded as `--meta` entries so +/// the rebuilt WASM has identical metadata to the original. +/// +/// cliver is intentionally not re-injected — the container's stellar adds it +/// automatically, and it will match the original's iff `bldimg` resolves to +/// the same container. +fn build_container_command(meta: &ExtractedMetadata) -> Vec { + let mut forwarded: Vec = meta.bldopts.clone(); + let mut metadata: Vec = Vec::new(); + + let mut push_meta = |k: &str, v: &str| { + metadata.push("--meta".to_string()); + metadata.push(format!("{k}={v}")); + }; + push_meta("bldimg", &meta.bldimg); + if let Some(v) = &meta.source_repo { + push_meta("source_repo", v); + } + if let Some(v) = &meta.source_rev { + push_meta("source_rev", v); + } + if let Some(v) = &meta.tarball_url { + push_meta("tarball_url", v); + } + if let Some(v) = &meta.tarball_sha256 { + push_meta("tarball_sha256", v); + } + for o in &meta.bldopts { + push_meta("bldopt", o); + } + + // `--locked` is always sent — even if the original somehow lacked it (a + // non-conformant build), the verifier insists on a locked rebuild so + // dependency drift can't move bytes underneath us. + if !forwarded.iter().any(|a| a == "--locked") { + forwarded.insert(0, "--locked".to_string()); + } + + verifiable::compose_container_args(&forwarded, &metadata) +} + +/// Locate the rebuilt WASM under `workdir`. The container writes to +/// `/target/wasm32v1-none/release/.wasm` (or `wasm32-unknown-unknown/release` +/// for older toolchains; check both). If a `--package=` bldopt was +/// recorded, prefer that file. +fn find_rebuilt_wasm(workdir: &Path, meta: &ExtractedMetadata) -> Result { + let preferred_pkg = meta + .bldopts + .iter() + .find_map(|opt| opt.strip_prefix("--package=").map(|s| s.replace('-', "_"))); + + // Cargo's `target/` lives next to the manifest's workspace root, which + // may be a subdirectory of `workdir` when `--manifest-path=…` was + // recorded (e.g. `hello_world/Cargo.toml` in a multi-crate repo). Anchor + // the search at the manifest's parent dir, falling back to `workdir`. + let target_base = meta + .bldopts + .iter() + .find_map(|opt| { + opt.strip_prefix("--manifest-path=") + .and_then(|p| Path::new(p).parent().map(Path::to_path_buf)) + .filter(|p| !p.as_os_str().is_empty()) + }) + .map_or_else(|| workdir.to_path_buf(), |sub| workdir.join(sub)); + + let candidates = [ + target_base.join("target/wasm32v1-none/release"), + target_base.join("target/wasm32-unknown-unknown/release"), + ]; + + let mut found: Vec = Vec::new(); + for dir in &candidates { + if !dir.is_dir() { + continue; + } + for entry in std::fs::read_dir(dir).map_err(|e| Error::ReadRebuilt { + path: dir.clone(), + source: e, + })? { + let p = entry + .map_err(|e| Error::ReadRebuilt { + path: dir.clone(), + source: e, + })? + .path(); + if p.extension().and_then(|s| s.to_str()) == Some("wasm") { + found.push(p); + } + } + } + + if let Some(pkg) = &preferred_pkg { + let want = format!("{pkg}.wasm"); + if let Some(p) = found.iter().find(|p| { + p.file_name() + .and_then(|s| s.to_str()) + .is_some_and(|n| n == want) + }) { + return Ok(p.clone()); + } + } + + match found.len() { + 0 => Err(Error::NoRebuiltWasm { + target: target_base.join("target"), + }), + 1 => Ok(found.into_iter().next().unwrap()), + _ => Err(Error::AmbiguousRebuiltWasm { + target: target_base.join("target"), + found: found + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", "), + }), + } +} + +// These mirror the regex strings used in verifiable.rs. They're kept here only +// so `Error::MetaFormat` can render the regex back to the user as part of the +// error message. The actual matching uses the helpers from verifiable.rs. +const BLDIMG_REGEX_STR: &str = + r"^(?:localhost(?::\d+)?|[^\s@/]*[.:][^\s@/]*)/[^\s@]+@sha256:[0-9a-f]{64}$"; +const SOURCE_REV_REGEX_STR: &str = r"^[0-9a-f]{40}$"; +const SOURCE_REPO_REGEX_STR: &str = r"^(https?://\S+|github:[^/\s]+/[^/\s]+)$"; +const TARBALL_URL_REGEX_STR: &str = r"^https?://\S+$"; +const TARBALL_SHA256_REGEX_STR: &str = r"^[0-9a-f]{64}$"; + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + use stellar_xdr::curr::{Limited, Limits, ScMetaEntry, ScMetaV0, WriteXdr}; + + fn make_wasm_with_meta(entries: &[(&str, &str)]) -> Vec { + let xdr = encode_meta(entries); + let mut wasm = empty_wasm_module(); + wasm_gen::write_custom_section(&mut wasm, "contractmetav0", &xdr); + wasm + } + + fn empty_wasm_module() -> Vec { + // Minimal valid WASM: magic + version, no sections. + vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00] + } + + fn encode_meta(entries: &[(&str, &str)]) -> Vec { + let mut buf = Vec::new(); + let mut writer = Limited::new(Cursor::new(&mut buf), Limits::none()); + for (k, v) in entries { + ScMetaEntry::ScMetaV0(ScMetaV0 { + key: (*k).to_string().try_into().unwrap(), + val: (*v).to_string().try_into().unwrap(), + }) + .write_xdr(&mut writer) + .unwrap(); + } + buf + } + + fn good_bldimg() -> String { + format!("docker.io/stellar/stellar-cli@sha256:{}", "a".repeat(64)) + } + + #[test] + fn extract_metadata_happy_path_git_source() { + let wasm = make_wasm_with_meta(&[ + ("bldimg", &good_bldimg()), + ("source_repo", "https://github.com/foo/bar"), + ("source_rev", &"b".repeat(40)), + ("bldopt", "--locked"), + ("bldopt", "--meta=home_domain=fnando.com"), + ("home_domain", "fnando.com"), + ("cliver", "26.0.0#abcdef"), + ]); + let meta = extract_metadata(&wasm).unwrap(); + assert_eq!(meta.bldimg, good_bldimg()); + assert_eq!( + meta.source_repo.as_deref(), + Some("https://github.com/foo/bar") + ); + assert_eq!(meta.source_rev.as_deref(), Some("b".repeat(40).as_str())); + assert_eq!( + meta.bldopts, + vec![ + "--locked".to_string(), + "--meta=home_domain=fnando.com".to_string() + ] + ); + assert!(meta.tarball_url.is_none()); + assert!(meta.tarball_sha256.is_none()); + } + + #[test] + fn extract_metadata_happy_path_tarball_pair() { + let wasm = make_wasm_with_meta(&[ + ("bldimg", &good_bldimg()), + ("tarball_url", "https://example.com/src.tar.gz"), + ("tarball_sha256", &"f".repeat(64)), + ("bldopt", "--locked"), + ]); + let meta = extract_metadata(&wasm).unwrap(); + assert_eq!( + meta.tarball_url.as_deref(), + Some("https://example.com/src.tar.gz") + ); + assert_eq!( + meta.tarball_sha256.as_deref(), + Some("f".repeat(64).as_str()) + ); + assert!(meta.source_repo.is_none()); + assert!(meta.source_rev.is_none()); + } + + #[test] + fn extract_metadata_missing_bldimg_errors() { + let wasm = make_wasm_with_meta(&[ + ("source_repo", "https://github.com/foo/bar"), + ("source_rev", &"b".repeat(40)), + ]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!(err, Error::MissingBldimg)); + } + + #[test] + fn extract_metadata_missing_source_id_errors() { + let wasm = make_wasm_with_meta(&[("bldimg", &good_bldimg())]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!(err, Error::MissingSourceId)); + } + + #[test] + fn extract_metadata_source_rev_without_repo_errors() { + let wasm = + make_wasm_with_meta(&[("bldimg", &good_bldimg()), ("source_rev", &"b".repeat(40))]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!(err, Error::SourceRevWithoutRepo)); + } + + #[test] + fn extract_metadata_bad_bldimg_format_errors() { + let wasm = make_wasm_with_meta(&[ + ("bldimg", "stellar/stellar-cli@sha256:abc"), // implicit hub + short + ("source_repo", "https://github.com/foo/bar"), + ("source_rev", &"b".repeat(40)), + ]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!( + err, + Error::MetaFormat { + field: "bldimg", + .. + } + )); + } + + #[test] + fn extract_metadata_bad_source_rev_format_errors() { + let wasm = make_wasm_with_meta(&[ + ("bldimg", &good_bldimg()), + ("source_repo", "https://github.com/foo/bar"), + ("source_rev", "not-a-sha"), + ]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!( + err, + Error::MetaFormat { + field: "source_rev", + .. + } + )); + } + + #[test] + fn extract_metadata_bad_tarball_sha256_format_errors() { + let wasm = make_wasm_with_meta(&[("bldimg", &good_bldimg()), ("tarball_sha256", "abc")]); + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!( + err, + Error::MetaFormat { + field: "tarball_sha256", + .. + } + )); + } + + #[test] + fn extract_metadata_ignores_cliver_and_user_meta() { + let wasm = make_wasm_with_meta(&[ + ("bldimg", &good_bldimg()), + ("source_repo", "https://github.com/foo/bar"), + ("source_rev", &"b".repeat(40)), + ("cliver", "26.0.0#abcdef"), + ("home_domain", "fnando.com"), + ("author", "alice"), + ]); + let meta = extract_metadata(&wasm).unwrap(); + // cliver and user meta land in neither bldopts nor source-ids. + assert!(meta.bldopts.is_empty()); + } + + #[test] + fn extract_metadata_empty_meta_errors() { + let wasm = empty_wasm_module(); // no contractmetav0 section + let err = extract_metadata(&wasm).unwrap_err(); + assert!(matches!(err, Error::NoMeta)); + } + + #[test] + fn trust_decision_bldimg_canonical_is_trusted() { + let img = format!("docker.io/stellar/stellar-cli@sha256:{}", "a".repeat(64)); + assert_eq!( + trust_decision(&img, TrustKind::Bldimg, false), + TrustDecision::Trusted + ); + assert_eq!( + trust_decision(&img, TrustKind::Bldimg, true), + TrustDecision::Trusted + ); + } + + #[test] + fn trust_decision_bldimg_other_registry_needs_confirmation() { + let img = format!("ghcr.io/stellar/stellar-cli@sha256:{}", "a".repeat(64)); + assert_eq!( + trust_decision(&img, TrustKind::Bldimg, false), + TrustDecision::NeedsConfirmation + ); + assert_eq!( + trust_decision(&img, TrustKind::Bldimg, true), + TrustDecision::Overridden + ); + } + + #[test] + fn trust_decision_bldimg_other_repo_on_dockerhub_needs_confirmation() { + // Same registry but different repo (fork) — not trusted. + let img = format!("docker.io/fnando/stellar-cli@sha256:{}", "a".repeat(64)); + assert_eq!( + trust_decision(&img, TrustKind::Bldimg, false), + TrustDecision::NeedsConfirmation + ); + } + + #[test] + fn trust_decision_tarball_always_needs_confirmation() { + assert_eq!( + trust_decision( + "https://github.com/foo/bar.tar.gz", + TrustKind::Tarball, + false + ), + TrustDecision::NeedsConfirmation + ); + assert_eq!( + trust_decision("/local/foo.tar.gz", TrustKind::Tarball, false), + TrustDecision::NeedsConfirmation + ); + } + + #[test] + fn trust_decision_tarball_override_with_trust() { + assert_eq!( + trust_decision( + "https://github.com/foo/bar.tar.gz", + TrustKind::Tarball, + true + ), + TrustDecision::Overridden + ); + } + + #[test] + fn parse_yes_accepts_all_case_variants() { + for yes in ["y", "Y", "yes", "YES", "Yes", "yEs", " y ", "yes\n"] { + assert!(parse_yes(yes), "{yes:?} should be yes"); + } + } + + #[test] + fn parse_yes_rejects_anything_else() { + for no in ["", "n", "N", "no", "NO", "x", "yup", "yeah", " "] { + assert!(!parse_yes(no), "{no:?} should not be yes"); + } + } + + #[test] + fn verify_tarball_sha256_matches() { + let bytes = b"hello, sep-58"; + let digest = format!("{:x}", Sha256::digest(bytes)); + verify_tarball_sha256(bytes, &digest).unwrap(); + // Case-insensitive: SEP-58 mandates lowercase but be lenient on input. + verify_tarball_sha256(bytes, &digest.to_ascii_uppercase()).unwrap(); + } + + #[test] + fn verify_tarball_sha256_mismatch_errors() { + let bytes = b"hello, sep-58"; + let bogus = "0".repeat(64); + let err = verify_tarball_sha256(bytes, &bogus).unwrap_err(); + assert!(matches!(err, Error::TarballHashMismatch { .. })); + } + + /// Build a tiny in-memory tar.gz with a single file and confirm extraction + /// drops the file at the expected path. Exercises the pure-Rust pipeline + /// (no shelling out, so this passes on Windows too). + #[test] + fn extract_tarball_unpacks_into_target() { + use flate2::write::GzEncoder; + use flate2::Compression; + use std::io::Write; + + let mut tar_bytes = Vec::new(); + { + let mut builder = tar::Builder::new(&mut tar_bytes); + let payload = b"contents"; + let mut header = tar::Header::new_gnu(); + header.set_path("hello.txt").unwrap(); + header.set_size(payload.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder.append(&header, &payload[..]).unwrap(); + builder.finish().unwrap(); + } + + let mut gz = Vec::new(); + { + let mut enc = GzEncoder::new(&mut gz, Compression::default()); + enc.write_all(&tar_bytes).unwrap(); + enc.finish().unwrap(); + } + + let dir = tempfile::TempDir::new().unwrap(); + extract_tarball(&gz, dir.path()).unwrap(); + let extracted = std::fs::read(dir.path().join("hello.txt")).unwrap(); + assert_eq!(extracted, b"contents"); + } + + #[tokio::test] + async fn materialize_source_errors_when_only_tarball_sha256() { + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: None, + source_rev: None, + tarball_url: None, + tarball_sha256: Some("f".repeat(64)), + bldopts: Vec::new(), + }; + let dir = tempfile::TempDir::new().unwrap(); + let print = Print::new(true); + let err = materialize_source(&meta, None, dir.path(), &print) + .await + .unwrap_err(); + assert!(matches!(err, Error::TarballUrlRequired)); + } + + #[test] + fn expand_source_repo_rewrites_github_shorthand() { + assert_eq!( + expand_source_repo("github:foo/bar"), + "https://github.com/foo/bar" + ); + } + + #[test] + fn expand_source_repo_passes_through_https() { + assert_eq!( + expand_source_repo("https://github.com/foo/bar"), + "https://github.com/foo/bar" + ); + assert_eq!( + expand_source_repo("https://gitlab.com/foo/bar.git"), + "https://gitlab.com/foo/bar.git" + ); + } + + #[test] + fn expand_source_repo_does_not_expand_malformed_github() { + // Missing the `/repo` suffix; the regex won't match so we pass through. + assert_eq!(expand_source_repo("github:foo"), "github:foo"); + // Extra path component; same. + assert_eq!( + expand_source_repo("github:foo/bar/baz"), + "github:foo/bar/baz" + ); + } + + #[test] + fn build_container_command_replays_bldopts_and_re_records_meta() { + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec![ + "--locked".to_string(), + "--meta=home_domain=fnando.com".to_string(), + "--optimize".to_string(), + ], + }; + let cmd = build_container_command(&meta); + + // Subcommand prefix. + assert_eq!(&cmd[..2], &["contract".to_string(), "build".to_string()]); + + // Bldopts are forwarded verbatim as flags to the inner `stellar contract build`. + assert!(cmd.contains(&"--locked".to_string())); + assert!(cmd.contains(&"--meta=home_domain=fnando.com".to_string())); + assert!(cmd.contains(&"--optimize".to_string())); + + // bldimg and source-ids are re-recorded as `--meta`. + assert!(cmd + .windows(2) + .any(|w| w[0] == "--meta" && w[1] == format!("bldimg={}", good_bldimg()))); + assert!(cmd + .windows(2) + .any(|w| w[0] == "--meta" && w[1] == "source_repo=https://github.com/foo/bar")); + + // Every bldopt is also re-recorded as a `bldopt=` meta so the rebuilt + // WASM mirrors the original's entries. + assert!(cmd + .windows(2) + .any(|w| w[0] == "--meta" && w[1] == "bldopt=--locked")); + } + + #[test] + fn build_container_command_injects_locked_when_missing() { + // A non-conformant origin might not have --locked in bldopts. Verify + // forces it anyway so dependency drift cannot move bytes. + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec!["--meta=author=alice".to_string()], + }; + let cmd = build_container_command(&meta); + let locked_count = cmd.iter().filter(|s| *s == "--locked").count(); + assert_eq!( + locked_count, 1, + "expected exactly one --locked, got {locked_count} in {cmd:?}" + ); + } + + #[test] + fn find_rebuilt_wasm_picks_single() { + let dir = tempfile::TempDir::new().unwrap(); + let release = dir.path().join("target/wasm32v1-none/release"); + std::fs::create_dir_all(&release).unwrap(); + std::fs::write(release.join("hello.wasm"), b"x").unwrap(); + + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec![], + }; + let p = find_rebuilt_wasm(dir.path(), &meta).unwrap(); + assert!(p.ends_with("hello.wasm")); + } + + #[test] + fn find_rebuilt_wasm_disambiguates_by_package() { + let dir = tempfile::TempDir::new().unwrap(); + let release = dir.path().join("target/wasm32v1-none/release"); + std::fs::create_dir_all(&release).unwrap(); + std::fs::write(release.join("hello.wasm"), b"x").unwrap(); + std::fs::write(release.join("other_thing.wasm"), b"x").unwrap(); + + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec!["--package=other-thing".to_string()], + }; + let p = find_rebuilt_wasm(dir.path(), &meta).unwrap(); + assert!(p.ends_with("other_thing.wasm")); + } + + #[test] + fn find_rebuilt_wasm_errors_when_ambiguous_without_package() { + let dir = tempfile::TempDir::new().unwrap(); + let release = dir.path().join("target/wasm32v1-none/release"); + std::fs::create_dir_all(&release).unwrap(); + std::fs::write(release.join("hello.wasm"), b"x").unwrap(); + std::fs::write(release.join("other.wasm"), b"x").unwrap(); + + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec![], + }; + let err = find_rebuilt_wasm(dir.path(), &meta).unwrap_err(); + assert!(matches!(err, Error::AmbiguousRebuiltWasm { .. })); + } + + #[test] + fn find_rebuilt_wasm_errors_when_none() { + let dir = tempfile::TempDir::new().unwrap(); + let meta = ExtractedMetadata { + bldimg: good_bldimg(), + source_repo: Some("https://github.com/foo/bar".to_string()), + source_rev: Some("b".repeat(40)), + tarball_url: None, + tarball_sha256: None, + bldopts: vec![], + }; + let err = find_rebuilt_wasm(dir.path(), &meta).unwrap_err(); + assert!(matches!(err, Error::NoRebuiltWasm { .. })); + } +} diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index 995a45a78..e2a1a8886 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -160,6 +160,7 @@ create_print_functions!(event, eventln, "📅"); create_print_functions!(blank, blankln, " "); create_print_functions!(gear, gearln, "⚙️"); create_print_functions!(dir, dirln, "📁"); +create_print_functions!(question, questionln, "❓"); #[cfg(test)] mod tests {