diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bb5fb79 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1809 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "ca-certs" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "clap_complete", + "clap_mangen", + "reqwest", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "clap_mangen" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cee52ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ca-certs" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +base64 = "0.22.1" +clap = { version = "4.5.60", features = ["derive"] } +clap_complete = "4.5.66" +clap_mangen = "0.2.31" +reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "rustls"] } + +[lints.rust] +warnings = "deny" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..231004c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 SFG545 and ReallyUnusual + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..104ae84 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# ca-certs + +`ca-certs` is a Linux CLI for managing CA trust store sources and extracted bundles in a `p11-kit` / `trust` (`update-ca-trust`-style) layout. + +It supports: + +- adding PEM CA certificates to `trust-source/anchors` +- regenerating extracted trust outputs (PEM bundles, OpenSSL bundle, Java cacerts, hash dir) +- syncing `/etc/ssl/certs` symlinks for the standard extraction path +- fetching/parsing/converting Mozilla NSS `certdata.txt` into a `p11-kit` trust source +- operating on an alternate root filesystem with `--root` (image/chroot builds) + +## Requirements + +- Linux (the tool exits on non-Linux targets) +- `trust` (from `p11-kit`) available in `PATH` +- `openssl` available in `PATH` for `certdata convert` / `certdata sync` +- `chroot` available in `PATH` when using `--root` with a non-`/` rootfs +- permission to write system trust paths (typically root) + +## Build + +### Cargo + +```bash +cargo build --release +``` + +Binary output: + +```text +target/release/ca-certs +``` + +### Meson (packaging-friendly) + +Builds the Rust binary and installs the man page + shell completions from `contrib/`. + +```bash +meson setup build +meson compile -C build +meson install -C build +``` + +## What It Manages + +Default source and output paths mirror common `update-ca-trust` / `p11-kit` layouts: + +- anchors: `/etc/ca-certificates/trust-source/anchors` +- extracted output dir: `/etc/ca-certificates/extracted` +- OpenSSL cert symlink dir: `/etc/ssl/certs` +- Java cacerts symlink: `/etc/ssl/certs/java/cacerts` +- Mozilla trust source (for `certdata convert/sync`): `/usr/share/ca-certificates/trust-source/mozilla.trust.p11-kit` + +`extract` generates: + +- `tls-ca-bundle.pem` +- `email-ca-bundle.pem` +- `objsign-ca-bundle.pem` +- `ca-bundle.trust.crt` +- `edk2-cacerts.bin` +- `java-cacerts.jks` +- `cadir/` (hashed cert directory) + +## Usage + +Show help: + +```bash +ca-certs --help +ca-certs add --help +ca-certs extract --help +ca-certs certdata --help +``` + +### Add a CA certificate + +Adds a PEM certificate to the anchors directory and refreshes extracted outputs by default. + +```bash +sudo ca-certs add /path/to/company-root.pem +``` + +Specify a custom anchor name: + +```bash +sudo ca-certs add /path/to/company-root.pem --name company-root +``` + +Add without extraction: + +```bash +sudo ca-certs add /path/to/company-root.pem --no-extract +``` + +Dry-run: + +```bash +ca-certs add /path/to/company-root.pem --dry-run +``` + +### Regenerate extracted trust outputs + +```bash +sudo ca-certs extract +``` + +Write extracted outputs to a custom directory (skips `/etc/ssl/certs` symlink sync for custom output): + +```bash +sudo ca-certs extract --output /tmp/ca-extracted +``` + +### Work on a rootfs/chroot image + +```bash +sudo ca-certs extract --root /mnt/image +sudo ca-certs add /path/to/company-root.pem --root /mnt/image +``` + +## Mozilla `certdata.txt` workflow + +The `certdata` command supports a fetch/parse/convert/sync pipeline for Mozilla NSS trust data. + +Fetch `certdata.txt`: + +```bash +ca-certs certdata fetch --output certdata.txt +``` + +Parse and print a summary: + +```bash +ca-certs certdata parse certdata.txt +``` + +Convert to a `p11-kit` trust source and extract outputs: + +```bash +sudo ca-certs certdata convert certdata.txt +``` + +Run the full pipeline (fetch -> convert -> extract): + +```bash +sudo ca-certs certdata sync +``` + +Use `--dry-run` on these commands to preview actions without modifying files. + +## Development + +Run tests: + +```bash +cargo test +``` + +The repository also includes a generated man page and shell completions under `contrib/`. + +## License + +MIT. See `LICENSE`. diff --git a/contrib/ca-certs.8 b/contrib/ca-certs.8 new file mode 100644 index 0000000..1744559 --- /dev/null +++ b/contrib/ca-certs.8 @@ -0,0 +1,31 @@ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH ca-certs 8 "ca-certs 0.1.0" +.SH NAME +ca\-certs \- Manage Linux CA trust store sources and extracted bundles (p11\-kit/trust) +.SH SYNOPSIS +\fBca\-certs\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIsubcommands\fR> +.SH DESCRIPTION +Manage Linux CA trust store sources and extracted bundles (p11\-kit/trust) +.SH OPTIONS +.TP +\fB\-h\fR, \fB\-\-help\fR +Print help +.TP +\fB\-V\fR, \fB\-\-version\fR +Print version +.SH SUBCOMMANDS +.TP +ca\-certs\-add(8) +Add a PEM CA certificate to trust\-source/anchors and refresh extracted outputs +.TP +ca\-certs\-extract(8) +Regenerate extracted trust bundles (like update\-ca\-trust extract) +.TP +ca\-certs\-certdata(8) +Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet) +.TP +ca\-certs\-help(8) +Print this message or the help of the given subcommand(s) +.SH VERSION +v0.1.0 diff --git a/contrib/completions/_ca-certs b/contrib/completions/_ca-certs new file mode 100644 index 0000000..2220bb6 --- /dev/null +++ b/contrib/completions/_ca-certs @@ -0,0 +1,401 @@ +#compdef ca-certs + +autoload -U is-at-least + +_ca-certs() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state line + _arguments "${_arguments_options[@]}" : \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +":: :_ca-certs_commands" \ +"*::: :->ca-certs" \ +&& ret=0 + case $state in + (ca-certs) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ca-certs-command-$line[1]:" + case $line[1] in + (add) +_arguments "${_arguments_options[@]}" : \ +'-n+[Output certificate name (without extension). Defaults to the input filename stem]:NAME:_default' \ +'--name=[Output certificate name (without extension). Defaults to the input filename stem]:NAME:_default' \ +'-o+[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:OUTPUT:_files' \ +'--output=[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:OUTPUT:_files' \ +'--root=[Target root filesystem (for chroot/image builds)]:ROOT:_files' \ +'--force[Overwrite an existing anchor if contents differ]' \ +'--no-extract[Add certificate but do not run extraction]' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +':cert -- Path to a PEM-encoded CA certificate:_files' \ +&& ret=0 +;; +(extract) +_arguments "${_arguments_options[@]}" : \ +'-o+[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:OUTPUT:_files' \ +'--output=[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:OUTPUT:_files' \ +'--root=[Target root filesystem (for chroot/image builds)]:ROOT:_files' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +&& ret=0 +;; +(certdata) +_arguments "${_arguments_options[@]}" : \ +'-h[Print help]' \ +'--help[Print help]' \ +":: :_ca-certs__certdata_commands" \ +"*::: :->certdata" \ +&& ret=0 + + case $state in + (certdata) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ca-certs-certdata-command-$line[1]:" + case $line[1] in + (fetch) +_arguments "${_arguments_options[@]}" : \ +'--url=[Source URL for certdata.txt]:URL:_default' \ +'--log-url=[Override log URL used to determine the latest revision (optional)]:LOG_URL:_default' \ +'-o+[Output file path]:OUTPUT:_files' \ +'--output=[Output file path]:OUTPUT:_files' \ +'--force[Overwrite an existing file if contents differ]' \ +'--no-revision-check[Always download even if the local file revision matches the remote revision]' \ +'--no-parse[Skip parsing/summary after download]' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +&& ret=0 +;; +(parse) +_arguments "${_arguments_options[@]}" : \ +'--limit=[Print up to N object summaries after the aggregate stats]:LIMIT:_default' \ +'-h[Print help]' \ +'--help[Print help]' \ +'::input -- Path to certdata.txt:_files' \ +&& ret=0 +;; +(convert) +_arguments "${_arguments_options[@]}" : \ +'--root=[Target root filesystem (for chroot/image builds)]:ROOT:_files' \ +'--output=[Destination p11-kit trust source file inside the target root]:OUTPUT:_files' \ +'-o+[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:EXTRACT_OUTPUT:_files' \ +'--extract-output=[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:EXTRACT_OUTPUT:_files' \ +'--force[Overwrite an existing output file if contents differ]' \ +'--no-extract[Skip running trust extraction after writing the Mozilla source bundle]' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'::input -- Path to certdata.txt:_files' \ +&& ret=0 +;; +(sync) +_arguments "${_arguments_options[@]}" : \ +'--url=[Source URL for certdata.txt]:URL:_default' \ +'--log-url=[Override log URL used to determine the latest revision (optional)]:LOG_URL:_default' \ +'--certdata-output=[Local certdata.txt path used for fetch and convert]:CERTDATA_OUTPUT:_files' \ +'--root=[Target root filesystem (for chroot/image builds)]:ROOT:_files' \ +'--mozilla-output=[Destination p11-kit trust source file inside the target root]:MOZILLA_OUTPUT:_files' \ +'-o+[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:EXTRACT_OUTPUT:_files' \ +'--extract-output=[Extracted output directory inside the target root (default\: /etc/ca-certificates/extracted)]:EXTRACT_OUTPUT:_files' \ +'--force[Overwrite existing files if contents differ]' \ +'--no-revision-check[Always download even if the local certdata revision matches the remote revision]' \ +'--no-parse[Skip parsing/summary after download]' \ +'--no-extract[Skip running trust extraction after writing the Mozilla source bundle]' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" : \ +":: :_ca-certs__certdata__help_commands" \ +"*::: :->help" \ +&& ret=0 + + case $state in + (help) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ca-certs-certdata-help-command-$line[1]:" + case $line[1] in + (fetch) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(parse) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(convert) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(sync) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; + esac + ;; +esac +;; + esac + ;; +esac +;; +(gen-artifacts) +_arguments "${_arguments_options[@]}" : \ +'--out-dir=[Directory to write generated packaging artifacts into]:OUT_DIR:_files' \ +'*--shell=[Shell completions to generate]:SHELLS:(bash fish zsh)' \ +'--dry-run[Print actions without modifying files]' \ +'-h[Print help]' \ +'--help[Print help]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" : \ +":: :_ca-certs__help_commands" \ +"*::: :->help" \ +&& ret=0 + + case $state in + (help) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ca-certs-help-command-$line[1]:" + case $line[1] in + (add) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(extract) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(certdata) +_arguments "${_arguments_options[@]}" : \ +":: :_ca-certs__help__certdata_commands" \ +"*::: :->certdata" \ +&& ret=0 + + case $state in + (certdata) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ca-certs-help-certdata-command-$line[1]:" + case $line[1] in + (fetch) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(parse) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(convert) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(sync) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; + esac + ;; +esac +;; +(gen-artifacts) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; + esac + ;; +esac +;; + esac + ;; +esac +} + +(( $+functions[_ca-certs_commands] )) || +_ca-certs_commands() { + local commands; commands=( +'add:Add a PEM CA certificate to trust-source/anchors and refresh extracted outputs' \ +'extract:Regenerate extracted trust bundles (like update-ca-trust extract)' \ +'certdata:Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet)' \ +'gen-artifacts:' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ca-certs commands' commands "$@" +} +(( $+functions[_ca-certs__add_commands] )) || +_ca-certs__add_commands() { + local commands; commands=() + _describe -t commands 'ca-certs add commands' commands "$@" +} +(( $+functions[_ca-certs__certdata_commands] )) || +_ca-certs__certdata_commands() { + local commands; commands=( +'fetch:Fetch certdata.txt from an NSS source URL' \ +'parse:Parse a local certdata.txt file and print a summary' \ +'convert:Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' \ +'sync:Run the full pipeline\: fetch certdata.txt, convert to p11-kit source, then extract outputs' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ca-certs certdata commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__convert_commands] )) || +_ca-certs__certdata__convert_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata convert commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__fetch_commands] )) || +_ca-certs__certdata__fetch_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata fetch commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help_commands] )) || +_ca-certs__certdata__help_commands() { + local commands; commands=( +'fetch:Fetch certdata.txt from an NSS source URL' \ +'parse:Parse a local certdata.txt file and print a summary' \ +'convert:Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' \ +'sync:Run the full pipeline\: fetch certdata.txt, convert to p11-kit source, then extract outputs' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ca-certs certdata help commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help__convert_commands] )) || +_ca-certs__certdata__help__convert_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata help convert commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help__fetch_commands] )) || +_ca-certs__certdata__help__fetch_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata help fetch commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help__help_commands] )) || +_ca-certs__certdata__help__help_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata help help commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help__parse_commands] )) || +_ca-certs__certdata__help__parse_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata help parse commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__help__sync_commands] )) || +_ca-certs__certdata__help__sync_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata help sync commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__parse_commands] )) || +_ca-certs__certdata__parse_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata parse commands' commands "$@" +} +(( $+functions[_ca-certs__certdata__sync_commands] )) || +_ca-certs__certdata__sync_commands() { + local commands; commands=() + _describe -t commands 'ca-certs certdata sync commands' commands "$@" +} +(( $+functions[_ca-certs__extract_commands] )) || +_ca-certs__extract_commands() { + local commands; commands=() + _describe -t commands 'ca-certs extract commands' commands "$@" +} +(( $+functions[_ca-certs__gen-artifacts_commands] )) || +_ca-certs__gen-artifacts_commands() { + local commands; commands=() + _describe -t commands 'ca-certs gen-artifacts commands' commands "$@" +} +(( $+functions[_ca-certs__help_commands] )) || +_ca-certs__help_commands() { + local commands; commands=( +'add:Add a PEM CA certificate to trust-source/anchors and refresh extracted outputs' \ +'extract:Regenerate extracted trust bundles (like update-ca-trust extract)' \ +'certdata:Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet)' \ +'gen-artifacts:' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ca-certs help commands' commands "$@" +} +(( $+functions[_ca-certs__help__add_commands] )) || +_ca-certs__help__add_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help add commands' commands "$@" +} +(( $+functions[_ca-certs__help__certdata_commands] )) || +_ca-certs__help__certdata_commands() { + local commands; commands=( +'fetch:Fetch certdata.txt from an NSS source URL' \ +'parse:Parse a local certdata.txt file and print a summary' \ +'convert:Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' \ +'sync:Run the full pipeline\: fetch certdata.txt, convert to p11-kit source, then extract outputs' \ + ) + _describe -t commands 'ca-certs help certdata commands' commands "$@" +} +(( $+functions[_ca-certs__help__certdata__convert_commands] )) || +_ca-certs__help__certdata__convert_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help certdata convert commands' commands "$@" +} +(( $+functions[_ca-certs__help__certdata__fetch_commands] )) || +_ca-certs__help__certdata__fetch_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help certdata fetch commands' commands "$@" +} +(( $+functions[_ca-certs__help__certdata__parse_commands] )) || +_ca-certs__help__certdata__parse_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help certdata parse commands' commands "$@" +} +(( $+functions[_ca-certs__help__certdata__sync_commands] )) || +_ca-certs__help__certdata__sync_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help certdata sync commands' commands "$@" +} +(( $+functions[_ca-certs__help__extract_commands] )) || +_ca-certs__help__extract_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help extract commands' commands "$@" +} +(( $+functions[_ca-certs__help__gen-artifacts_commands] )) || +_ca-certs__help__gen-artifacts_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help gen-artifacts commands' commands "$@" +} +(( $+functions[_ca-certs__help__help_commands] )) || +_ca-certs__help__help_commands() { + local commands; commands=() + _describe -t commands 'ca-certs help help commands' commands "$@" +} + +if [ "$funcstack[1]" = "_ca-certs" ]; then + _ca-certs "$@" +else + compdef _ca-certs ca-certs +fi diff --git a/contrib/completions/ca-certs.bash b/contrib/completions/ca-certs.bash new file mode 100644 index 0000000..24e6e5d --- /dev/null +++ b/contrib/completions/ca-certs.bash @@ -0,0 +1,558 @@ +_ca-certs() { + local i cur prev opts cmd + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + cur="$2" + else + cur="${COMP_WORDS[COMP_CWORD]}" + fi + prev="$3" + cmd="" + opts="" + + for i in "${COMP_WORDS[@]:0:COMP_CWORD}" + do + case "${cmd},${i}" in + ",$1") + cmd="ca__certs" + ;; + ca__certs,add) + cmd="ca__certs__add" + ;; + ca__certs,certdata) + cmd="ca__certs__certdata" + ;; + ca__certs,extract) + cmd="ca__certs__extract" + ;; + ca__certs,gen-artifacts) + cmd="ca__certs__gen__artifacts" + ;; + ca__certs,help) + cmd="ca__certs__help" + ;; + ca__certs__certdata,convert) + cmd="ca__certs__certdata__convert" + ;; + ca__certs__certdata,fetch) + cmd="ca__certs__certdata__fetch" + ;; + ca__certs__certdata,help) + cmd="ca__certs__certdata__help" + ;; + ca__certs__certdata,parse) + cmd="ca__certs__certdata__parse" + ;; + ca__certs__certdata,sync) + cmd="ca__certs__certdata__sync" + ;; + ca__certs__certdata__help,convert) + cmd="ca__certs__certdata__help__convert" + ;; + ca__certs__certdata__help,fetch) + cmd="ca__certs__certdata__help__fetch" + ;; + ca__certs__certdata__help,help) + cmd="ca__certs__certdata__help__help" + ;; + ca__certs__certdata__help,parse) + cmd="ca__certs__certdata__help__parse" + ;; + ca__certs__certdata__help,sync) + cmd="ca__certs__certdata__help__sync" + ;; + ca__certs__help,add) + cmd="ca__certs__help__add" + ;; + ca__certs__help,certdata) + cmd="ca__certs__help__certdata" + ;; + ca__certs__help,extract) + cmd="ca__certs__help__extract" + ;; + ca__certs__help,gen-artifacts) + cmd="ca__certs__help__gen__artifacts" + ;; + ca__certs__help,help) + cmd="ca__certs__help__help" + ;; + ca__certs__help__certdata,convert) + cmd="ca__certs__help__certdata__convert" + ;; + ca__certs__help__certdata,fetch) + cmd="ca__certs__help__certdata__fetch" + ;; + ca__certs__help__certdata,parse) + cmd="ca__certs__help__certdata__parse" + ;; + ca__certs__help__certdata,sync) + cmd="ca__certs__help__certdata__sync" + ;; + *) + ;; + esac + done + + case "${cmd}" in + ca__certs) + opts="-h -V --help --version add extract certdata gen-artifacts help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__add) + opts="-n -o -h --name --force --no-extract --output --root --dry-run --help " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --name) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -n) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -o) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --root) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata) + opts="-h --help fetch parse convert sync help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__convert) + opts="-o -h --root --output --force --no-extract --extract-output --dry-run --help [INPUT]" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --root) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --extract-output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -o) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__fetch) + opts="-o -h --url --log-url --output --force --no-revision-check --no-parse --dry-run --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --url) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --log-url) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -o) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help) + opts="fetch parse convert sync help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help__convert) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help__fetch) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help__help) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help__parse) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__help__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__parse) + opts="-h --limit --help [INPUT]" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --limit) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__certdata__sync) + opts="-o -h --url --log-url --certdata-output --root --mozilla-output --force --no-revision-check --no-parse --no-extract --extract-output --dry-run --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --url) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --log-url) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --certdata-output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --root) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --mozilla-output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --extract-output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -o) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__extract) + opts="-o -h --output --root --dry-run --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --output) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -o) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --root) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__gen__artifacts) + opts="-h --out-dir --shell --dry-run --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --out-dir) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --shell) + COMPREPLY=($(compgen -W "bash fish zsh" -- "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help) + opts="add extract certdata gen-artifacts help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__add) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__certdata) + opts="fetch parse convert sync" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__certdata__convert) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__certdata__fetch) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__certdata__parse) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__certdata__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__extract) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__gen__artifacts) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ca__certs__help__help) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + esac +} + +if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then + complete -F _ca-certs -o nosort -o bashdefault -o default ca-certs +else + complete -F _ca-certs -o bashdefault -o default ca-certs +fi diff --git a/contrib/completions/ca-certs.fish b/contrib/completions/ca-certs.fish new file mode 100644 index 0000000..8f7189a --- /dev/null +++ b/contrib/completions/ca-certs.fish @@ -0,0 +1,99 @@ +# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. +function __fish_ca_certs_global_optspecs + string join \n h/help V/version +end + +function __fish_ca_certs_needs_command + # Figure out if the current invocation already has a command. + set -l cmd (commandline -opc) + set -e cmd[1] + argparse -s (__fish_ca_certs_global_optspecs) -- $cmd 2>/dev/null + or return + if set -q argv[1] + # Also print the command, so this can be used to figure out what it is. + echo $argv[1] + return 1 + end + return 0 +end + +function __fish_ca_certs_using_subcommand + set -l cmd (__fish_ca_certs_needs_command) + test -z "$cmd" + and return 1 + contains -- $cmd[1] $argv +end + +complete -c ca-certs -n "__fish_ca_certs_needs_command" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_needs_command" -s V -l version -d 'Print version' +complete -c ca-certs -n "__fish_ca_certs_needs_command" -f -a "add" -d 'Add a PEM CA certificate to trust-source/anchors and refresh extracted outputs' +complete -c ca-certs -n "__fish_ca_certs_needs_command" -f -a "extract" -d 'Regenerate extracted trust bundles (like update-ca-trust extract)' +complete -c ca-certs -n "__fish_ca_certs_needs_command" -f -a "certdata" -d 'Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet)' +complete -c ca-certs -n "__fish_ca_certs_needs_command" -f -a "gen-artifacts" +complete -c ca-certs -n "__fish_ca_certs_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -s n -l name -d 'Output certificate name (without extension). Defaults to the input filename stem' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -s o -l output -d 'Extracted output directory inside the target root (default: /etc/ca-certificates/extracted)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -l root -d 'Target root filesystem (for chroot/image builds)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -l force -d 'Overwrite an existing anchor if contents differ' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -l no-extract -d 'Add certificate but do not run extraction' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand add" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand extract" -s o -l output -d 'Extracted output directory inside the target root (default: /etc/ca-certificates/extracted)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand extract" -l root -d 'Target root filesystem (for chroot/image builds)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand extract" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand extract" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -f -a "fetch" -d 'Fetch certdata.txt from an NSS source URL' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -f -a "parse" -d 'Parse a local certdata.txt file and print a summary' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -f -a "convert" -d 'Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -f -a "sync" -d 'Run the full pipeline: fetch certdata.txt, convert to p11-kit source, then extract outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and not __fish_seen_subcommand_from fetch parse convert sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l url -d 'Source URL for certdata.txt' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l log-url -d 'Override log URL used to determine the latest revision (optional)' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -s o -l output -d 'Output file path' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l force -d 'Overwrite an existing file if contents differ' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l no-revision-check -d 'Always download even if the local file revision matches the remote revision' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l no-parse -d 'Skip parsing/summary after download' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from fetch" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from parse" -l limit -d 'Print up to N object summaries after the aggregate stats' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from parse" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -l root -d 'Target root filesystem (for chroot/image builds)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -l output -d 'Destination p11-kit trust source file inside the target root' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -s o -l extract-output -d 'Extracted output directory inside the target root (default: /etc/ca-certificates/extracted)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -l force -d 'Overwrite an existing output file if contents differ' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -l no-extract -d 'Skip running trust extraction after writing the Mozilla source bundle' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from convert" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l url -d 'Source URL for certdata.txt' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l log-url -d 'Override log URL used to determine the latest revision (optional)' -r +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l certdata-output -d 'Local certdata.txt path used for fetch and convert' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l root -d 'Target root filesystem (for chroot/image builds)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l mozilla-output -d 'Destination p11-kit trust source file inside the target root' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -s o -l extract-output -d 'Extracted output directory inside the target root (default: /etc/ca-certificates/extracted)' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l force -d 'Overwrite existing files if contents differ' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l no-revision-check -d 'Always download even if the local certdata revision matches the remote revision' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l no-parse -d 'Skip parsing/summary after download' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l no-extract -d 'Skip running trust extraction after writing the Mozilla source bundle' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from sync" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from help" -f -a "fetch" -d 'Fetch certdata.txt from an NSS source URL' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from help" -f -a "parse" -d 'Parse a local certdata.txt file and print a summary' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from help" -f -a "convert" -d 'Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from help" -f -a "sync" -d 'Run the full pipeline: fetch certdata.txt, convert to p11-kit source, then extract outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand certdata; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand gen-artifacts" -l out-dir -d 'Directory to write generated packaging artifacts into' -r -F +complete -c ca-certs -n "__fish_ca_certs_using_subcommand gen-artifacts" -l shell -d 'Shell completions to generate' -r -f -a "bash\t'' +fish\t'' +zsh\t''" +complete -c ca-certs -n "__fish_ca_certs_using_subcommand gen-artifacts" -l dry-run -d 'Print actions without modifying files' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand gen-artifacts" -s h -l help -d 'Print help' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and not __fish_seen_subcommand_from add extract certdata gen-artifacts help" -f -a "add" -d 'Add a PEM CA certificate to trust-source/anchors and refresh extracted outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and not __fish_seen_subcommand_from add extract certdata gen-artifacts help" -f -a "extract" -d 'Regenerate extracted trust bundles (like update-ca-trust extract)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and not __fish_seen_subcommand_from add extract certdata gen-artifacts help" -f -a "certdata" -d 'Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and not __fish_seen_subcommand_from add extract certdata gen-artifacts help" -f -a "gen-artifacts" +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and not __fish_seen_subcommand_from add extract certdata gen-artifacts help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and __fish_seen_subcommand_from certdata" -f -a "fetch" -d 'Fetch certdata.txt from an NSS source URL' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and __fish_seen_subcommand_from certdata" -f -a "parse" -d 'Parse a local certdata.txt file and print a summary' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and __fish_seen_subcommand_from certdata" -f -a "convert" -d 'Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs' +complete -c ca-certs -n "__fish_ca_certs_using_subcommand help; and __fish_seen_subcommand_from certdata" -f -a "sync" -d 'Run the full pipeline: fetch certdata.txt, convert to p11-kit source, then extract outputs' diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..1322976 --- /dev/null +++ b/meson.build @@ -0,0 +1,84 @@ +project( + 'ca-certs', + version: '0.1.0', + meson_version: '>=0.60.0', +) + +cargo = find_program('cargo', required: true) +sh = find_program('sh', required: true) + +# Track Rust inputs explicitly so Meson knows when the Cargo build output is stale. +tracked_rust_inputs = files( + 'Cargo.toml', + 'Cargo.lock', + 'src/main.rs', +) + +# Track packaging assets in one place so install rules stay auditable. +tracked_packaging_assets = files( + 'LICENSE', + 'contrib/ca-certs.8', + 'contrib/completions/ca-certs.bash', + 'contrib/completions/ca-certs.fish', + 'contrib/completions/_ca-certs', +) + +tracked_install_paths = [ + join_paths(get_option('bindir'), 'ca-certs'), + join_paths(get_option('datadir'), 'licenses', meson.project_name(), 'LICENSE'), + join_paths(get_option('mandir'), 'man8', 'ca-certs.8'), + join_paths(get_option('datadir'), 'bash-completion', 'completions', 'ca-certs'), + join_paths(get_option('datadir'), 'fish', 'vendor_completions.d', 'ca-certs.fish'), + join_paths(get_option('datadir'), 'zsh', 'site-functions', '_ca-certs'), +] + +cargo_target_dir = meson.current_build_dir() / 'cargo-target' + +ca_certs_bin = custom_target( + 'ca-certs-bin', + output: 'ca-certs', + depend_files: tracked_rust_inputs, + command: [ + sh, '-c', + 'set -eu; src="$1"; out="$2"; target_dir="$3"; cd "$src"; CARGO_TARGET_DIR="$target_dir" "$4" build --locked --release; cp "$target_dir/release/ca-certs" "$out"', + 'meson-cargo-build', + meson.project_source_root(), + '@OUTPUT@', + cargo_target_dir, + cargo, + ], + build_by_default: true, + console: true, + install: true, + install_dir: get_option('bindir'), +) + +install_data( + 'contrib/ca-certs.8', + install_dir: join_paths(get_option('mandir'), 'man8'), +) + +install_data( + 'contrib/completions/ca-certs.bash', + install_dir: join_paths(get_option('datadir'), 'bash-completion', 'completions'), + rename: 'ca-certs', +) + +install_data( + 'contrib/completions/ca-certs.fish', + install_dir: join_paths(get_option('datadir'), 'fish', 'vendor_completions.d'), +) + +install_data( + 'contrib/completions/_ca-certs', + install_dir: join_paths(get_option('datadir'), 'zsh', 'site-functions'), +) + +summary( + { + 'tracked rust inputs': tracked_rust_inputs.length(), + 'tracked packaging assets': tracked_packaging_assets.length(), + 'tracked install paths': '\n ' + '\n '.join(tracked_install_paths), + }, + section: 'Packaging', +) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9bcb0e3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,1888 @@ +use anyhow::{Context, Result, bail}; +use base64::Engine; +use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; +use std::collections::{BTreeMap, HashMap}; +use std::ffi::OsStr; +use std::fs; +use std::io::Write; +#[cfg(unix)] +use std::os::unix::fs::{PermissionsExt, symlink}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +// Default paths mirror the p11-kit / update-ca-trust layout used by this system. +const DEFAULT_ROOT: &str = "/"; +const DEFAULT_ANCHORS_DIR: &str = "/etc/ca-certificates/trust-source/anchors"; +const DEFAULT_EXTRACTED_DIR: &str = "/etc/ca-certificates/extracted"; +const DEFAULT_SSL_CERTS_DIR: &str = "/etc/ssl/certs"; +const DEFAULT_JAVA_CACERTS_LINK: &str = "/etc/ssl/certs/java/cacerts"; +const DEFAULT_CERTDATA_URL: &str = + "https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ckfw/builtins/certdata.txt"; +const DEFAULT_CERTDATA_OUTPUT: &str = "certdata.txt"; +const DEFAULT_MOZILLA_TRUST_P11KIT: &str = + "/usr/share/ca-certificates/trust-source/mozilla.trust.p11-kit"; + +// Top-level CLI entrypoint with `add` and `extract` subcommands. +#[derive(Parser, Debug)] +#[command( + version, + about = "Manage Linux CA trust store sources and extracted bundles (p11-kit/trust)" +)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Add a PEM CA certificate to trust-source/anchors and refresh extracted outputs + Add(AddArgs), + /// Regenerate extracted trust bundles (like update-ca-trust extract) + Extract(ExtractArgs), + /// Fetch and parse Mozilla/NSS certdata.txt (no certificate generation yet) + Certdata { + #[command(subcommand)] + command: CertdataCommands, + }, + #[command(hide = true)] + GenArtifacts(GenArtifactsArgs), +} + +// Common flags shared by subcommands. +#[derive(Args, Debug)] +struct CommonArgs { + /// Target root filesystem (for chroot/image builds) + #[arg(long, default_value = DEFAULT_ROOT)] + root: PathBuf, + + /// Print actions without modifying files + #[arg(long)] + dry_run: bool, +} + +// `add` installs a new anchor and optionally runs extraction. +#[derive(Args, Debug)] +struct AddArgs { + /// Path to a PEM-encoded CA certificate + cert: PathBuf, + + /// Output certificate name (without extension). Defaults to the input filename stem. + #[arg(short, long)] + name: Option, + + /// Overwrite an existing anchor if contents differ + #[arg(long)] + force: bool, + + /// Add certificate but do not run extraction + #[arg(long)] + no_extract: bool, + + /// Extracted output directory inside the target root (default: /etc/ca-certificates/extracted) + #[arg(short = 'o', long)] + output: Option, + + #[command(flatten)] + common: CommonArgs, +} + +// `extract` rebuilds derived trust outputs only. +#[derive(Args, Debug)] +struct ExtractArgs { + /// Extracted output directory inside the target root (default: /etc/ca-certificates/extracted) + #[arg(short = 'o', long)] + output: Option, + + #[command(flatten)] + common: CommonArgs, +} + +// `certdata` helper subcommands used for the future make-ca style pipeline. +#[derive(Subcommand, Debug)] +enum CertdataCommands { + /// Fetch certdata.txt from an NSS source URL + Fetch(CertdataFetchArgs), + /// Parse a local certdata.txt file and print a summary + Parse(CertdataParseArgs), + /// Convert certdata.txt into a Mozilla p11-kit trust bundle and optionally extract outputs + Convert(CertdataConvertArgs), + /// Run the full pipeline: fetch certdata.txt, convert to p11-kit source, then extract outputs + Sync(CertdataSyncArgs), +} + +#[derive(Args, Debug)] +struct CertdataFetchArgs { + /// Source URL for certdata.txt + #[arg(long, default_value = DEFAULT_CERTDATA_URL)] + url: String, + + /// Override log URL used to determine the latest revision (optional) + #[arg(long)] + log_url: Option, + + /// Output file path + #[arg(short, long, default_value = DEFAULT_CERTDATA_OUTPUT)] + output: PathBuf, + + /// Overwrite an existing file if contents differ + #[arg(long)] + force: bool, + + /// Always download even if the local file revision matches the remote revision + #[arg(long)] + no_revision_check: bool, + + /// Skip parsing/summary after download + #[arg(long = "no-parse", action = clap::ArgAction::SetFalse, default_value_t = true)] + parse: bool, + + /// Print actions without modifying files + #[arg(long)] + dry_run: bool, +} + +#[derive(Args, Debug)] +struct CertdataParseArgs { + /// Path to certdata.txt + #[arg(default_value = DEFAULT_CERTDATA_OUTPUT)] + input: PathBuf, + + /// Print up to N object summaries after the aggregate stats + #[arg(long, default_value_t = 5)] + limit: usize, +} + +#[derive(Args, Debug)] +struct CertdataConvertArgs { + /// Path to certdata.txt + #[arg(default_value = DEFAULT_CERTDATA_OUTPUT)] + input: PathBuf, + + /// Target root filesystem (for chroot/image builds) + #[arg(long, default_value = DEFAULT_ROOT)] + root: PathBuf, + + /// Destination p11-kit trust source file inside the target root + #[arg(long, default_value = DEFAULT_MOZILLA_TRUST_P11KIT)] + output: PathBuf, + + /// Overwrite an existing output file if contents differ + #[arg(long)] + force: bool, + + /// Skip running trust extraction after writing the Mozilla source bundle + #[arg(long)] + no_extract: bool, + + /// Extracted output directory inside the target root (default: /etc/ca-certificates/extracted) + #[arg(short = 'o', long)] + extract_output: Option, + + /// Print actions without modifying files + #[arg(long)] + dry_run: bool, +} + +#[derive(Args, Debug)] +struct CertdataSyncArgs { + /// Source URL for certdata.txt + #[arg(long, default_value = DEFAULT_CERTDATA_URL)] + url: String, + + /// Override log URL used to determine the latest revision (optional) + #[arg(long)] + log_url: Option, + + /// Local certdata.txt path used for fetch and convert + #[arg(long, default_value = DEFAULT_CERTDATA_OUTPUT)] + certdata_output: PathBuf, + + /// Target root filesystem (for chroot/image builds) + #[arg(long, default_value = DEFAULT_ROOT)] + root: PathBuf, + + /// Destination p11-kit trust source file inside the target root + #[arg(long, default_value = DEFAULT_MOZILLA_TRUST_P11KIT)] + mozilla_output: PathBuf, + + /// Overwrite existing files if contents differ + #[arg(long)] + force: bool, + + /// Always download even if the local certdata revision matches the remote revision + #[arg(long)] + no_revision_check: bool, + + /// Skip parsing/summary after download + #[arg(long = "no-parse", action = clap::ArgAction::SetFalse, default_value_t = true)] + parse: bool, + + /// Skip running trust extraction after writing the Mozilla source bundle + #[arg(long)] + no_extract: bool, + + /// Extracted output directory inside the target root (default: /etc/ca-certificates/extracted) + #[arg(short = 'o', long)] + extract_output: Option, + + /// Print actions without modifying files + #[arg(long)] + dry_run: bool, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] +enum CompletionShell { + Bash, + Fish, + Zsh, +} + +#[derive(Args, Debug)] +struct GenArtifactsArgs { + /// Directory to write generated packaging artifacts into + #[arg(long, default_value = "contrib")] + out_dir: PathBuf, + + /// Shell completions to generate + #[arg(long = "shell", value_enum, num_args = 1.., default_values_t = [CompletionShell::Bash, CompletionShell::Fish, CompletionShell::Zsh])] + shells: Vec, + + /// Print actions without modifying files + #[arg(long)] + dry_run: bool, +} + +// A single `trust extract` output we want to generate. +#[derive(Clone, Copy, Debug)] +struct ExtractJob { + format: &'static str, + filter: &'static str, + purpose: Option<&'static str>, + comment: bool, + relative_dest: &'static str, +} + +// Output set mirrors `/bin/update-ca-trust extract`. +const EXTRACT_JOBS: &[ExtractJob] = &[ + ExtractJob { + format: "pem-bundle", + filter: "ca-anchors", + purpose: Some("server-auth"), + comment: true, + relative_dest: "tls-ca-bundle.pem", + }, + ExtractJob { + format: "pem-bundle", + filter: "ca-anchors", + purpose: Some("email"), + comment: true, + relative_dest: "email-ca-bundle.pem", + }, + ExtractJob { + format: "pem-bundle", + filter: "ca-anchors", + purpose: Some("code-signing"), + comment: true, + relative_dest: "objsign-ca-bundle.pem", + }, + ExtractJob { + format: "openssl-bundle", + filter: "certificates", + purpose: None, + comment: true, + relative_dest: "ca-bundle.trust.crt", + }, + ExtractJob { + format: "edk2-cacerts", + filter: "ca-anchors", + purpose: Some("server-auth"), + comment: false, + relative_dest: "edk2-cacerts.bin", + }, + ExtractJob { + format: "java-cacerts", + filter: "ca-anchors", + purpose: Some("server-auth"), + comment: false, + relative_dest: "java-cacerts.jks", + }, + ExtractJob { + format: "pem-directory-hash", + filter: "ca-anchors", + purpose: Some("server-auth"), + comment: false, + relative_dest: "cadir", + }, +]; + +// Parsed representation of NSS certdata.txt. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CertDataDocument { + revision: Option, + objects: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CertDataObject { + attributes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CertDataAttribute { + name: String, + value_type: String, + value: CertDataValue, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum CertDataValue { + Inline(String), + Multiline(Vec), +} + +impl CertDataObject { + fn attr(&self, name: &str) -> Option<&CertDataAttribute> { + self.attributes.iter().find(|attr| attr.name == name) + } + + fn inline_attr_value(&self, name: &str) -> Option<&str> { + match self.attr(name) { + Some(CertDataAttribute { + value: CertDataValue::Inline(value), + .. + }) => Some(value.as_str()), + _ => None, + } + } + + fn multiline_attr_lines(&self, name: &str) -> Option<&[String]> { + match self.attr(name) { + Some(CertDataAttribute { + value: CertDataValue::Multiline(lines), + .. + }) => Some(lines.as_slice()), + _ => None, + } + } +} + +fn main() { + if let Err(err) = run() { + eprintln!("error: {err:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + // This tool manipulates Linux trust-store paths and relies on `trust`. + if !cfg!(target_os = "linux") { + bail!("this tool targets Linux trust stores"); + } + + let cli = Cli::parse(); + match cli.command { + Commands::Add(args) => run_add(args), + Commands::Extract(args) => run_extract(args), + Commands::Certdata { command } => run_certdata(command), + Commands::GenArtifacts(args) => run_gen_artifacts(args), + } +} + +fn run_certdata(command: CertdataCommands) -> Result<()> { + match command { + CertdataCommands::Fetch(args) => run_certdata_fetch(args), + CertdataCommands::Parse(args) => run_certdata_parse(args), + CertdataCommands::Convert(args) => run_certdata_convert(args), + CertdataCommands::Sync(args) => run_certdata_sync(args), + } +} + +fn run_certdata_sync(args: CertdataSyncArgs) -> Result<()> { + // Reuse the existing subcommand implementations so behavior stays aligned. + run_certdata_fetch(CertdataFetchArgs { + url: args.url, + log_url: args.log_url, + output: args.certdata_output.clone(), + force: args.force, + no_revision_check: args.no_revision_check, + parse: args.parse, + dry_run: args.dry_run, + })?; + + run_certdata_convert(CertdataConvertArgs { + input: args.certdata_output, + root: args.root, + output: args.mozilla_output, + force: args.force, + no_extract: args.no_extract, + extract_output: args.extract_output, + dry_run: args.dry_run, + }) +} + +fn run_gen_artifacts(args: GenArtifactsArgs) -> Result<()> { + let man_path = args.out_dir.join("ca-certs.8"); + let completions_dir = args.out_dir.join("completions"); + + println!( + "Generating packaging artifacts in {}", + args.out_dir.display() + ); + println!("Man page: {}", man_path.display()); + println!("Completions dir: {}", completions_dir.display()); + + if args.dry_run { + println!("[dry-run] Would create {}", args.out_dir.display()); + println!("[dry-run] Would create {}", completions_dir.display()); + for shell in &args.shells { + println!( + "[dry-run] Would generate {} completion", + completion_shell_name(*shell) + ); + } + println!("[dry-run] Would generate man page ca-certs.8"); + return Ok(()); + } + + fs::create_dir_all(&args.out_dir) + .with_context(|| format!("failed to create {}", args.out_dir.display()))?; + fs::create_dir_all(&completions_dir) + .with_context(|| format!("failed to create {}", completions_dir.display()))?; + + let mut man_buf = Vec::new(); + clap_mangen::Man::new(Cli::command()) + .section("8") + .render(&mut man_buf) + .context("failed to render man page")?; + fs::write(&man_path, &man_buf) + .with_context(|| format!("failed to write {}", man_path.display()))?; + + for shell in args.shells { + let clap_shell = match shell { + CompletionShell::Bash => clap_complete::Shell::Bash, + CompletionShell::Fish => clap_complete::Shell::Fish, + CompletionShell::Zsh => clap_complete::Shell::Zsh, + }; + let mut cmd = Cli::command(); + let generated = + clap_complete::generate_to(clap_shell, &mut cmd, "ca-certs", &completions_dir) + .with_context(|| { + format!( + "failed to generate {} completion", + completion_shell_name(shell) + ) + })?; + println!("Generated completion: {}", generated.display()); + } + + println!("Generated man page: {}", man_path.display()); + Ok(()) +} + +fn completion_shell_name(shell: CompletionShell) -> &'static str { + match shell { + CompletionShell::Bash => "bash", + CompletionShell::Fish => "fish", + CompletionShell::Zsh => "zsh", + } +} + +fn run_add(args: AddArgs) -> Result<()> { + // Validate the input before touching the trust store. + let cert_bytes = fs::read(&args.cert) + .with_context(|| format!("failed to read certificate file {}", args.cert.display()))?; + ensure_pem_certificate(&cert_bytes)?; + + let cert_name = sanitize_name(args.name.as_deref().unwrap_or_else(|| { + args.cert + .file_stem() + .and_then(OsStr::to_str) + .unwrap_or("custom-ca") + })); + if cert_name.is_empty() { + bail!("certificate name resolved to an empty value"); + } + + // Anchors are source inputs; extracted bundles are generated from them. + let anchors_dir_target = PathBuf::from(DEFAULT_ANCHORS_DIR); + let anchors_dir_host = path_in_root(&args.common.root, &anchors_dir_target); + let anchor_file = anchors_dir_host.join(format!("{cert_name}.pem")); + + println!("Anchor dir: {}", anchors_dir_host.display()); + println!("Anchor file: {}", anchor_file.display()); + install_file(&anchor_file, &cert_bytes, args.force, args.common.dry_run)?; + + if args.no_extract { + println!("Skipping extraction (`--no-extract`)."); + return Ok(()); + } + + let output_target = args + .output + .unwrap_or_else(|| PathBuf::from(DEFAULT_EXTRACTED_DIR)); + extract_trust(&args.common.root, &output_target, args.common.dry_run)?; + println!("CA certificate added and trust store refreshed."); + Ok(()) +} + +fn run_extract(args: ExtractArgs) -> Result<()> { + let output_target = args + .output + .unwrap_or_else(|| PathBuf::from(DEFAULT_EXTRACTED_DIR)); + extract_trust(&args.common.root, &output_target, args.common.dry_run)?; + println!("Trust store extraction complete."); + Ok(()) +} + +fn run_certdata_fetch(args: CertdataFetchArgs) -> Result<()> { + println!("Fetching certdata.txt from {}", args.url); + println!("Output: {}", args.output.display()); + let log_url = args + .log_url + .clone() + .or_else(|| derive_hg_log_url(&args.url)) + .filter(|s| !s.is_empty()); + if let Some(log_url) = &log_url { + println!("Revision check URL: {log_url}"); + } else if !args.no_revision_check { + println!( + "Revision check URL: ; downloading unconditionally" + ); + } + + if args.dry_run { + println!( + "[dry-run] Would fetch {} -> {}", + args.url, + args.output.display() + ); + if let Some(log_url) = &log_url { + println!("[dry-run] Would check remote revision via {log_url}"); + } + return Ok(()); + } + + let client = reqwest::blocking::Client::builder() + .build() + .context("failed to build HTTP client")?; + let existing_revision = read_certdata_revision_from_path(&args.output) + .ok() + .flatten(); + + let mut remote_revision = None; + if !args.no_revision_check { + if let Some(log_url) = &log_url { + let log_html = http_get_text(&client, log_url)?; + remote_revision = extract_hg_revision_from_log_html(&log_html).ok(); + if let Some(rev) = &remote_revision { + println!("Remote revision: {rev}"); + if !args.force && existing_revision.as_deref() == Some(rev.as_str()) { + println!( + "No update required (local revision matches remote). Use --force to download anyway." + ); + return Ok(()); + } + } else { + println!("Could not parse revision from log page; continuing with download."); + } + } + } + + let raw_body = http_get_text(&client, &args.url)?; + let normalized_body = normalize_certdata_download(&raw_body, remote_revision.clone()); + let parsed = + parse_certdata(&normalized_body).context("downloaded certdata.txt failed to parse")?; + + if let (Some(old), Some(new)) = (existing_revision.as_deref(), parsed.revision.as_deref()) { + if old == new && !args.force { + println!( + "No update required (downloaded revision matches local file). Use --force to overwrite anyway." + ); + return Ok(()); + } + } + + if args.parse { + print_certdata_summary(&parsed, 5); + } + + install_file(&args.output, normalized_body.as_bytes(), args.force, false)?; + println!("certdata.txt fetched successfully."); + Ok(()) +} + +fn http_get_text(client: &reqwest::blocking::Client, url: &str) -> Result { + let response = client + .get(url) + .send() + .with_context(|| format!("failed to request {url}"))? + .error_for_status() + .with_context(|| format!("server returned an error for {url}"))?; + response.text().context("failed to read response body") +} + +fn derive_hg_log_url(raw_url: &str) -> Option { + if raw_url.contains("/raw-file/") { + Some(raw_url.replacen("/raw-file/", "/log/", 1)) + } else { + None + } +} + +fn extract_hg_revision_from_log_html(html: &str) -> Result { + // make-ca grabs the first `` line and takes the text before `<`, which is the changeset id. + for line in html.lines() { + let line = line.trim(); + if !line.contains("") { + continue; + } + let prefix = line.split('<').next().unwrap_or("").trim(); + if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_hexdigit()) { + return Ok(prefix.to_string()); + } + } + bail!("unable to find a changeset revision in hg log HTML") +} + +fn read_certdata_revision_from_path(path: &Path) -> Result> { + if !path.exists() { + return Ok(None); + } + let content = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + Ok(extract_certdata_revision_from_text(&content)) +} + +fn extract_certdata_revision_from_text(content: &str) -> Option { + content.lines().find_map(|line| { + let trimmed = line.trim_start(); + let rest = trimmed.strip_prefix('#')?.trim_start(); + let rev = rest.strip_prefix("Revision:")?.trim(); + if rev.is_empty() { + None + } else { + Some(rev.to_string()) + } + }) +} + +fn normalize_certdata_download(raw_body: &str, revision: Option) -> String { + // Ensure a single make-ca style `# Revision:` line exists near the top. + let normalized_lines: Vec<&str> = raw_body + .lines() + .filter(|line| { + let trimmed = line.trim_start(); + let Some(rest) = trimmed.strip_prefix('#') else { + return true; + }; + let rest = rest.trim_start(); + !rest.starts_with("Revision:") + }) + .collect(); + + let mut out = String::new(); + if let Some(rev) = revision { + out.push_str("# Revision:"); + out.push_str(&rev); + out.push('\n'); + } + out.push_str(&normalized_lines.join("\n")); + if raw_body.ends_with('\n') && !out.ends_with('\n') { + out.push('\n'); + } + out +} + +fn run_certdata_parse(args: CertdataParseArgs) -> Result<()> { + let content = fs::read_to_string(&args.input) + .with_context(|| format!("failed to read {}", args.input.display()))?; + let parsed = parse_certdata(&content) + .with_context(|| format!("failed to parse {}", args.input.display()))?; + print_certdata_summary(&parsed, args.limit); + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TrustUsage { + Trusted, + Distrusted, + Neutral, +} + +#[derive(Debug, Clone)] +struct MozillaCertEntry { + join_key: String, + label: String, + der_cert: Vec, + mozilla_policy: bool, + server_distrust_after: Option, + email_distrust_after: Option, +} + +#[derive(Debug, Clone, Copy)] +struct MozillaTrustEntry { + server_auth: TrustUsage, + email: TrustUsage, + code_signing: TrustUsage, +} + +fn run_certdata_convert(args: CertdataConvertArgs) -> Result<()> { + let output_target = args.output; + let output_host = path_in_root(&args.root, &output_target); + println!("Converting certdata: {}", args.input.display()); + println!("Output p11-kit (target path): {}", output_target.display()); + println!("Output p11-kit (host path): {}", output_host.display()); + + // Convert parsed NSS objects into the same p11-kit source style Arch ships. + let parsed = if args.dry_run && !args.input.exists() { + println!( + "[dry-run] Input {} does not exist yet; skipping parse counts", + args.input.display() + ); + None + } else { + let content = fs::read_to_string(&args.input) + .with_context(|| format!("failed to read {}", args.input.display()))?; + Some( + parse_certdata(&content) + .with_context(|| format!("failed to parse {}", args.input.display()))?, + ) + }; + + if args.dry_run { + if let Some(parsed) = &parsed { + let (certs, trusts) = count_cert_and_trust_objects(parsed); + println!("[dry-run] Parsed objects: {}", parsed.objects.len()); + println!( + "[dry-run] Would convert {certs} certificate objects with {trusts} trust objects" + ); + } + println!("[dry-run] Would write {}", output_host.display()); + } else { + let parsed = parsed.context("certdata parse result missing")?; + let p11kit = convert_certdata_to_p11kit_bundle(&parsed)?; + install_file(&output_host, p11kit.as_bytes(), args.force, false)?; + } + + if args.no_extract { + println!("Skipping extraction (`--no-extract`)."); + return Ok(()); + } + + let extract_output = args + .extract_output + .unwrap_or_else(|| PathBuf::from(DEFAULT_EXTRACTED_DIR)); + extract_trust(&args.root, &extract_output, args.dry_run)?; + println!("certdata conversion and extraction complete."); + Ok(()) +} + +fn count_cert_and_trust_objects(doc: &CertDataDocument) -> (usize, usize) { + let mut certs = 0usize; + let mut trusts = 0usize; + for obj in &doc.objects { + match obj.inline_attr_value("CKA_CLASS") { + Some("CKO_CERTIFICATE") => certs += 1, + Some("CKO_NSS_TRUST") => trusts += 1, + _ => {} + } + } + (certs, trusts) +} + +fn convert_certdata_to_p11kit_bundle(doc: &CertDataDocument) -> Result { + // Build separate maps for certificate payloads and trust metadata, then join by issuer+serial. + let mut certs = Vec::new(); + let mut trusts: HashMap = HashMap::new(); + + for obj in &doc.objects { + match obj.inline_attr_value("CKA_CLASS") { + Some("CKO_CERTIFICATE") => { + if let Some(entry) = parse_mozilla_cert_object(obj)? { + certs.push(entry); + } + } + Some("CKO_NSS_TRUST") => { + if let Some((key, trust)) = parse_mozilla_trust_object(obj)? { + trusts.insert(key, trust); + } + } + _ => {} + } + } + + let mut out = String::new(); + out.push_str("# Generated by ca-certs from Mozilla/NSS certdata.txt\n"); + if let Some(rev) = &doc.revision { + out.push_str("# Revision:"); + out.push_str(rev); + out.push('\n'); + } + out.push('\n'); + + let mut converted = 0usize; + let mut skipped_missing_trust = 0usize; + for cert in certs { + let Some(trust) = trusts.get(&cert.join_key).copied() else { + skipped_missing_trust += 1; + continue; + }; + let cert_pem = pem_encode("CERTIFICATE", &cert.der_cert); + let pubkey_pem = openssl_pubkey_pem_from_der(&cert.der_cert) + .with_context(|| format!("failed to extract public key for {}", cert.label))?; + let rendered = render_p11kit_cert_pair(&cert, &trust, &pubkey_pem, &cert_pem)?; + out.push_str(&rendered); + out.push('\n'); + converted += 1; + } + + println!("Converted Mozilla cert/trust pairs: {converted}"); + if skipped_missing_trust > 0 { + println!("Skipped certificates without matching trust object: {skipped_missing_trust}"); + } + + Ok(out) +} + +fn parse_mozilla_cert_object(obj: &CertDataObject) -> Result> { + let join_key = match cert_object_join_key(obj)? { + Some(key) => key, + None => return Ok(None), + }; + let der_cert = match obj.multiline_attr_lines("CKA_VALUE") { + Some(lines) => decode_certdata_octal_multiline(lines) + .context("failed to decode CKA_VALUE certificate DER")?, + None => return Ok(None), + }; + let label = obj + .inline_attr_value("CKA_LABEL") + .map(certdata_inline_unquote) + .unwrap_or_else(|| "".to_string()); + + Ok(Some(MozillaCertEntry { + join_key, + label, + der_cert, + mozilla_policy: parse_ck_bool(obj.inline_attr_value("CKA_NSS_MOZILLA_CA_POLICY")) + .unwrap_or(false), + server_distrust_after: parse_distrust_after_value( + obj.attr("CKA_NSS_SERVER_DISTRUST_AFTER"), + )?, + email_distrust_after: parse_distrust_after_value(obj.attr("CKA_NSS_EMAIL_DISTRUST_AFTER"))?, + })) +} + +fn parse_mozilla_trust_object(obj: &CertDataObject) -> Result> { + let join_key = match cert_object_join_key(obj)? { + Some(key) => key, + None => return Ok(None), + }; + Ok(Some(( + join_key, + MozillaTrustEntry { + server_auth: parse_trust_usage(obj.inline_attr_value("CKA_TRUST_SERVER_AUTH")), + email: parse_trust_usage(obj.inline_attr_value("CKA_TRUST_EMAIL_PROTECTION")), + code_signing: parse_trust_usage(obj.inline_attr_value("CKA_TRUST_CODE_SIGNING")), + }, + ))) +} + +fn cert_object_join_key(obj: &CertDataObject) -> Result> { + // issuer+serial matches how NSS trust objects reference certificate objects. + let issuer = match obj.multiline_attr_lines("CKA_ISSUER") { + Some(v) => decode_certdata_octal_multiline(v).context("failed to decode CKA_ISSUER")?, + None => return Ok(None), + }; + let serial = match obj.multiline_attr_lines("CKA_SERIAL_NUMBER") { + Some(v) => { + decode_certdata_octal_multiline(v).context("failed to decode CKA_SERIAL_NUMBER")? + } + None => return Ok(None), + }; + Ok(Some(format!( + "{}:{}", + hex_encode_lower(&issuer), + hex_encode_lower(&serial) + ))) +} + +fn parse_ck_bool(value: Option<&str>) -> Option { + match value?.trim() { + "CK_TRUE" => Some(true), + "CK_FALSE" => Some(false), + _ => None, + } +} + +fn parse_trust_usage(value: Option<&str>) -> TrustUsage { + match value.map(str::trim) { + Some("CKT_NSS_TRUSTED_DELEGATOR") => TrustUsage::Trusted, + Some("CKT_NSS_NOT_TRUSTED") => TrustUsage::Distrusted, + _ => TrustUsage::Neutral, + } +} + +fn parse_distrust_after_value(attr: Option<&CertDataAttribute>) -> Result> { + let Some(attr) = attr else { return Ok(None) }; + match (&*attr.value_type, &attr.value) { + ("CK_BBOOL", CertDataValue::Inline(v)) => match v.trim() { + "CK_FALSE" => Ok(Some("%00".to_string())), + _ => Ok(None), + }, + (t, CertDataValue::Multiline(lines)) if t.starts_with("MULTILINE_") => { + let bytes = decode_certdata_octal_multiline(lines)?; + Ok(Some(percent_encode_bytes(&bytes))) + } + _ => Ok(None), + } +} + +fn decode_certdata_octal_multiline(lines: &[String]) -> Result> { + let mut out = Vec::new(); + for line in lines { + decode_certdata_octal_line_into(line, &mut out)?; + } + Ok(out) +} + +fn decode_certdata_octal_line_into(line: &str, out: &mut Vec) -> Result<()> { + // certdata multiline values encode raw bytes as `\ooo` octal escapes. + let bytes = line.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] == b'\\' { + if i + 3 >= bytes.len() { + bail!("truncated octal escape in certdata line"); + } + let oct = &line[i + 1..i + 4]; + let value = u8::from_str_radix(oct, 8) + .with_context(|| format!("invalid octal escape \\{oct}"))?; + out.push(value); + i += 4; + } else { + out.push(bytes[i]); + i += 1; + } + } + Ok(()) +} + +fn hex_encode_lower(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push(nibble_to_hex(b >> 4)); + out.push(nibble_to_hex(b & 0x0f)); + } + out +} + +fn nibble_to_hex(n: u8) -> char { + match n { + 0..=9 => (b'0' + n) as char, + 10..=15 => (b'a' + (n - 10)) as char, + _ => '?', + } +} + +fn percent_encode_bytes(bytes: &[u8]) -> String { + let mut out = String::new(); + for b in bytes { + out.push('%'); + out.push(nibble_to_hex(b >> 4)); + out.push(nibble_to_hex(b & 0x0f)); + } + out +} + +fn pem_encode(label: &str, der: &[u8]) -> String { + let b64 = base64::engine::general_purpose::STANDARD.encode(der); + let mut out = String::new(); + out.push_str("-----BEGIN "); + out.push_str(label); + out.push_str("-----\n"); + for chunk in b64.as_bytes().chunks(64) { + out.push_str(std::str::from_utf8(chunk).unwrap_or("")); + out.push('\n'); + } + out.push_str("-----END "); + out.push_str(label); + out.push_str("-----\n"); + out +} + +fn openssl_pubkey_pem_from_der(der_cert: &[u8]) -> Result { + // Reuse OpenSSL (like make-ca) to avoid implementing X.509 SPKI extraction here. + let mut child = Command::new("openssl") + .args(["x509", "-inform", "DER", "-pubkey", "-noout"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to spawn `openssl x509`")?; + + { + let stdin = child + .stdin + .as_mut() + .context("failed to open openssl stdin")?; + stdin + .write_all(der_cert) + .context("failed to write DER certificate to openssl stdin")?; + } + + let output = child + .wait_with_output() + .context("failed to wait for openssl x509")?; + if !output.status.success() { + bail!( + "openssl x509 failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); + } + Ok(String::from_utf8(output.stdout).context("openssl pubkey output was not UTF-8")?) +} + +fn render_p11kit_cert_pair( + cert: &MozillaCertEntry, + trust: &MozillaTrustEntry, + pubkey_pem: &str, + cert_pem: &str, +) -> Result { + // Each CA emits an extension object (EKU trust policy) and a certificate trust object. + let (p11trust_line, p11_oid, p11_value) = p11_trust_tuple(trust)?; + let mut out = String::new(); + + out.push_str("[p11-kit-object-v1]\n"); + out.push_str(&format!("label: \"{}\"\n", p11kit_quote(&cert.label))); + out.push_str("class: x-certificate-extension\n"); + out.push_str(&format!("object-id: {p11_oid}\n")); + out.push_str(&format!("value: \"{p11_value}\"\n")); + out.push_str("modifiable: false\n"); + out.push_str(pubkey_pem); + if !pubkey_pem.ends_with('\n') { + out.push('\n'); + } + out.push('\n'); + + out.push_str("[p11-kit-object-v1]\n"); + out.push_str(&format!("label: \"{}\"\n", p11kit_quote(&cert.label))); + out.push_str(p11trust_line); + out.push('\n'); + out.push_str(&format!( + "nss-mozilla-ca-policy: {}\n", + if cert.mozilla_policy { "true" } else { "false" } + )); + out.push_str("modifiable: false\n"); + if let Some(v) = &cert.server_distrust_after { + out.push_str(&format!("nss-server-distrust-after: \"{v}\"\n")); + } + if let Some(v) = &cert.email_distrust_after { + out.push_str(&format!("nss-email-distrust-after: \"{v}\"\n")); + } + out.push_str(cert_pem); + Ok(out) +} + +fn p11kit_quote(input: &str) -> String { + input.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn p11_trust_tuple( + trust: &MozillaTrustEntry, +) -> Result<(&'static str, &'static str, &'static str)> { + // These values mirror make-ca's p11-kit extension constants. + let any_distrusted = matches!(trust.server_auth, TrustUsage::Distrusted) + || matches!(trust.email, TrustUsage::Distrusted) + || matches!(trust.code_signing, TrustUsage::Distrusted); + + if any_distrusted { + return Ok(( + "x-distrusted: true", + "1.3.6.1.4.1.3319.6.10.1", + "0.%06%0a%2b%06%01%04%01%99w%06%0a%01%04 0%1e%06%08%2b%06%01%05%05%07%03%04%06%08%2b%06%01%05%05%07%03%01%06%08%2b%06%01%05%05%07%03%03", + )); + } + + let mut key = String::from("p11"); + if matches!(trust.server_auth, TrustUsage::Trusted) { + key.push_str("sa"); + } + if matches!(trust.email, TrustUsage::Trusted) { + key.push_str("sm"); + } + if matches!(trust.code_signing, TrustUsage::Trusted) { + key.push_str("cs"); + } + + let value = match key.as_str() { + "p11sasmcs" => { + "0%2a%06%03U%1d%25%01%01%ff%04 0%1e%06%08%2b%06%01%05%05%07%03%04%06%08%2b%06%01%05%05%07%03%01%06%08%2b%06%01%05%05%07%03%03" + } + "p11sasm" => { + "0 %06%03U%1d%25%01%01%ff%04%160%14%06%08%2b%06%01%05%05%07%03%04%06%08%2b%06%01%05%05%07%03%01" + } + "p11sacs" => { + "0 %06%03U%1d%25%01%01%ff%04%160%14%06%08%2b%06%01%05%05%07%03%01%06%08%2b%06%01%05%05%07%03%03" + } + "p11sa" => "0%16%06%03U%1d%25%01%01%ff%04%0c0%0a%06%08%2b%06%01%05%05%07%03%01", + "p11smcs" => { + "0 %06%03U%1d%25%01%01%ff%04%160%14%06%08%2b%06%01%05%05%07%03%04%06%08%2b%06%01%05%05%07%03%03" + } + "p11sm" => "0%16%06%03U%1d%25%01%01%ff%04%0c0%0a%06%08%2b%06%01%05%05%07%03%04", + "p11cs" => "0%16%06%03U%1d%25%01%01%ff%04%0c0%0a%06%08%2b%06%01%05%05%07%03%03", + "p11" => "0%18%06%03U%1d%25%01%01%ff%04%0e0%0c%06%0a%2b%06%01%04%01%99w%06%0a%10", + _ => bail!("unsupported trust combination key {key}"), + }; + + Ok(("trusted: true", "2.5.29.37", value)) +} + +fn parse_certdata(input: &str) -> Result { + let mut revision = None; + let mut objects = Vec::new(); + let mut current_attrs = Vec::new(); + let mut pending_multiline: Option<(String, String, Vec)> = None; + + for (idx, raw_line) in input.lines().enumerate() { + let line_no = idx + 1; + let line = raw_line.trim_end_matches('\r'); + let trimmed = line.trim(); + + if let Some((name, value_type, lines)) = &mut pending_multiline { + if trimmed == "END" { + current_attrs.push(CertDataAttribute { + name: name.clone(), + value_type: value_type.clone(), + value: CertDataValue::Multiline(std::mem::take(lines)), + }); + pending_multiline = None; + } else { + lines.push(line.to_string()); + } + continue; + } + + if trimmed.is_empty() { + push_certdata_object(&mut objects, &mut current_attrs); + continue; + } + + if let Some(rest) = trimmed.strip_prefix('#') { + if let Some(rev) = rest.trim().strip_prefix("Revision:") { + revision = Some(rev.trim().to_string()); + } + continue; + } + + // certdata.txt includes top-level markers like BEGINDATA which are not attributes. + if trimmed == "BEGINDATA" || trimmed == "ENDDATA" { + continue; + } + + let (name, value_type, value) = parse_certdata_attr_header(line) + .with_context(|| format!("invalid certdata attribute at line {line_no}: {line}"))?; + + if value_type.starts_with("MULTILINE_") { + if !value.is_empty() { + bail!("unexpected trailing data after multiline marker at line {line_no}"); + } + pending_multiline = Some((name.to_string(), value_type.to_string(), Vec::new())); + } else { + current_attrs.push(CertDataAttribute { + name: name.to_string(), + value_type: value_type.to_string(), + value: CertDataValue::Inline(value.to_string()), + }); + } + } + + if let Some((name, _, _)) = pending_multiline { + bail!("unterminated multiline value for attribute {name}"); + } + + push_certdata_object(&mut objects, &mut current_attrs); + + Ok(CertDataDocument { revision, objects }) +} + +fn push_certdata_object( + objects: &mut Vec, + current_attrs: &mut Vec, +) { + if current_attrs.is_empty() { + return; + } + objects.push(CertDataObject { + attributes: std::mem::take(current_attrs), + }); +} + +fn parse_certdata_attr_header(line: &str) -> Result<(&str, &str, &str)> { + let (name, rest) = take_token(line).context("missing attribute name")?; + let (value_type, rest) = take_token(rest).context("missing attribute type")?; + Ok((name, value_type, rest.trim_start())) +} + +fn take_token(input: &str) -> Option<(&str, &str)> { + let input = input.trim_start(); + if input.is_empty() { + return None; + } + let end = input.find(char::is_whitespace).unwrap_or(input.len()); + Some((&input[..end], &input[end..])) +} + +fn print_certdata_summary(doc: &CertDataDocument, limit: usize) { + let mut class_counts: BTreeMap = BTreeMap::new(); + let mut multiline_attrs = 0usize; + + for obj in &doc.objects { + let class = obj + .inline_attr_value("CKA_CLASS") + .unwrap_or("UNKNOWN") + .to_string(); + *class_counts.entry(class).or_default() += 1; + multiline_attrs += obj + .attributes + .iter() + .filter(|attr| matches!(attr.value, CertDataValue::Multiline(_))) + .count(); + } + + println!( + "certdata revision: {}", + doc.revision.as_deref().unwrap_or("") + ); + println!("objects: {}", doc.objects.len()); + println!("multiline attributes: {}", multiline_attrs); + println!("classes:"); + for (class, count) in class_counts { + println!(" {class}: {count}"); + } + + if limit == 0 { + return; + } + + println!("sample objects (up to {limit}):"); + for (idx, obj) in doc.objects.iter().take(limit).enumerate() { + let class = obj.inline_attr_value("CKA_CLASS").unwrap_or("UNKNOWN"); + let label = obj + .inline_attr_value("CKA_LABEL") + .map(certdata_inline_unquote) + .unwrap_or_else(|| "".to_string()); + println!( + " {}. class={} label={} attrs={}", + idx + 1, + class, + label, + obj.attributes.len() + ); + } +} + +fn certdata_inline_unquote(value: &str) -> String { + let trimmed = value.trim(); + if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { + trimmed[1..trimmed.len() - 1].replace("\\\"", "\"") + } else { + trimmed.to_string() + } +} + +fn ensure_pem_certificate(bytes: &[u8]) -> Result<()> { + // Keep validation intentionally simple: we only require a PEM certificate block. + let text = std::str::from_utf8(bytes).context("certificate is not valid UTF-8 PEM text")?; + if !text.contains("-----BEGIN CERTIFICATE-----") || !text.contains("-----END CERTIFICATE-----") + { + bail!("certificate must be PEM-encoded and contain a CERTIFICATE block"); + } + Ok(()) +} + +fn sanitize_name(input: &str) -> String { + // Restrict filenames to portable characters to avoid odd trust-source names. + input + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { + c + } else { + '_' + } + }) + .collect::() + .trim_matches('.') + .to_string() +} + +fn install_file(dest_file: &Path, bytes: &[u8], force: bool, dry_run: bool) -> Result<()> { + // Avoid rewriting identical content to keep runs idempotent. + if let Ok(existing) = fs::read(dest_file) { + if existing == bytes { + println!("File already exists with identical contents."); + return Ok(()); + } + if !force { + bail!( + "destination already exists with different contents: {} (use --force to overwrite)", + dest_file.display() + ); + } + println!("Overwriting existing file (`--force`)."); + } + + if dry_run { + println!("[dry-run] Would write {}", dest_file.display()); + return Ok(()); + } + + let parent = dest_file + .parent() + .context("destination path has no parent directory")?; + fs::create_dir_all(parent) + .with_context(|| format!("failed to create directory {}", parent.display()))?; + + let file_name = dest_file + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("temp.pem"); + // Write atomically via a temporary file in the same directory. + let tmp_file = parent.join(format!(".{file_name}.tmp")); + + fs::write(&tmp_file, bytes) + .with_context(|| format!("failed to write temporary file {}", tmp_file.display()))?; + #[cfg(unix)] + { + fs::set_permissions(&tmp_file, fs::Permissions::from_mode(0o644)) + .with_context(|| format!("failed to set permissions on {}", tmp_file.display()))?; + } + fs::rename(&tmp_file, dest_file).with_context(|| { + format!( + "failed to move {} into place at {}", + tmp_file.display(), + dest_file.display() + ) + })?; + println!("Installed {}", dest_file.display()); + Ok(()) +} + +fn extract_trust(root: &Path, output_target: &Path, dry_run: bool) -> Result<()> { + let output_host = path_in_root(root, output_target); + println!( + "Extract destination (target path): {}", + output_target.display() + ); + println!("Extract destination (host path): {}", output_host.display()); + + if dry_run { + println!("[dry-run] Would create {}", output_host.display()); + } else { + fs::create_dir_all(&output_host) + .with_context(|| format!("failed to create {}", output_host.display()))?; + } + + // Generate each derived output from the shared p11-kit trust database. + for job in EXTRACT_JOBS { + let dest_target = output_target.join(job.relative_dest); + println!( + "trust extract: --format={} --filter={} -> {}", + job.format, + job.filter, + dest_target.display() + ); + run_trust_extract_job(root, job, &dest_target, dry_run)?; + } + + // Match `update-ca-trust` behavior only for the standard extraction destination. + if output_target == Path::new(DEFAULT_EXTRACTED_DIR) { + sync_system_ssl_symlinks(root, output_target, dry_run)?; + } else { + println!("Skipping /etc/ssl/certs symlink sync for custom output directory."); + } + + Ok(()) +} + +fn run_trust_extract_job( + root: &Path, + job: &ExtractJob, + dest_target: &Path, + dry_run: bool, +) -> Result<()> { + // Build the `trust extract` invocation exactly from the job description. + let mut args = vec![ + "extract".to_string(), + "--overwrite".to_string(), + format!("--format={}", job.format), + format!("--filter={}", job.filter), + ]; + if job.comment { + args.push("--comment".to_string()); + } + if let Some(purpose) = job.purpose { + args.push(format!("--purpose={purpose}")); + } + args.push(dest_target.display().to_string()); + + run_target_command( + root, + "trust", + &args, + dry_run, + &[("P11_KIT_NO_USER_CONFIG", "1")], + ) + .context("trust extract failed") +} + +fn sync_system_ssl_symlinks(root: &Path, extracted_target: &Path, dry_run: bool) -> Result<()> { + let ssl_certs_host = path_in_root(root, Path::new(DEFAULT_SSL_CERTS_DIR)); + let java_link_host = path_in_root(root, Path::new(DEFAULT_JAVA_CACERTS_LINK)); + let cadir_host = path_in_root(root, &extracted_target.join("cadir")); + let java_src_host = path_in_root(root, &extracted_target.join("java-cacerts.jks")); + + println!("Syncing /etc/ssl/certs links from {}", cadir_host.display()); + + if dry_run { + println!("[dry-run] Would ensure {}", ssl_certs_host.display()); + println!( + "[dry-run] Would link files from {} into {}", + cadir_host.display(), + ssl_certs_host.display() + ); + println!( + "[dry-run] Would delete broken symlinks in {}", + ssl_certs_host.display() + ); + println!( + "[dry-run] Would link {} -> {}", + java_link_host.display(), + java_src_host.display() + ); + return Ok(()); + } + + fs::create_dir_all(&ssl_certs_host) + .with_context(|| format!("failed to create {}", ssl_certs_host.display()))?; + let java_parent = java_link_host + .parent() + .context("java cacerts link path has no parent")?; + fs::create_dir_all(java_parent) + .with_context(|| format!("failed to create {}", java_parent.display()))?; + + // Link every extracted hash file into `/etc/ssl/certs`, then prune stale links. + for entry in fs::read_dir(&cadir_host) + .with_context(|| format!("failed to read {}", cadir_host.display()))? + { + let entry = entry?; + let source = entry.path(); + let link_path = ssl_certs_host.join(entry.file_name()); + replace_symlink(&source, &link_path)?; + } + + remove_broken_symlinks(&ssl_certs_host)?; + replace_symlink(&java_src_host, &java_link_host)?; + Ok(()) +} + +fn remove_broken_symlinks(dir: &Path) -> Result<()> { + // `update-ca-trust` prunes stale links after re-linking the extracted directory. + for entry in fs::read_dir(dir).with_context(|| format!("failed to read {}", dir.display()))? { + let entry = entry?; + let path = entry.path(); + let meta = fs::symlink_metadata(&path) + .with_context(|| format!("failed to stat {}", path.display()))?; + if !meta.file_type().is_symlink() { + continue; + } + if fs::metadata(&path).is_err() { + fs::remove_file(&path) + .with_context(|| format!("failed to remove broken symlink {}", path.display()))?; + } + } + Ok(()) +} + +fn replace_symlink(source: &Path, link_path: &Path) -> Result<()> { + let parent = link_path + .parent() + .context("link path has no parent directory")?; + let rel_target = make_relative_path(parent, source); + + // Replace pre-existing files/symlinks, but never clobber a real directory. + if let Ok(meta) = fs::symlink_metadata(link_path) { + if meta.is_dir() && !meta.file_type().is_symlink() { + bail!( + "refusing to replace directory with symlink: {}", + link_path.display() + ); + } + fs::remove_file(link_path) + .with_context(|| format!("failed to remove {}", link_path.display()))?; + } + + symlink(&rel_target, link_path).with_context(|| { + format!( + "failed to create symlink {} -> {}", + link_path.display(), + rel_target.display() + ) + })?; + Ok(()) +} + +fn make_relative_path(from_dir: &Path, to_path: &Path) -> PathBuf { + // Prefer relative symlinks so extracted trees stay relocatable under a rootfs. + for (depth, ancestor) in from_dir.ancestors().enumerate() { + if let Ok(suffix) = to_path.strip_prefix(ancestor) { + let mut rel = PathBuf::new(); + for _ in 0..depth { + rel.push(".."); + } + rel.push(suffix); + return rel; + } + } + to_path.to_path_buf() +} + +fn run_target_command( + root: &Path, + program: &str, + args: &[String], + dry_run: bool, + envs: &[(&str, &str)], +) -> Result<()> { + if dry_run { + print_command_preview(root, program, args, envs); + return Ok(()); + } + + // Execute directly on the host or via chroot for image builds. + let mut cmd = if is_root_fs(root) { + let mut cmd = Command::new(program); + cmd.args(args); + cmd + } else { + let mut cmd = Command::new("chroot"); + cmd.arg(root).arg(program).args(args); + cmd + }; + + // Prevent `trust` from loading user-level configuration. + for (k, v) in envs { + cmd.env(k, v); + } + + let output = cmd.output().with_context(|| { + if is_root_fs(root) { + format!("failed to execute `{}`", program) + } else { + format!("failed to execute `chroot {} {}`", root.display(), program) + } + })?; + + if output.status.success() { + if !output.stdout.is_empty() { + print!("{}", String::from_utf8_lossy(&output.stdout)); + } + if !output.stderr.is_empty() { + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + } + return Ok(()); + } + + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let mut details = Vec::new(); + if !stdout.is_empty() { + details.push(format!("stdout: {stdout}")); + } + if !stderr.is_empty() { + details.push(format!("stderr: {stderr}")); + } + let suffix = if details.is_empty() { + String::new() + } else { + format!("\n{}", details.join("\n")) + }; + + bail!( + "command exited with status {}{}", + output + .status + .code() + .map(|c| c.to_string()) + .unwrap_or_else(|| "signal".to_string()), + suffix + ); +} + +fn print_command_preview(root: &Path, program: &str, args: &[String], envs: &[(&str, &str)]) { + // Keep dry-run output close to the real shell command for easier debugging. + let env_prefix = envs + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(" "); + let arg_string = if args.is_empty() { + String::new() + } else { + format!(" {}", args.join(" ")) + }; + + if is_root_fs(root) { + if env_prefix.is_empty() { + println!("[dry-run] Would run: {program}{arg_string}"); + } else { + println!("[dry-run] Would run: {env_prefix} {program}{arg_string}"); + } + } else if env_prefix.is_empty() { + println!( + "[dry-run] Would run: chroot {} {}{}", + root.display(), + program, + arg_string + ); + } else { + println!( + "[dry-run] Would run: {} chroot {} {}{}", + env_prefix, + root.display(), + program, + arg_string + ); + } +} + +fn path_in_root(root: &Path, target_path: &Path) -> PathBuf { + // Interpret absolute target paths as paths inside the selected rootfs. + if target_path.is_absolute() { + root.join(target_path.strip_prefix("/").unwrap_or(target_path)) + } else { + root.join(target_path) + } +} + +fn is_root_fs(root: &Path) -> bool { + root == Path::new(DEFAULT_ROOT) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE_CERTDATA: &str = r#" +# Revision:20250604 +BEGINDATA +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_LABEL UTF8 "Example Root CA" +CKA_VALUE MULTILINE_OCTAL +\060\202 +\001\002 +END + +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_LABEL UTF8 "Example Root CA" +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR + +"#; + + #[test] + fn pem_validation_accepts_basic_cert_block() { + let pem = b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"; + assert!(ensure_pem_certificate(pem).is_ok()); + } + + #[test] + fn pem_validation_rejects_non_pem() { + let err = ensure_pem_certificate(b"not a certificate").unwrap_err(); + assert!(err.to_string().contains("PEM-encoded")); + } + + #[test] + fn pem_validation_rejects_non_utf8() { + let err = ensure_pem_certificate(&[0xff, 0xfe, 0xfd]).unwrap_err(); + assert!(err.to_string().contains("UTF-8")); + } + + #[test] + fn sanitize_name_replaces_invalid_chars_and_trims_dots() { + assert_eq!( + sanitize_name("..ACME Root CA (prod).."), + "ACME_Root_CA__prod_" + ); + } + + #[test] + fn sanitize_name_keeps_safe_chars() { + assert_eq!(sanitize_name("ca-root_v1.2-3"), "ca-root_v1.2-3"); + } + + #[test] + fn relative_path_same_parent() { + let from = Path::new("/etc/ssl/certs"); + let to = Path::new("/etc/ssl/certs/abc.pem"); + assert_eq!(make_relative_path(from, to), PathBuf::from("abc.pem")); + } + + #[test] + fn relative_path_across_siblings() { + let from = Path::new("/etc/ssl/certs/java"); + let to = Path::new("/etc/ca-certificates/extracted/java-cacerts.jks"); + assert_eq!( + make_relative_path(from, to), + PathBuf::from("../../../ca-certificates/extracted/java-cacerts.jks") + ); + } + + #[test] + fn path_in_root_maps_absolute_target() { + assert_eq!( + path_in_root(Path::new("/mnt/rootfs"), Path::new("/etc/ssl/certs")), + PathBuf::from("/mnt/rootfs/etc/ssl/certs") + ); + } + + #[test] + fn path_in_root_maps_relative_target() { + assert_eq!( + path_in_root(Path::new("/mnt/rootfs"), Path::new("custom/out")), + PathBuf::from("/mnt/rootfs/custom/out") + ); + } + + #[test] + fn root_detection_matches_default_only() { + assert!(is_root_fs(Path::new("/"))); + assert!(!is_root_fs(Path::new("/mnt/rootfs"))); + } + + #[test] + fn certdata_parser_extracts_revision_and_objects() { + let doc = parse_certdata(SAMPLE_CERTDATA).unwrap(); + assert_eq!(doc.revision.as_deref(), Some("20250604")); + assert_eq!(doc.objects.len(), 2); + assert_eq!( + doc.objects[0].inline_attr_value("CKA_CLASS"), + Some("CKO_CERTIFICATE") + ); + assert_eq!( + doc.objects[1].inline_attr_value("CKA_CLASS"), + Some("CKO_NSS_TRUST") + ); + } + + #[test] + fn certdata_parser_handles_multiline_attributes() { + let doc = parse_certdata(SAMPLE_CERTDATA).unwrap(); + let attr = doc.objects[0].attr("CKA_VALUE").unwrap(); + match &attr.value { + CertDataValue::Multiline(lines) => { + assert_eq!( + lines, + &vec![r"\060\202".to_string(), r"\001\002".to_string()] + ); + } + other => panic!("expected multiline value, got {other:?}"), + } + } + + #[test] + fn certdata_parser_rejects_unterminated_multiline() { + let input = "CKA_VALUE MULTILINE_OCTAL\n\\060\\202\n"; + let err = parse_certdata(input).unwrap_err(); + assert!(err.to_string().contains("unterminated multiline")); + } + + #[test] + fn certdata_inline_unquote_removes_quotes() { + assert_eq!(certdata_inline_unquote("\"Test CA\""), "Test CA"); + assert_eq!( + certdata_inline_unquote("CKO_CERTIFICATE"), + "CKO_CERTIFICATE" + ); + } + + #[test] + fn derive_hg_log_url_from_raw_file_url() { + let raw = "https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ckfw/builtins/certdata.txt"; + assert_eq!( + derive_hg_log_url(raw).as_deref(), + Some("https://hg.mozilla.org/projects/nss/log/tip/lib/ckfw/builtins/certdata.txt") + ); + } + + #[test] + fn derive_hg_log_url_returns_none_for_non_hg_raw_url() { + assert!(derive_hg_log_url("https://example.com/certdata.txt").is_none()); + } + + #[test] + fn extract_hg_revision_from_log_html_matches_make_ca_style_line() { + let html = r#" +x +76e6887ecc1a5410233ad9c5f4cadae4e298a37b
created 2026-02-06 10:55 +0000 +"#; + assert_eq!( + extract_hg_revision_from_log_html(html).unwrap(), + "76e6887ecc1a5410233ad9c5f4cadae4e298a37b" + ); + } + + #[test] + fn extract_certdata_revision_from_text_reads_header() { + let text = "# Revision:abc123\n# certdata\n"; + assert_eq!( + extract_certdata_revision_from_text(text).as_deref(), + Some("abc123") + ); + } + + #[test] + fn normalize_certdata_download_inserts_and_replaces_revision_line() { + let raw = "# Revision:old\n# certdata\nBEGINDATA\n"; + let out = normalize_certdata_download(raw, Some("newrev".to_string())); + assert!(out.starts_with("# Revision:newrev\n")); + assert!(!out.contains("# Revision:old")); + assert!(out.contains("BEGINDATA")); + } + + #[test] + fn decode_certdata_octal_multiline_decodes_bytes() { + let lines = vec![r"\101\102".to_string(), r"\103".to_string()]; + let bytes = decode_certdata_octal_multiline(&lines).unwrap(); + assert_eq!(bytes, b"ABC"); + } + + #[test] + fn percent_encode_bytes_encodes_lower_hex() { + assert_eq!(percent_encode_bytes(&[0x00, 0x2a, 0xff]), "%00%2a%ff"); + } + + #[test] + fn p11_trust_tuple_trusted_server_auth_matches_make_ca_constant() { + let trust = MozillaTrustEntry { + server_auth: TrustUsage::Trusted, + email: TrustUsage::Neutral, + code_signing: TrustUsage::Neutral, + }; + let (trust_line, oid, value) = p11_trust_tuple(&trust).unwrap(); + assert_eq!(trust_line, "trusted: true"); + assert_eq!(oid, "2.5.29.37"); + assert!(value.contains("%07%03%01")); + } + + #[test] + fn p11_trust_tuple_any_distrust_uses_x_distrusted() { + let trust = MozillaTrustEntry { + server_auth: TrustUsage::Neutral, + email: TrustUsage::Distrusted, + code_signing: TrustUsage::Neutral, + }; + let (trust_line, oid, _) = p11_trust_tuple(&trust).unwrap(); + assert_eq!(trust_line, "x-distrusted: true"); + assert_eq!(oid, "1.3.6.1.4.1.3319.6.10.1"); + } +}