From 7a697370997898741d5cd0416efdfce5b322210b Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Mon, 20 Jul 2026 19:23:38 -0600 Subject: [PATCH] feat(rust): add offline release-manifest validator and renderer Vendor the canonical CTO v1 Draft 2020-12 schema with its import descriptor. Add the isolated rust-release-manifest workspace library and CLI for real JSON Schema validation, live semantic binding, deterministic rendering, and complete five-file payload classification. Add make check-rust-release-manifest to make ci with fixture, MANIFEST, and RELEASE_DIR modes while leaving the observer binary dependency graph unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 1 + Cargo.lock | 825 +++++++++++++++-- Cargo.toml | 1 + Makefile | 25 +- RELEASING.md | 33 + contracts/README.md | 6 + contracts/rust-release-manifest-import.json | 10 + crates/rust-release-manifest/Cargo.toml | 28 + crates/rust-release-manifest/src/lib.rs | 848 ++++++++++++++++++ crates/rust-release-manifest/src/main.rs | 68 ++ crates/rust-release-manifest/src/tests.rs | 556 ++++++++++++ deny.toml | 2 + .../rust-release-manifest.schema.json | 184 ++++ 13 files changed, 2491 insertions(+), 96 deletions(-) create mode 100644 contracts/rust-release-manifest-import.json create mode 100644 crates/rust-release-manifest/Cargo.toml create mode 100644 crates/rust-release-manifest/src/lib.rs create mode 100644 crates/rust-release-manifest/src/main.rs create mode 100644 crates/rust-release-manifest/src/tests.rs create mode 100644 vendor/rust-release-manifest/rust-release-manifest.schema.json diff --git a/AGENTS.md b/AGENTS.md index 8a41d98..e884b5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,7 @@ make bootstrap # Install rustup if needed, then establish pinned tools make install # Establish pinned Rust/tools and install the observer make format # Format Rust source make test # Run locked Rust tests +make check-rust-release-manifest # Validate release-manifest fixtures offline make ci # Host evidence: Rust format, lint, tests, offline policy make audit # Refresh RustSec data, then check advisories make update-deps # Sole unlocked Cargo dependency-update path diff --git a/Cargo.lock b/Cargo.lock index 02f4da3..f6bf9ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,35 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -67,6 +96,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ar" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" + [[package]] name = "arrayref" version = "0.3.9" @@ -187,7 +222,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -216,13 +251,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -243,6 +278,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.13.1" @@ -280,17 +330,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "bytes" @@ -300,11 +362,13 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -356,9 +420,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" dependencies = [ "clap_builder", "clap_derive", @@ -378,14 +442,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -473,6 +537,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "data-url" version = "0.3.2" @@ -513,7 +583,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -528,12 +598,43 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-display-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-primitive-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" +dependencies = [ + "num-traits", + "quote", + "syn 2.0.119", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -552,7 +653,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -601,11 +702,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -616,6 +728,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -647,6 +769,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -656,26 +795,36 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -684,9 +833,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -703,32 +852,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-io", @@ -773,6 +922,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -782,7 +945,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core", "wasm-bindgen", ] @@ -830,7 +993,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -866,7 +1029,7 @@ dependencies = [ "futures-util", "glib", "gstreamer-sys", - "itertools", + "itertools 0.15.0", "kstring", "libc", "muldiv", @@ -897,6 +1060,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -963,9 +1131,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1195,6 +1363,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.15.0" @@ -1210,6 +1387,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -1221,6 +1408,51 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9baf521a926fd1e6f94af6922be9686b61c8a89a5fb7e14d6a1e21b79738d4cc" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "serde", + "serde_json", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917b3be16b2662538aafb55201790561e1f1ad1a0454baf42f33bd9de343778b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + [[package]] name = "ksni" version = "0.3.6" @@ -1236,9 +1468,9 @@ dependencies = [ [[package]] name = "kstring" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" dependencies = [ "static_assertions", ] @@ -1263,9 +1495,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "a7743783ea728ef5c31194c6590797eed286449b4a4e87d626d8a51f0a94e732" [[package]] name = "libflac-sys" @@ -1316,6 +1548,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" @@ -1328,6 +1569,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac-notification-sys" version = "0.6.15" @@ -1357,6 +1609,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mime" version = "0.3.17" @@ -1400,6 +1658,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify-rust" version = "4.18.0" @@ -1423,6 +1690,45 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -1437,7 +1743,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1449,12 +1755,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ + "num-bigint", "num-integer", "num-traits", ] @@ -1548,12 +1865,41 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + [[package]] name = "pastey" version = "0.2.3" @@ -1657,9 +2003,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1731,13 +2077,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1770,6 +2122,81 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "referencing" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2b21a85c07fcf1cb95ff7ab8a687fcd37fa70db59975b5a032abdaadfdf42e" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "reqwest" version = "0.12.28" @@ -1858,6 +2285,52 @@ dependencies = [ "memchr", ] +[[package]] +name = "rpm" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c8e1e837dd547798d3a82dd335caa39ea8aae4609743c9c905e177a53c16c7" +dependencies = [ + "base64", + "bitflags", + "digest", + "enum-display-derive", + "enum-primitive-derive", + "hex", + "itertools 0.14.0", + "log", + "memchr", + "nom", + "num", + "num-derive", + "num-traits", + "sha1", + "sha2", + "sha3", + "thiserror", + "zeroize", +] + +[[package]] +name = "rust-release-manifest" +version = "1.0.0" +dependencies = [ + "ar", + "chrono", + "clap", + "flate2", + "jsonschema", + "rpm", + "serde", + "serde_json", + "sha2", + "tar", + "tempfile", + "toml", + "xz2", + "zstd", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1924,6 +2397,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sd-notify" version = "0.5.0" @@ -1935,9 +2414,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1945,29 +2424,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1978,13 +2457,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -2008,6 +2487,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2019,6 +2509,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2114,7 +2614,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn", + "syn 2.0.119", "tempfile", "tokio", "tokio-util", @@ -2170,6 +2670,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.119" @@ -2181,6 +2692,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2198,7 +2720,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2214,6 +2736,17 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -2246,22 +2779,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -2275,9 +2808,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -2345,9 +2878,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.4" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2362,13 +2895,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2510,7 +3043,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2577,6 +3110,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2647,6 +3186,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2665,6 +3214,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "want" version = "0.3.1" @@ -2680,6 +3235,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -2722,7 +3286,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -2827,9 +3391,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -2923,7 +3487,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2934,7 +3498,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3104,6 +3668,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" @@ -3127,12 +3697,31 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xmlwriter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.3" @@ -3152,7 +3741,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -3194,14 +3783,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.17.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "zbus_names", "zvariant", "zvariant_utils", @@ -3209,15 +3798,35 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.3" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", "winnow", "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -3235,7 +3844,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -3275,7 +3884,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3284,11 +3893,39 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" dependencies = [ "endi", "enumflags2", @@ -3300,14 +3937,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "zvariant_utils", ] @@ -3320,6 +3957,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.119", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index a3daa72..9eb5541 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "crates/rust-release-manifest", "crates/solstone-linux", ] resolver = "3" diff --git a/Makefile b/Makefile index c92a0ba..ca741a5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # solstone-linux Makefile # Standalone Linux desktop observer for solstone -.PHONY: all bootstrap install format test check-observer-contract ci audit update-deps shellcheck install-service uninstall-service service-restart service-status service-logs versions clean clean-install release legacy-python-bootstrap legacy-python-install legacy-python-format legacy-python-test legacy-python-test-only legacy-python-ci legacy-python-release legacy-python-release-test check-toolchain-env establish-toolchain rust-preflight check-cargo-deny +.PHONY: all bootstrap install format test check-observer-contract check-rust-release-manifest ci audit update-deps shellcheck install-service uninstall-service service-restart service-status service-logs versions clean clean-install release legacy-python-bootstrap legacy-python-install legacy-python-format legacy-python-test legacy-python-test-only legacy-python-ci legacy-python-release legacy-python-release-test check-toolchain-env establish-toolchain rust-preflight check-cargo-deny APP := solstone-linux UNIT := solstone-linux.service @@ -91,10 +91,31 @@ check-observer-contract: rust-preflight tail -50 "$$output"; \ grep -Eq 'test result: ok\. 1 passed; 0 failed' "$$output" || { echo "error: observer contract named test did not execute" >&2; exit 1; } +check-rust-release-manifest: rust-preflight + @echo "Rust release manifest schema: 1" + @echo "Rust release manifest schema SHA-256: d4eabf52bcc68b56945912d351f818e5444fe8c6461cb5c48b096f87b17a875c" + @if [ -n "$(MANIFEST)" ] && [ -n "$(RELEASE_DIR)" ]; then echo "error: release manifest mode mismatch: expected one selector, actual two" >&2; exit 1; \ + elif [ -n "$(MANIFEST)" ]; then \ + CARGO_NET_OFFLINE=true $(CARGO) run $(CARGO_LOCKED) -p rust-release-manifest -- validate --manifest "$(MANIFEST)"; \ + elif [ -n "$(RELEASE_DIR)" ]; then \ + CARGO_NET_OFFLINE=true $(CARGO) run $(CARGO_LOCKED) -p rust-release-manifest -- validate --release-dir "$(RELEASE_DIR)"; \ + else \ + inventory=$$(CARGO_NET_OFFLINE=true $(CARGO) test $(CARGO_LOCKED) -p rust-release-manifest tests::rust_release_manifest_conformance -- --list); \ + printf '%s\n' "$$inventory"; \ + printf '%s\n' "$$inventory" | grep -Fx 'tests::rust_release_manifest_conformance: test' >/dev/null || { echo "error: release manifest test inventory mismatch" >&2; exit 1; }; \ + actual=$$(printf '%s\n' "$$inventory" | grep -c '^tests::rust_release_manifest_conformance:'); \ + [ "$$actual" -eq 1 ] || { echo "error: release manifest test inventory mismatch: expected 1, actual $$actual" >&2; exit 1; }; \ + output=$$(mktemp); \ + trap 'rm -f "$$output"' EXIT; \ + CARGO_NET_OFFLINE=true $(CARGO) test $(CARGO_LOCKED) -p rust-release-manifest tests::rust_release_manifest_conformance -- --exact >"$$output" 2>&1 || { status=$$?; tail -50 "$$output"; exit $$status; }; \ + tail -50 "$$output"; \ + grep -Eq 'test result: ok\. 1 passed; 0 failed' "$$output" || { echo "error: release manifest named test did not execute" >&2; exit 1; }; \ + fi + shellcheck: shellcheck $(SHELLCHECK_SCRIPTS) -ci: rust-preflight check-cargo-deny check-observer-contract +ci: rust-preflight check-cargo-deny check-observer-contract check-rust-release-manifest @echo "Evidence class: host evidence (format, lint, tests, and offline dependency policy)." @echo "This gate does not run target-package validation or the release FLAC soak." $(CARGO) fmt --check diff --git a/RELEASING.md b/RELEASING.md index 08388b7..599e711 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -76,6 +76,39 @@ Confirm that each artifact contains the binary, LICENSE, INSTALL-NOTES, and the icon set declared in the member package manifest. It must contain no systemd unit and no desktop file. +### Render and validate the release manifest + +After all three product artifacts exist, provide explicit release evidence to +the repository-local renderer. It writes `rust-release-manifest.json` and +`SHA256SUMS` without discovering tool versions or contacting a service. Both +files list the tarball, Debian package, and RPM in POSIX-basename order. +Checksum rows use lowercase SHA-256, two spaces, the basename, and LF; neither +companion file lists itself or the other. + +Run the inert offline fixture gate with no payload selector: + +```bash +make check-rust-release-manifest +``` + +Verify one named manifest and its exact artifact bytes with: + +```bash +make check-rust-release-manifest MANIFEST=dist/rust/rust-release-manifest.json +``` + +This mode is not candidate-readiness classification because it does not reject +unrelated directory entries. Classify a complete candidate with: + +```bash +make check-rust-release-manifest RELEASE_DIR=dist/rust +``` + +Candidate classification requires exactly five regular, non-symlink files: +the tarball, Debian package, RPM, `SHA256SUMS`, and manifest. The renderer and +validator are offline and provider-neutral; signing and publication remain +separate operator actions. + ## 5. Blocking first-release FLAC validation This checkpoint is mandatory. Do not release based only on a successful link. diff --git a/contracts/README.md b/contracts/README.md index e5c081e..db25922 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -12,3 +12,9 @@ To adopt an authority revision: 6. Run `make check-observer-contract` to verify provenance, frozen bytes, identities, and Linux production behavior offline. Bundle versions, solstone-linux application releases, and observer wire-protocol versions evolve independently. If conformance exposes an incompatibility, resolve it against the journal authority before changing consumer behavior. Never edit authority fixtures or vectors to fit the observer. + +## Rust release manifest schema import + +solstone-linux keeps a frozen, byte-exact copy of the Rust release manifest schema in `vendor/rust-release-manifest/`. The repository-local renderer and validator use that copy to produce and verify release evidence fully offline. + +To adopt an authority revision, verify the schema identity, dialect, byte length, and SHA-256 before replacing the vendored file. Then update `rust-release-manifest-import.json` with the public authority revision and exact schema provenance. Do not reformat or regenerate the schema during import. Run `make check-rust-release-manifest` to verify the frozen schema, renderer, checksums, package metadata, and release-directory classifier offline. diff --git a/contracts/rust-release-manifest-import.json b/contracts/rust-release-manifest-import.json new file mode 100644 index 0000000..730369f --- /dev/null +++ b/contracts/rust-release-manifest-import.json @@ -0,0 +1,10 @@ +{ + "authority_repository": "https://github.com/solpbc/solstone-linux", + "authority_commit": "977ecc43588d93ba81b9e67327888543492c153a", + "schema_version": 1, + "schema_path": "rust-release-manifest.schema.json", + "schema_sha256": "d4eabf52bcc68b56945912d351f818e5444fe8c6461cb5c48b096f87b17a875c", + "vendored_root": "vendor/rust-release-manifest", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_id": "https://solpbc.org/schemas/rust-release-manifest/v1.json" +} diff --git a/crates/rust-release-manifest/Cargo.toml b/crates/rust-release-manifest/Cargo.toml new file mode 100644 index 0000000..68cec4b --- /dev/null +++ b/crates/rust-release-manifest/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rust-release-manifest" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +ar = "0.9.0" +chrono = "0.4" +clap = { workspace = true, features = ["derive"] } +flate2 = "1" +jsonschema = { version = "=0.48.1", default-features = false } +rpm = { version = "0.23.5", default-features = false, features = ["payload"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +tar = "0.4" +toml = "1" +xz2 = "0.1.7" +zstd = { version = "0.13.3", default-features = false } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/rust-release-manifest/src/lib.rs b/crates/rust-release-manifest/src/lib.rs new file mode 100644 index 0000000..a1a2fac --- /dev/null +++ b/crates/rust-release-manifest/src/lib.rs @@ -0,0 +1,848 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +//! Offline validation and deterministic rendering for Rust release manifests. + +use chrono::DateTime; +use flate2::read::GzDecoder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::{BufReader, Cursor, Read}; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::path::{Component, Path, PathBuf}; +use std::process::Command; +use tar::Archive; +use xz2::read::XzDecoder; + +pub const SCHEMA_VERSION: u64 = 1; +pub const SCHEMA_SHA256: &str = "d4eabf52bcc68b56945912d351f818e5444fe8c6461cb5c48b096f87b17a875c"; +pub const MANIFEST_NAME: &str = "rust-release-manifest.json"; +pub const CHECKSUM_NAME: &str = "SHA256SUMS"; +pub const PRODUCT: &str = "solstone-linux"; +pub const TARGET_TRIPLE: &str = "x86_64-unknown-linux-gnu"; +pub const CARGO_DENY_VERSION: &str = "0.20.2"; +pub const MANIFEST_OK_MESSAGE: &str = + "Named manifest and artifacts verified; this is NOT candidate-readiness classification."; +pub const RELEASE_DIR_OK_MESSAGE: &str = + "Release directory verified as a complete five-file candidate."; + +const SCHEMA_BYTES: &[u8] = + include_bytes!("../../../vendor/rust-release-manifest/rust-release-manifest.schema.json"); +const TOOL_KEYS: [&str; 18] = [ + "cargo_deb", + "cargo_generate_rpm", + "container_engine", + "dpkg_deb", + "fedora_image_digest", + "fedora_os", + "manifest_validator", + "rpm", + "signing_mode", + "ubuntu_cargo", + "ubuntu_compiler", + "ubuntu_glibc", + "ubuntu_gzip", + "ubuntu_image_digest", + "ubuntu_linker", + "ubuntu_os", + "ubuntu_rustc", + "ubuntu_tar", +]; +#[cfg(test)] +const EXCEPTIONS: [&str; 2] = ["RUSTSEC-2026-0194", "RUSTSEC-2026-0195"]; + +#[derive(Debug)] +pub struct Error(String); + +impl Error { + fn new(message: impl Into) -> Self { + Self(message.into()) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for Error {} + +type Result = std::result::Result; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Evidence { + pub schema_version: u64, + pub product: String, + pub version: String, + pub source_commit: String, + pub source_dirty: bool, + pub cargo_lock_sha256: String, + pub rust: RustEvidence, + pub target: TargetEvidence, + pub native_tools: BTreeMap, + pub dependency_policy: DependencyPolicy, + pub active_exceptions: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RustEvidence { + pub rustc_verbose: String, + pub cargo_version: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum TargetEvidence { + Compiled { + triple: String, + profile: String, + features: Vec, + }, + Source, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DependencyPolicy { + pub cargo_deny_version: String, + pub deterministic_gate: String, + pub advisory_checked_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Artifact { + pub path: String, + pub sha256: String, + pub bytes: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Manifest { + pub schema_version: u64, + pub product: String, + pub version: String, + pub source_commit: String, + pub source_dirty: bool, + pub cargo_lock_sha256: String, + pub rust: RustEvidence, + pub target: TargetEvidence, + pub native_tools: BTreeMap, + pub dependency_policy: DependencyPolicy, + pub active_exceptions: Vec, + pub artifacts: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +struct PackageIdentity { + name: String, + version: String, + release: Option, + arch: String, +} + +pub fn render_manifest(evidence: Evidence, release_dir: &Path) -> Result { + validate_evidence(&evidence)?; + let artifacts = artifact_paths(release_dir)? + .into_iter() + .map(|path| artifact(&path)) + .collect::>>()?; + let manifest = Manifest { + schema_version: evidence.schema_version, + product: evidence.product, + version: evidence.version, + source_commit: evidence.source_commit, + source_dirty: evidence.source_dirty, + cargo_lock_sha256: evidence.cargo_lock_sha256, + rust: evidence.rust, + target: evidence.target, + native_tools: evidence.native_tools, + dependency_policy: evidence.dependency_policy, + active_exceptions: evidence.active_exceptions, + artifacts, + }; + let mut output = serde_json::to_string_pretty(&manifest).map_err(display_error)?; + output.push('\n'); + validate_manifest_bytes(output.as_bytes())?; + Ok(output) +} + +pub fn render_sha256sums(artifacts: &[Artifact]) -> Result { + let mut sorted = artifacts.to_vec(); + sorted.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes())); + validate_artifact_set(&sorted)?; + Ok(sorted + .iter() + .map(|item| format!("{} {}\n", item.sha256, item.path)) + .collect()) +} + +pub fn validate_manifest_bytes(bytes: &[u8]) -> Result { + verify_schema()?; + let value: Value = serde_json::from_slice(bytes).map_err(display_error)?; + let schema: Value = serde_json::from_slice(SCHEMA_BYTES).map_err(display_error)?; + let validator = jsonschema::draft202012::options() + .should_validate_formats(true) + .build(&schema) + .map_err(display_error)?; + if let Err(error) = validator.validate(&value) { + return Err(Error::new(format!("manifest schema mismatch: {error}"))); + } + let manifest: Manifest = serde_json::from_value(value).map_err(display_error)?; + validate_manifest_policy(&manifest)?; + Ok(manifest) +} + +pub fn verify_manifest_mode(path: &Path) -> Result<()> { + verify_manifest(path, true) +} + +fn verify_manifest(path: &Path, bind_live: bool) -> Result<()> { + require_regular(path, "manifest")?; + let root = path + .parent() + .ok_or_else(|| Error::new("manifest parent missing"))?; + let manifest = validate_manifest_bytes(&fs::read(path).map_err(display_error)?)?; + if bind_live { + validate_live(&manifest, root)?; + } + verify_artifacts(&manifest, root)?; + verify_checksums(&manifest, root) +} + +pub fn classify_release_dir(root: &Path) -> Result<()> { + classify_release(root, true) +} + +fn classify_release(root: &Path, bind_live: bool) -> Result<()> { + require_directory(root, "release root")?; + let mut names = BTreeSet::new(); + for entry in fs::read_dir(root).map_err(display_error)? { + let entry = entry.map_err(display_error)?; + let name = entry + .file_name() + .into_string() + .map_err(|_| Error::new("release inventory contains non-UTF-8 path"))?; + portable_path(&name)?; + require_regular(&entry.path(), &name)?; + if !names.insert(name.clone()) { + return Err(Error::new(format!("duplicate release path: {name}"))); + } + } + let manifests: Vec<_> = names + .iter() + .filter(|name| name.as_str() == MANIFEST_NAME) + .collect(); + if manifests.len() != 1 || names.len() != 5 { + return Err(Error::new(format!( + "release inventory mismatch: expected 5 files, actual {}", + names.len() + ))); + } + verify_manifest(&root.join(MANIFEST_NAME), bind_live) +} + +pub fn write_rendered(evidence: Evidence, release_dir: &Path) -> Result<()> { + let manifest_text = render_manifest(evidence, release_dir)?; + let manifest = validate_manifest_bytes(manifest_text.as_bytes())?; + let sums = render_sha256sums(&manifest.artifacts)?; + fs::write(release_dir.join(MANIFEST_NAME), manifest_text).map_err(display_error)?; + fs::write(release_dir.join(CHECKSUM_NAME), sums).map_err(display_error) +} + +pub fn schema_bytes() -> &'static [u8] { + SCHEMA_BYTES +} + +fn validate_evidence(evidence: &Evidence) -> Result<()> { + if evidence.schema_version != SCHEMA_VERSION + || evidence.product != PRODUCT + || evidence.source_dirty + { + return Err(Error::new("release evidence identity mismatch")); + } + validate_native_tools(&evidence.native_tools)?; + validate_timestamp(&evidence.dependency_policy.advisory_checked_at) +} + +fn validate_manifest_policy(manifest: &Manifest) -> Result<()> { + validate_native_tools(&manifest.native_tools)?; + validate_timestamp(&manifest.dependency_policy.advisory_checked_at)?; + validate_artifact_set(&manifest.artifacts) +} + +fn validate_timestamp(value: &str) -> Result<()> { + DateTime::parse_from_rfc3339(value).map_err(|_| Error::new("advisory time mismatch"))?; + if !value.ends_with('Z') { + return Err(Error::new( + "advisory time mismatch: expected canonical UTC Z suffix", + )); + } + Ok(()) +} + +fn validate_native_tools(tools: &BTreeMap) -> Result<()> { + let actual = tools.keys().map(String::as_str).collect::>(); + let expected = TOOL_KEYS.into_iter().collect::>(); + if actual != expected { + return Err(Error::new(format!( + "native tools mismatch: expected 18 keys, actual {}", + actual.len() + ))); + } + exact_tool(tools, "cargo_deb", "3.7.0")?; + exact_tool(tools, "cargo_generate_rpm", "0.21.0")?; + exact_tool(tools, "manifest_validator", env!("CARGO_PKG_VERSION"))?; + exact_tool(tools, "signing_mode", "unsigned")?; + for key in ["ubuntu_image_digest", "fedora_image_digest"] { + let value = &tools[key]; + if !is_sha256(value) { + return Err(Error::new(format!("native tool {key} mismatch"))); + } + } + for key in TOOL_KEYS { + if !matches!( + key, + "cargo_deb" + | "cargo_generate_rpm" + | "manifest_validator" + | "signing_mode" + | "ubuntu_image_digest" + | "fedora_image_digest" + ) { + validate_identity(key, &tools[key])?; + } + } + Ok(()) +} + +fn exact_tool(tools: &BTreeMap, key: &str, expected: &str) -> Result<()> { + if tools[key] != expected { + return Err(Error::new(format!( + "native tool {key} mismatch: expected {expected}, actual {}", + tools[key] + ))); + } + Ok(()) +} + +fn validate_identity(key: &str, value: &str) -> Result<()> { + let lower = value.to_ascii_lowercase(); + let forbidden = [ + "token", + "secret", + "password", + "bearer", + "localhost", + ".local", + ".internal", + "staging", + "sandbox", + "preview", + " dev ", + " test ", + "socket", + "pipe:", + "ipc:", + ]; + let approved_prefixes: &[&str] = match key { + "container_engine" => &["podman ", "docker "], + "ubuntu_os" => &["Ubuntu "], + "ubuntu_rustc" => &["rustc "], + "ubuntu_cargo" => &["cargo "], + "ubuntu_compiler" => &["gcc ", "cc "], + "ubuntu_linker" => &["GNU ld ", "ld "], + "ubuntu_glibc" => &["glibc "], + "ubuntu_tar" => &["GNU tar ", "tar "], + "ubuntu_gzip" => &["gzip "], + "fedora_os" => &["Fedora "], + "dpkg_deb" => &["dpkg-deb "], + "rpm" => &["RPM ", "rpm "], + _ => &[], + }; + let bad = !approved_prefixes + .iter() + .any(|prefix| value.starts_with(prefix)) + || value.trim() != value + || value.contains(" ") + || value.chars().any(|character| character.is_control()) + || value.contains(['$', '%', '@', '/', '\\']) + || value.contains("://") + || forbidden.iter().any(|word| lower.contains(word)) + || !value.chars().any(|character| character.is_ascii_digit()) + || value.split_whitespace().count() > 6; + if bad { + return Err(Error::new(format!("native tool {key} identity mismatch"))); + } + Ok(()) +} + +fn validate_artifact_set(artifacts: &[Artifact]) -> Result<()> { + if artifacts.len() != 3 { + return Err(Error::new(format!( + "artifact inventory mismatch: expected 3, actual {}", + artifacts.len() + ))); + } + let mut previous: Option<&str> = None; + let mut kinds = BTreeSet::new(); + for artifact in artifacts { + portable_path(&artifact.path)?; + if Path::new(&artifact.path).file_name() != Some(OsStr::new(&artifact.path)) { + return Err(Error::new("artifact path mismatch: expected basename")); + } + if !is_sha256(&artifact.sha256) || artifact.bytes == 0 { + return Err(Error::new(format!( + "artifact metadata mismatch: {}", + artifact.path + ))); + } + if previous.is_some_and(|name| name.as_bytes() >= artifact.path.as_bytes()) { + return Err(Error::new("artifact ordering mismatch")); + } + previous = Some(&artifact.path); + kinds.insert(artifact_kind(&artifact.path)?); + } + if kinds != BTreeSet::from(["deb", "rpm", "tar"]) { + return Err(Error::new("artifact type inventory mismatch")); + } + Ok(()) +} + +fn artifact_kind(name: &str) -> Result<&'static str> { + if name.starts_with("solstone-linux-") && name.ends_with("-linux-x86_64.tar.gz") { + Ok("tar") + } else if name.starts_with("solstone-linux_") && name.ends_with("-1_amd64.deb") { + Ok("deb") + } else if name.starts_with("solstone-linux-") && name.ends_with("-1.x86_64.rpm") { + Ok("rpm") + } else { + Err(Error::new(format!("artifact basename mismatch: {name}"))) + } +} + +fn artifact_paths(root: &Path) -> Result> { + require_directory(root, "release root")?; + let mut paths = Vec::new(); + for entry in fs::read_dir(root).map_err(display_error)? { + let entry = entry.map_err(display_error)?; + let name = entry + .file_name() + .into_string() + .map_err(|_| Error::new("artifact path is not UTF-8"))?; + if artifact_kind(&name).is_ok() { + require_regular(&entry.path(), &name)?; + paths.push(entry.path()); + } + } + paths.sort_by(|left, right| left.file_name().cmp(&right.file_name())); + if paths.len() != 3 { + return Err(Error::new(format!( + "artifact inventory mismatch: expected 3, actual {}", + paths.len() + ))); + } + Ok(paths) +} + +fn artifact(path: &Path) -> Result { + require_regular(path, "artifact")?; + let bytes = fs::read(path).map_err(display_error)?; + Ok(Artifact { + path: path + .file_name() + .and_then(OsStr::to_str) + .ok_or_else(|| Error::new("artifact basename missing"))? + .to_owned(), + sha256: digest(&bytes), + bytes: u64::try_from(bytes.len()).map_err(display_error)?, + }) +} + +fn verify_artifacts(manifest: &Manifest, root: &Path) -> Result<()> { + for item in &manifest.artifacts { + let path = root.join(&item.path); + require_regular(&path, &item.path)?; + let actual = artifact(&path)?; + if actual != *item { + return Err(Error::new(format!( + "artifact bytes mismatch: {}", + item.path + ))); + } + verify_package_identity(&path, &manifest.version)?; + } + Ok(()) +} + +pub fn verify_checksums(manifest: &Manifest, root: &Path) -> Result<()> { + let path = root.join(CHECKSUM_NAME); + require_regular(&path, CHECKSUM_NAME)?; + let actual = fs::read_to_string(path).map_err(display_error)?; + let expected = render_sha256sums(&manifest.artifacts)?; + if actual != expected { + return Err(Error::new("checksum inventory mismatch")); + } + Ok(()) +} + +fn verify_package_identity(path: &Path, version: &str) -> Result<()> { + let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default(); + validate_version(version)?; + let expected = match artifact_kind(name)? { + "tar" => PackageIdentity { + name: PRODUCT.to_owned(), + version: tar_version(path)?, + release: None, + arch: "x86_64".to_owned(), + }, + "deb" => deb_identity(path)?, + "rpm" => rpm_identity(path)?, + _ => unreachable!(), + }; + if expected.name != PRODUCT + || expected.version != version + || !matches!(expected.arch.as_str(), "x86_64" | "amd64") + || expected + .release + .as_deref() + .is_some_and(|release| release != "1") + { + return Err(Error::new(format!("package metadata mismatch: {name}"))); + } + Ok(()) +} + +fn tar_version(path: &Path) -> Result { + let file = File::open(path).map_err(display_error)?; + let mut archive = Archive::new(GzDecoder::new(file)); + let mut roots = BTreeSet::new(); + for entry in archive.entries().map_err(display_error)? { + let entry = entry.map_err(display_error)?; + let path = entry.path().map_err(display_error)?; + let root = path + .components() + .next() + .and_then(|part| match part { + Component::Normal(value) => value.to_str(), + _ => None, + }) + .ok_or_else(|| Error::new("tar root mismatch"))?; + roots.insert(root.to_owned()); + } + if roots.len() != 1 { + return Err(Error::new("tar root inventory mismatch")); + } + let root = roots.pop_first().unwrap(); + root.strip_prefix("solstone-linux-") + .and_then(|value| value.strip_suffix("-linux-x86_64")) + .map(str::to_owned) + .ok_or_else(|| Error::new("tar embedded version mismatch")) +} + +fn deb_identity(path: &Path) -> Result { + let file = File::open(path).map_err(display_error)?; + let mut archive = ar::Archive::new(file); + let mut control = None; + while let Some(entry) = archive.next_entry() { + let mut entry = entry.map_err(display_error)?; + let name = std::str::from_utf8(entry.header().identifier()) + .map_err(display_error)? + .trim_end_matches('/') + .to_owned(); + if name.starts_with("control.tar.") { + let mut compressed = Vec::new(); + entry.read_to_end(&mut compressed).map_err(display_error)?; + control = Some(read_control_archive(&name, compressed)?); + } + } + parse_deb_control(&control.ok_or_else(|| Error::new("deb control archive missing"))?) +} + +fn read_control_archive(name: &str, bytes: Vec) -> Result { + let reader: Box = if name.ends_with(".xz") { + Box::new(XzDecoder::new(Cursor::new(bytes))) + } else if name.ends_with(".gz") { + Box::new(GzDecoder::new(Cursor::new(bytes))) + } else if name.ends_with(".zst") { + Box::new(zstd::stream::read::Decoder::new(Cursor::new(bytes)).map_err(display_error)?) + } else { + return Err(Error::new("deb control compression mismatch")); + }; + let mut archive = Archive::new(reader); + for entry in archive.entries().map_err(display_error)? { + let mut entry = entry.map_err(display_error)?; + let path = entry.path().map_err(display_error)?; + if matches!(path.to_str(), Some("control" | "./control")) { + let mut body = String::new(); + entry.read_to_string(&mut body).map_err(display_error)?; + return Ok(body); + } + } + Err(Error::new("deb control metadata missing")) +} + +fn parse_deb_control(body: &str) -> Result { + let fields = body + .lines() + .filter_map(|line| line.split_once(":")) + .map(|(key, value)| (key, value.trim())) + .collect::>(); + let version = field(&fields, "Version")?; + let (version, release) = version + .rsplit_once('-') + .ok_or_else(|| Error::new("deb version mismatch"))?; + Ok(PackageIdentity { + name: field(&fields, "Package")?.to_owned(), + version: version.to_owned(), + release: Some(release.to_owned()), + arch: field(&fields, "Architecture")?.to_owned(), + }) +} + +fn field<'a>(fields: &'a BTreeMap<&str, &str>, key: &str) -> Result<&'a str> { + let value = fields + .get(key) + .copied() + .ok_or_else(|| Error::new(format!("package field missing: {key}")))?; + if value.is_empty() + || value.trim() != value + || value.starts_with('-') + || value.chars().any(char::is_control) + { + return Err(Error::new(format!("package field mismatch: {key}"))); + } + Ok(value) +} + +fn rpm_identity(path: &Path) -> Result { + let file = File::open(path).map_err(display_error)?; + let mut reader = BufReader::new(file); + let package = rpm::PackageMetadata::parse(&mut reader).map_err(display_error)?; + Ok(PackageIdentity { + name: package.get_name().map_err(display_error)?.to_owned(), + version: package.get_version().map_err(display_error)?.to_owned(), + release: Some(package.get_release().map_err(display_error)?.to_owned()), + arch: package.get_arch().map_err(display_error)?.to_owned(), + }) +} + +fn validate_live(manifest: &Manifest, payload_root: &Path) -> Result<()> { + let root = workspace_root()?; + let root_toml: toml::Value = + toml::from_str(&fs::read_to_string(root.join("Cargo.toml")).map_err(display_error)?) + .map_err(display_error)?; + let member_toml: toml::Value = toml::from_str( + &fs::read_to_string(root.join("crates/solstone-linux/Cargo.toml")) + .map_err(display_error)?, + ) + .map_err(display_error)?; + let version = root_toml["workspace"]["package"]["version"] + .as_str() + .ok_or_else(|| Error::new("workspace version missing"))?; + let product = member_toml["package"]["name"] + .as_str() + .ok_or_else(|| Error::new("product name missing"))?; + let commit = command(&root, &["git", "rev-parse", "HEAD"])?; + let lock_digest = digest(&fs::read(root.join("Cargo.lock")).map_err(display_error)?); + let makefile = fs::read_to_string(root.join("Makefile")).map_err(display_error)?; + let cargo_deny_version = makefile + .lines() + .find_map(|line| line.strip_prefix("CARGO_DENY_VERSION := ")) + .ok_or_else(|| Error::new("cargo-deny version authority missing"))?; + let deny: toml::Value = + toml::from_str(&fs::read_to_string(root.join("deny.toml")).map_err(display_error)?) + .map_err(display_error)?; + let active_exceptions = deny["advisories"]["ignore"] + .as_array() + .ok_or_else(|| Error::new("advisory exception authority missing"))? + .iter() + .map(|entry| { + entry["id"] + .as_str() + .ok_or_else(|| Error::new("advisory exception id missing")) + }) + .collect::>>()?; + if manifest.product != product + || manifest.version != version + || manifest.source_commit != commit + || manifest.source_dirty + || manifest.cargo_lock_sha256 != lock_digest + || manifest.dependency_policy.cargo_deny_version != cargo_deny_version + || manifest.dependency_policy.deterministic_gate != "pass" + || manifest + .active_exceptions + .iter() + .map(String::as_str) + .collect::>() + != active_exceptions + || !matches!(&manifest.target, TargetEvidence::Compiled { triple, profile, features } + if triple == TARGET_TRIPLE && profile == "release" && features.is_empty()) + { + return Err(Error::new("live release evidence mismatch")); + } + require_clean_tree(&root, payload_root) +} + +fn require_clean_tree(root: &Path, payload_root: &Path) -> Result<()> { + let status = command( + root, + &[ + "git", + "status", + "--porcelain", + "--untracked-files=all", + "--ignored=no", + ], + )?; + if status.is_empty() { + return Ok(()); + } + let allowed = payload_root + .canonicalize() + .unwrap_or_else(|_| payload_root.to_owned()); + for line in status.lines() { + let relative = line.get(3..).unwrap_or_default().trim_matches('"'); + let path = root.join(relative); + if !path.starts_with(&allowed) { + return Err(Error::new("source dirty mismatch: expected clean tree")); + } + } + Ok(()) +} + +fn workspace_root() -> Result { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .map_err(display_error) +} + +fn command(root: &Path, args: &[&str]) -> Result { + let output = Command::new(args[0]) + .args(&args[1..]) + .current_dir(root) + .output() + .map_err(display_error)?; + if !output.status.success() { + return Err(Error::new(format!("command mismatch: {}", args[0]))); + } + String::from_utf8(output.stdout) + .map(|value| value.trim().to_owned()) + .map_err(display_error) +} + +fn verify_schema() -> Result<()> { + if SCHEMA_BYTES.len() != 4416 || digest(SCHEMA_BYTES) != SCHEMA_SHA256 { + return Err(Error::new("vendored schema bytes mismatch")); + } + let schema: Value = serde_json::from_slice(SCHEMA_BYTES).map_err(display_error)?; + if schema["$schema"] != "https://json-schema.org/draft/2020-12/schema" + || schema["$id"] != "https://solpbc.org/schemas/rust-release-manifest/v1.json" + { + return Err(Error::new("vendored schema identity mismatch")); + } + Ok(()) +} + +fn portable_path(path: &str) -> Result<()> { + if path.is_empty() + || path.starts_with(['/', '\\']) + || path.contains('\\') + || path.chars().any(char::is_control) + || Path::new(path) + .components() + .any(|part| !matches!(part, Component::Normal(_))) + { + return Err(Error::new(format!("unsafe path: {path:?}"))); + } + for part in path.split('/') { + if part.is_empty() + || part.ends_with(['.', ' ']) + || part.contains(['<', '>', ':', '"', '|', '?', '*']) + { + return Err(Error::new(format!("non-portable path: {path:?}"))); + } + let stem = part + .split('.') + .next() + .unwrap() + .trim_end() + .to_ascii_uppercase(); + let reserved = matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") + || stem + .strip_prefix("COM") + .or_else(|| stem.strip_prefix("LPT")) + .is_some_and(|suffix| { + suffix.len() == 1 && matches!(suffix.as_bytes()[0], b'1'..=b'9') + }); + if reserved { + return Err(Error::new(format!("reserved path: {path:?}"))); + } + } + Ok(()) +} + +fn require_regular(path: &Path, label: &str) -> Result<()> { + let metadata = fs::symlink_metadata(path).map_err(display_error)?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(Error::new(format!( + "{label} mismatch: expected no-follow regular file" + ))); + } + if metadata.mode() & 0o7111 != 0 { + return Err(Error::new(format!("{label} mode mismatch"))); + } + Ok(()) +} + +fn require_directory(path: &Path, label: &str) -> Result<()> { + let metadata = fs::symlink_metadata(path).map_err(display_error)?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(Error::new(format!( + "{label} mismatch: expected no-follow directory" + ))); + } + if metadata.file_type().is_socket() + || metadata.file_type().is_fifo() + || metadata.file_type().is_block_device() + || metadata.file_type().is_char_device() + { + return Err(Error::new(format!("{label} special file mismatch"))); + } + Ok(()) +} + +fn validate_version(value: &str) -> Result<()> { + let valid = !value.is_empty() + && !value.starts_with('-') + && value.trim() == value + && value.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '.' | '+' | '-') + }); + if !valid { + return Err(Error::new("version mismatch")); + } + Ok(()) +} + +fn is_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} + +fn digest(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +fn display_error(error: impl std::fmt::Display) -> Error { + Error::new(error.to_string()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/rust-release-manifest/src/main.rs b/crates/rust-release-manifest/src/main.rs new file mode 100644 index 0000000..1efa369 --- /dev/null +++ b/crates/rust-release-manifest/src/main.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +use clap::{Parser, Subcommand}; +use rust_release_manifest::{ + Evidence, MANIFEST_OK_MESSAGE, RELEASE_DIR_OK_MESSAGE, classify_release_dir, + verify_manifest_mode, write_rendered, +}; +use std::fs; +use std::path::PathBuf; + +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + Validate { + #[arg(long, conflicts_with = "release_dir")] + manifest: Option, + #[arg(long, conflicts_with = "manifest")] + release_dir: Option, + }, + Render { + #[arg(long)] + evidence: PathBuf, + #[arg(long)] + release_dir: PathBuf, + }, +} + +fn run() -> Result<(), Box> { + match Cli::parse().command { + Command::Validate { + manifest: Some(path), + release_dir: None, + } => { + verify_manifest_mode(&path)?; + println!("{MANIFEST_OK_MESSAGE}"); + } + Command::Validate { + manifest: None, + release_dir: Some(path), + } => { + classify_release_dir(&path)?; + println!("{RELEASE_DIR_OK_MESSAGE}"); + } + Command::Validate { .. } => return Err("exactly one validation path is required".into()), + Command::Render { + evidence, + release_dir, + } => { + let evidence: Evidence = serde_json::from_slice(&fs::read(evidence)?)?; + write_rendered(evidence, &release_dir)?; + println!("Rendered deterministic release manifest and SHA256SUMS."); + } + } + Ok(()) +} + +fn main() { + if let Err(error) = run() { + eprintln!("error: {error}"); + std::process::exit(1); + } +} diff --git a/crates/rust-release-manifest/src/tests.rs b/crates/rust-release-manifest/src/tests.rs new file mode 100644 index 0000000..5d99222 --- /dev/null +++ b/crates/rust-release-manifest/src/tests.rs @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +use super::*; +use flate2::Compression; +use flate2::write::GzEncoder; +use std::io::Write; +use std::os::unix::fs::symlink; + +fn tools() -> BTreeMap { + BTreeMap::from([ + ("container_engine".into(), "podman 5.4.0".into()), + ("ubuntu_image_digest".into(), "a".repeat(64)), + ("ubuntu_os".into(), "Ubuntu 22.04".into()), + ("ubuntu_rustc".into(), "rustc 1.97.1".into()), + ("ubuntu_cargo".into(), "cargo 1.97.1".into()), + ("ubuntu_compiler".into(), "gcc 11.4.0".into()), + ("ubuntu_linker".into(), "GNU ld 2.38".into()), + ("ubuntu_glibc".into(), "glibc 2.35".into()), + ("ubuntu_tar".into(), "GNU tar 1.34".into()), + ("ubuntu_gzip".into(), "gzip 1.10".into()), + ("cargo_deb".into(), "3.7.0".into()), + ("dpkg_deb".into(), "dpkg-deb 1.21.1".into()), + ("fedora_image_digest".into(), "b".repeat(64)), + ("fedora_os".into(), "Fedora 42".into()), + ("cargo_generate_rpm".into(), "0.21.0".into()), + ("rpm".into(), "RPM 4.20.0".into()), + ( + "manifest_validator".into(), + env!("CARGO_PKG_VERSION").into(), + ), + ("signing_mode".into(), "unsigned".into()), + ]) +} + +fn evidence() -> Evidence { + let root = workspace_root().unwrap(); + let version: toml::Value = + toml::from_str(&fs::read_to_string(root.join("Cargo.toml")).unwrap()).unwrap(); + Evidence { + schema_version: 1, + product: PRODUCT.into(), + version: version["workspace"]["package"]["version"] + .as_str() + .unwrap() + .into(), + source_commit: command(&root, &["git", "rev-parse", "HEAD"]).unwrap(), + source_dirty: false, + cargo_lock_sha256: digest(&fs::read(root.join("Cargo.lock")).unwrap()), + rust: RustEvidence { + rustc_verbose: "rustc 1.97.1".into(), + cargo_version: "cargo 1.97.1".into(), + }, + target: TargetEvidence::Compiled { + triple: TARGET_TRIPLE.into(), + profile: "release".into(), + features: vec![], + }, + native_tools: tools(), + dependency_policy: DependencyPolicy { + cargo_deny_version: CARGO_DENY_VERSION.into(), + deterministic_gate: "pass".into(), + advisory_checked_at: "2026-07-20T12:34:56Z".into(), + }, + active_exceptions: EXCEPTIONS.iter().map(|value| (*value).into()).collect(), + } +} + +fn tarball(root: &Path, version: &str) -> PathBuf { + let name = format!("solstone-linux-{version}-linux-x86_64.tar.gz"); + let path = root.join(name); + let encoder = GzEncoder::new(File::create(&path).unwrap(), Compression::default()); + let mut builder = tar::Builder::new(encoder); + let bytes = b"fixture"; + let mut header = tar::Header::new_gnu(); + header.set_size(bytes.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder + .append_data( + &mut header, + format!("solstone-linux-{version}-linux-x86_64/LICENSE"), + &bytes[..], + ) + .unwrap(); + builder.into_inner().unwrap().finish().unwrap(); + path +} + +fn control_tar(version: &str) -> Vec { + let mut archive = tar::Builder::new(Vec::new()); + let body = format!("Package: solstone-linux\nVersion: {version}-1\nArchitecture: amd64\n"); + let mut header = tar::Header::new_gnu(); + header.set_size(body.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + archive + .append_data(&mut header, "./control", body.as_bytes()) + .unwrap(); + archive.into_inner().unwrap() +} + +fn deb(root: &Path, version: &str) -> PathBuf { + let path = root.join(format!("solstone-linux_{version}-1_amd64.deb")); + let mut archive = ar::Builder::new(File::create(&path).unwrap()); + let marker = b"2.0\n"; + let header = ar::Header::new(b"debian-binary".to_vec(), marker.len() as u64); + archive.append(&header, &marker[..]).unwrap(); + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&control_tar(version)).unwrap(); + let compressed = encoder.finish().unwrap(); + let header = ar::Header::new(b"control.tar.gz".to_vec(), compressed.len() as u64); + archive.append(&header, &compressed[..]).unwrap(); + path +} + +fn rpm_file(root: &Path, version: &str) -> PathBuf { + let path = root.join(format!("solstone-linux-{version}-1.x86_64.rpm")); + let package = rpm::PackageBuilder::new(PRODUCT, version, "AGPL-3.0-only", "x86_64", "fixture") + .build() + .unwrap(); + package.write(&mut File::create(&path).unwrap()).unwrap(); + path +} + +fn release_fixture() -> tempfile::TempDir { + let temp = tempfile::tempdir().unwrap(); + let version = evidence().version; + tarball(temp.path(), &version); + deb(temp.path(), &version); + rpm_file(temp.path(), &version); + temp +} + +#[test] +fn rust_release_manifest_conformance() { + verify_schema().unwrap(); + let vendor = workspace_root() + .unwrap() + .join("vendor/rust-release-manifest"); + let entries = fs::read_dir(&vendor) + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .collect::>(); + assert_eq!( + entries, + BTreeSet::from(["rust-release-manifest.schema.json".into()]) + ); + let descriptor: Value = serde_json::from_slice( + &fs::read( + workspace_root() + .unwrap() + .join("contracts/rust-release-manifest-import.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(descriptor["schema_version"], 1); + assert_eq!(descriptor["schema_sha256"], SCHEMA_SHA256); + assert_eq!( + descriptor["schema_id"], + "https://solpbc.org/schemas/rust-release-manifest/v1.json" + ); + let temp = release_fixture(); + let first = render_manifest(evidence(), temp.path()).unwrap(); + let second = render_manifest(evidence(), temp.path()).unwrap(); + assert_eq!(first, second); + let other = tempfile::tempdir().unwrap(); + for entry in fs::read_dir(temp.path()).unwrap() { + let entry = entry.unwrap(); + fs::copy(entry.path(), other.path().join(entry.file_name())).unwrap(); + } + assert_eq!(first, render_manifest(evidence(), other.path()).unwrap()); + let manifest = validate_manifest_bytes(first.as_bytes()).unwrap(); + assert_eq!(manifest.artifacts.len(), 3); + assert_eq!( + manifest + .artifacts + .iter() + .map(|item| item.path.clone()) + .collect::>(), + { + let mut names = manifest + .artifacts + .iter() + .map(|item| item.path.clone()) + .collect::>(); + names.sort(); + names + } + ); + for item in &manifest.artifacts { + verify_package_identity(&temp.path().join(&item.path), &evidence().version).unwrap(); + } + let sums = render_sha256sums(&manifest.artifacts).unwrap(); + assert_eq!(sums.lines().count(), 3); + assert!(sums.ends_with('\n')); + + let mut bad: Value = serde_json::from_str(&first).unwrap(); + bad["dependency_policy"]["advisory_checked_at"] = + Value::String("2026-07-20T12:34:56+00:00".into()); + assert!(validate_manifest_bytes(serde_json::to_string(&bad).unwrap().as_bytes()).is_err()); + bad["dependency_policy"]["advisory_checked_at"] = Value::String("not-a-date".into()); + assert!(validate_manifest_bytes(serde_json::to_string(&bad).unwrap().as_bytes()).is_err()); + + for key in TOOL_KEYS { + let mut value: Value = serde_json::from_str(&first).unwrap(); + value["native_tools"].as_object_mut().unwrap().remove(key); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + for canary in [ + "$VERSION", + "tool 1.0 token=abc", + "tool 1.0 builder.internal", + "tool 1.0 /usr/bin/tool", + "tool 1.0 staging", + "tool 1.0\nnext", + "arbitrary prose with version 1", + ] { + let mut value: Value = serde_json::from_str(&first).unwrap(); + value["native_tools"]["container_engine"] = Value::String(canary.into()); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + let mut value: Value = serde_json::from_str(&first).unwrap(); + value["native_tools"]["ubuntu_image_digest"] = + Value::String(format!("sha256:{}", "a".repeat(64))); + assert!(validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err()); +} + +#[test] +fn package_readers_reject_malformed_and_stale_bytes() { + let temp = release_fixture(); + let version = evidence().version; + let stale = tarball(temp.path(), "9.9.9"); + assert!(verify_package_identity(&stale, &version).is_err()); + fs::write(&stale, b"not gzip").unwrap(); + assert!(tar_version(&stale).is_err()); + let bad_deb = temp.path().join("solstone-linux_9.9.9-1_amd64.deb"); + fs::write(&bad_deb, b"truncated").unwrap(); + assert!(deb_identity(&bad_deb).is_err()); + let bad_rpm = temp.path().join("solstone-linux-9.9.9-1.x86_64.rpm"); + fs::write(&bad_rpm, b"truncated").unwrap(); + assert!(rpm_identity(&bad_rpm).is_err()); +} + +#[test] +fn checksum_and_complete_inventory_mutations_fail() { + let temp = release_fixture(); + let text = render_manifest(evidence(), temp.path()).unwrap(); + let manifest = validate_manifest_bytes(text.as_bytes()).unwrap(); + fs::write(temp.path().join(MANIFEST_NAME), text).unwrap(); + fs::write( + temp.path().join(CHECKSUM_NAME), + render_sha256sums(&manifest.artifacts).unwrap(), + ) + .unwrap(); + for item in &manifest.artifacts { + verify_package_identity(&temp.path().join(&item.path), &manifest.version).unwrap(); + } + verify_checksums(&manifest, temp.path()).unwrap(); + classify_release(temp.path(), false).unwrap(); + let original = fs::read_to_string(temp.path().join(CHECKSUM_NAME)).unwrap(); + let mut lines = original.lines().collect::>(); + lines.swap(0, 1); + fs::write( + temp.path().join(CHECKSUM_NAME), + format!("{}\n", lines.join("\n")), + ) + .unwrap(); + assert!(verify_checksums(&manifest, temp.path()).is_err()); + fs::write(temp.path().join("extra"), b"extra").unwrap(); + assert!(classify_release_dir(temp.path()).is_err()); +} + +#[test] +fn schema_rejects_required_forbidden_and_path_mutations() { + let temp = release_fixture(); + let text = render_manifest(evidence(), temp.path()).unwrap(); + let original: Value = serde_json::from_str(&text).unwrap(); + for key in [ + "schema_version", + "product", + "version", + "source_commit", + "source_dirty", + "cargo_lock_sha256", + "rust", + "target", + "native_tools", + "dependency_policy", + "active_exceptions", + "artifacts", + ] { + let mut value = original.clone(); + value.as_object_mut().unwrap().remove(key); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + for path in ["/absolute", "C:drive", "../escape", "bad\\name"] { + let mut value = original.clone(); + value["artifacts"][0]["path"] = Value::String(path.into()); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + let mut value = original; + value["unexpected"] = Value::Bool(true); + assert!(validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err()); +} + +fn rendered_manifest() -> (tempfile::TempDir, Manifest) { + let temp = release_fixture(); + let text = render_manifest(evidence(), temp.path()).unwrap(); + let manifest = validate_manifest_bytes(text.as_bytes()).unwrap(); + (temp, manifest) +} + +#[test] +fn live_semantic_drift_is_rejected_field_by_field() { + let (outside, manifest) = rendered_manifest(); + let reject = |candidate: &Manifest| { + assert!(validate_live(candidate, outside.path()).is_err()); + }; + + let mut candidate = manifest.clone(); + candidate.product = "other-observer".into(); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.version = "2.0.0".into(); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.source_commit = "0".repeat(40); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.cargo_lock_sha256 = "0".repeat(64); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.dependency_policy.cargo_deny_version = "0.20.3".into(); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.dependency_policy.deterministic_gate = "fail".into(); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.source_dirty = true; + reject(&candidate); + + let mut candidate = manifest.clone(); + candidate.active_exceptions.pop(); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.active_exceptions.push("RUSTSEC-2026-9999".into()); + reject(&candidate); + let mut candidate = manifest.clone(); + candidate.active_exceptions[0] = "RUSTSEC-2026-9998".into(); + reject(&candidate); + + for target in [ + TargetEvidence::Compiled { + triple: "aarch64-unknown-linux-gnu".into(), + profile: "release".into(), + features: vec![], + }, + TargetEvidence::Compiled { + triple: TARGET_TRIPLE.into(), + profile: "debug".into(), + features: vec![], + }, + TargetEvidence::Compiled { + triple: TARGET_TRIPLE.into(), + profile: "release".into(), + features: vec!["foo".into()], + }, + TargetEvidence::Source, + ] { + let mut candidate = manifest.clone(); + candidate.target = target; + reject(&candidate); + } +} + +#[test] +fn native_tool_exact_identity_and_digest_mutations_fail() { + let (temp, _) = rendered_manifest(); + let original: Value = + serde_json::from_str(&render_manifest(evidence(), temp.path()).unwrap()).unwrap(); + let reject = |mutated: &Value| { + assert!( + validate_manifest_bytes(serde_json::to_string(mutated).unwrap().as_bytes()).is_err() + ); + }; + + for key in ["unknown_tool", "cargo_deny"] { + let mut value = original.clone(); + value["native_tools"][key] = Value::String("tool 1.0".into()); + reject(&value); + } + for (key, wrong) in [ + ("cargo_deb", "3.7.1"), + ("cargo_generate_rpm", "0.21.1"), + ("manifest_validator", "9.9.9"), + ("signing_mode", "signed"), + ] { + let mut value = original.clone(); + value["native_tools"][key] = Value::String(wrong.into()); + reject(&value); + } + for key in [ + "container_engine", + "ubuntu_os", + "ubuntu_rustc", + "ubuntu_cargo", + "ubuntu_compiler", + "ubuntu_linker", + "ubuntu_glibc", + "ubuntu_tar", + "ubuntu_gzip", + "fedora_os", + "dpkg_deb", + "rpm", + ] { + let mut value = original.clone(); + value["native_tools"][key] = Value::String("wrong-tool 1.0".into()); + reject(&value); + } + for key in ["ubuntu_image_digest", "fedora_image_digest"] { + for malformed in [ + "A".repeat(64), + "a".repeat(63), + "a".repeat(65), + format!("{}g", "a".repeat(63)), + ] { + let mut value = original.clone(); + value["native_tools"][key] = Value::String(malformed); + reject(&value); + } + } +} + +#[test] +fn artifact_file_path_and_checksum_mutations_fail() { + let (temp, manifest) = rendered_manifest(); + fs::write( + temp.path().join(MANIFEST_NAME), + serde_json::to_string_pretty(&manifest).unwrap() + "\n", + ) + .unwrap(); + let sums = render_sha256sums(&manifest.artifacts).unwrap(); + fs::write(temp.path().join(CHECKSUM_NAME), &sums).unwrap(); + + let artifact_path = temp.path().join(&manifest.artifacts[0].path); + fs::write(&artifact_path, b"mutated after render").unwrap(); + assert!(verify_artifacts(&manifest, temp.path()).is_err()); + assert!( + verify_checksums(&manifest, temp.path()).is_err() + || verify_artifacts(&manifest, temp.path()).is_err() + ); + + let (missing, missing_manifest) = rendered_manifest(); + fs::remove_file(missing.path().join(&missing_manifest.artifacts[0].path)).unwrap(); + assert!(verify_artifacts(&missing_manifest, missing.path()).is_err()); + assert!(classify_release(missing.path(), false).is_err()); + + let (linked, linked_manifest) = rendered_manifest(); + let linked_path = linked.path().join(&linked_manifest.artifacts[0].path); + let target = linked.path().join("link-target"); + fs::rename(&linked_path, &target).unwrap(); + symlink(&target, &linked_path).unwrap(); + assert!(verify_artifacts(&linked_manifest, linked.path()).is_err()); + + let variants = { + let lines = sums.lines().collect::>(); + let uppercase = format!("{}\n", sums.to_ascii_uppercase().trim_end()); + let short = format!( + "{}\n", + lines + .iter() + .map(|line| &line[1..]) + .collect::>() + .join("\n") + ); + let duplicate = format!("{}\n{}\n", sums.trim_end(), lines[0]); + let missing = format!("{}\n", lines[..2].join("\n")); + let extra = format!("{}{} extra.rpm\n", sums, "c".repeat(64)); + let crlf = sums.replace('\n', "\r\n"); + let no_final_lf = sums.trim_end_matches('\n').to_owned(); + [ + uppercase, + short, + duplicate, + missing, + extra, + crlf, + no_final_lf, + ] + }; + for variant in variants { + fs::write(temp.path().join(CHECKSUM_NAME), variant).unwrap(); + assert!(verify_checksums(&manifest, temp.path()).is_err()); + } + + let mut directory_path = manifest.artifacts.clone(); + directory_path[0].path = "sub/file.tar.gz".into(); + assert!(validate_artifact_set(&directory_path).is_err()); + assert!(validate_artifact_set(&manifest.artifacts[..2]).is_err()); +} + +#[test] +fn schema_target_commit_and_datetime_boundaries_are_enforced() { + let (temp, _) = rendered_manifest(); + let original: Value = + serde_json::from_str(&render_manifest(evidence(), temp.path()).unwrap()).unwrap(); + + let mut source = original.clone(); + source["target"] = serde_json::json!({"kind":"source"}); + assert!(validate_manifest_bytes(serde_json::to_string(&source).unwrap().as_bytes()).is_ok()); + + for target in [ + serde_json::json!({"kind":"compiled","triple":TARGET_TRIPLE,"profile":"release"}), + serde_json::json!({"kind":"source","triple":TARGET_TRIPLE}), + ] { + let mut value = original.clone(); + value["target"] = target; + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + + let mut commit64 = original.clone(); + commit64["source_commit"] = Value::String("a".repeat(64)); + assert!(validate_manifest_bytes(serde_json::to_string(&commit64).unwrap().as_bytes()).is_ok()); + for commit in ["a".repeat(39), "a".repeat(41), "A".repeat(40)] { + let mut value = original.clone(); + value["source_commit"] = Value::String(commit); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } + + for timestamp in ["2026-07-20T12:34:56z", "2026-07-20T12:34:56+01:00"] { + let mut value = original.clone(); + value["dependency_policy"]["advisory_checked_at"] = Value::String(timestamp.into()); + assert!( + validate_manifest_bytes(serde_json::to_string(&value).unwrap().as_bytes()).is_err() + ); + } +} + +#[test] +fn manifest_mode_success_message_disclaims_candidate_readiness() { + assert!(MANIFEST_OK_MESSAGE.contains("NOT candidate-readiness classification")); +} diff --git a/deny.toml b/deny.toml index 3df9399..e660d44 100644 --- a/deny.toml +++ b/deny.toml @@ -17,9 +17,11 @@ allow = [ "CDLA-Permissive-2.0", "ISC", "MIT", + "MIT-0", "MPL-2.0", "Unicode-3.0", "Unlicense", + "Zlib", ] confidence-threshold = 0.8 diff --git a/vendor/rust-release-manifest/rust-release-manifest.schema.json b/vendor/rust-release-manifest/rust-release-manifest.schema.json new file mode 100644 index 0000000..9351de1 --- /dev/null +++ b/vendor/rust-release-manifest/rust-release-manifest.schema.json @@ -0,0 +1,184 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://solpbc.org/schemas/rust-release-manifest/v1.json", + "title": "sol pbc Rust release manifest", + "description": "Fleet-readable provenance for final distributed Rust artifacts.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "product", + "version", + "source_commit", + "source_dirty", + "cargo_lock_sha256", + "rust", + "target", + "native_tools", + "dependency_policy", + "active_exceptions", + "artifacts" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "product": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "source_commit": { + "type": "string", + "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" + }, + "source_dirty": { + "const": false + }, + "cargo_lock_sha256": { + "$ref": "#/$defs/sha256" + }, + "rust": { + "type": "object", + "additionalProperties": false, + "required": [ + "rustc_verbose", + "cargo_version" + ], + "properties": { + "rustc_verbose": { + "type": "string", + "minLength": 1 + }, + "cargo_version": { + "type": "string", + "minLength": 1 + } + } + }, + "target": { + "description": "One homogeneous compiled-target lane or one Rust source distribution.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "triple", + "profile", + "features" + ], + "properties": { + "kind": { + "const": "compiled" + }, + "triple": { + "type": "string", + "minLength": 1 + }, + "profile": { + "type": "string", + "minLength": 1 + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "const": "source" + } + } + } + ] + }, + "native_tools": { + "description": "Release-affecting native SDK, compiler, linker, packager, signer, and container versions.", + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "dependency_policy": { + "type": "object", + "additionalProperties": false, + "required": [ + "cargo_deny_version", + "deterministic_gate", + "advisory_checked_at" + ], + "properties": { + "cargo_deny_version": { + "type": "string", + "minLength": 1 + }, + "deterministic_gate": { + "const": "pass" + }, + "advisory_checked_at": { + "type": "string", + "format": "date-time" + } + } + }, + "active_exceptions": { + "description": "Repository-local dependency or standard exception identifiers active for this release.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "artifacts": { + "description": "Final distributed bytes after signing, notarization, or repacking.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "sha256", + "bytes" + ], + "properties": { + "path": { + "description": "POSIX relative path from the companion manifest's directory to the final artifact.", + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\]+$" + }, + "sha256": { + "$ref": "#/$defs/sha256" + }, + "bytes": { + "type": "integer", + "minimum": 1 + } + } + } + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + } + } +} -- 2.51.2