apt-sources-0.3.0/.cargo_vcs_info.json0000644000000001511046102023000133200ustar { "git": { "sha1": "8ef0a9bbfdb31d404a336769b331a2ee5640624a" }, "path_in_vcs": "apt-sources" }apt-sources-0.3.0/Cargo.lock0000644000002671011046102023000113050ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" 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 = "apt-sources" version = "0.3.0" dependencies = [ "clap", "colored", "deb822-fast", "env_logger", "indicatif", "indoc", "itertools 0.14.0", "launchpadlib", "log", "regex", "reqwest 0.12.28", "sequoia-net", "sequoia-openpgp", "serde_json", "tempfile", "tokio", "tracing", "url", ] [[package]] name = "argon2" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures", "password-hash", ] [[package]] name = "ascii-canvas" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" dependencies = [ "term", ] [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", ] [[package]] name = "aws-lc-sys" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", "dunce", "fs_extra", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "buffered-reader" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" dependencies = [ "flate2", "libc", ] [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" 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 = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[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 = "chrono" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "console" version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", "unicode-width", "windows-sys 0.61.2", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "typenum", ] [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deb822-derive" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83ef29a094bcb2b7dd0f609ace7f5a34ef9a62e0731ebd350637640320a3b15" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "deb822-fast" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "114c474fa4cd5d6d24bb5e68b36fa4ef70f5b830e3cc14a9b66a12e71a15aeb9" dependencies = [ "deb822-derive", ] [[package]] name = "debversion" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79ef0d110ecfe7c87db7ff3a87756cd31d601073f440619963bea29f06694dc" dependencies = [ "chrono", "lazy-regex", "num-bigint", "serde", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[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 = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "ena" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" dependencies = [ "log", ] [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "env_filter" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[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 = "futf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", ] [[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-macro" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", "syn", ] [[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-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "generic-array" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" dependencies = [ "rustversion", "typenum", ] [[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 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "wasip2", "wasip3", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hickory-client" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "156579a5cd8d1fc6f0df87cc21b6ee870db978a163a1ba484acd98a4eff5a6de" dependencies = [ "cfg-if", "data-encoding", "futures-channel", "futures-util", "hickory-proto", "once_cell", "radix_trie", "rand 0.8.6", "thiserror 1.0.69", "tokio", "tracing", ] [[package]] name = "hickory-proto" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" dependencies = [ "async-trait", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", "idna", "ipnet", "once_cell", "openssl", "rand 0.8.6", "thiserror 1.0.69", "tinyvec", "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", "lru-cache", "once_cell", "parking_lot", "rand 0.8.6", "resolv-conf", "smallvec", "thiserror 1.0.69", "tokio", "tracing", ] [[package]] name = "html2md" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cff9891f2e0d9048927fbdfc28b11bf378f6a93c7ba70b23d0fbee9af6071b4" dependencies = [ "html5ever", "jni 0.19.0", "lazy_static", "markup5ever_rcdom", "percent-encoding", "regex", ] [[package]] name = "html5ever" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", "markup5ever", "proc-macro2", "quote", "syn", ] [[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.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "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", "system-configuration", "tokio", "tower-service", "tracing", "windows-registry", ] [[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", "utf8_iter", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "indicatif" version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", "unicode-width", "unit-prefix", "web-time", ] [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "ipconfig" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ "socket2", "widestring", "windows-registry", "windows-result", "windows-sys 0.61.2", ] [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" 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 = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde_core", ] [[package]] name = "jiff-static" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jni" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ "cesu8", "combine", "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", ] [[package]] name = "jni" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ "cfg-if", "combine", "jni-macros", "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.18", "walkdir", "windows-link", ] [[package]] name = "jni-macros" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", "rustc_version", "simd_cesu8", "syn", ] [[package]] name = "jni-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" dependencies = [ "jni-sys 0.4.1", ] [[package]] name = "jni-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ "jni-sys-macros", ] [[package]] name = "jni-sys-macros" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", "syn", ] [[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.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] [[package]] name = "keyring" version = "3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" dependencies = [ "byteorder", "linux-keyutils", "log", "security-framework 2.11.1", "security-framework 3.7.0", "windows-sys 0.60.2", "zeroize", ] [[package]] name = "lalrpop" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a80a963123205c7157323c99611bc4abb65dcbd62ef46dc4bac74a3941bc75" dependencies = [ "ascii-canvas", "bit-set", "ena", "itertools 0.14.0", "lalrpop-util", "petgraph", "regex", "regex-syntax", "sha3", "string_cache 0.9.0", "term", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884f3e747ed2dcee867cda1b0c31a048f9e20de2d916a248949319921a2e666e" dependencies = [ "regex-automata", ] [[package]] name = "launchpadlib" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff3adfceea2d22a21fcdff64ef03fade0ab7ae9653c4c15bd31072f8bc684ce6" dependencies = [ "chrono", "debversion", "form_urlencoded", "keyring", "lazy_static", "log", "mime", "percent-encoding", "rand 0.9.4", "reqwest 0.13.3", "serde", "serde_json", "url", "wadl", ] [[package]] name = "lazy-regex" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", "windows-link", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-keyutils" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ "bitflags", "libc", ] [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ "linked-hash-map", ] [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markup5ever" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", "phf", "phf_codegen", "string_cache 0.8.9", "string_cache_codegen", "tendril", ] [[package]] name = "markup5ever_rcdom" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" dependencies = [ "html5ever", "markup5ever", "tendril", "xml5ever", ] [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memsec" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "native-tls" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework 3.7.0", "security-framework-sys", "tempfile", ] [[package]] name = "nettle" version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2578a3627c28fefb60f1680e20e85f38bd8c8bf98c288b370489a573b0640907" dependencies = [ "getrandom 0.4.2", "libc", "nettle-sys", "thiserror 2.0.18", "typenum", ] [[package]] name = "nettle-sys" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f35502358aa77e598570bbf9a79ad19c4985a1bcd157b5d70197688dafca9b48" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ "smallvec", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "password-hash" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", "subtle", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", "indexmap", ] [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared 0.11.3", ] [[package]] name = "phf_codegen" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared 0.11.3", ] [[package]] name = "phf_generator" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", "rand 0.8.6", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "phf_shared" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" 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 = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[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.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.4", "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.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radix_trie" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ "endian-type", "nibble_vec", ] [[package]] name = "rand" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[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 0.9.5", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.17", ] [[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 = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "reqwest" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "js-sys", "log", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "resolv-conf" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" 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 3.7.0", ] [[package]] name = "rustls-pki-types" version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", ] [[package]] name = "rustls-platform-verifier" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "jni 0.22.4", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", ] [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "sequoia-net" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c0ea4f555b2b109fb39d6c5ebdb614945d82da0105957591adf4641e9cc3b2" dependencies = [ "anyhow", "base64", "futures-util", "hickory-client", "hickory-resolver", "http", "hyper", "hyper-tls", "libc", "percent-encoding", "reqwest 0.12.28", "sequoia-openpgp", "thiserror 2.0.18", "tokio", "url", "z-base-32", ] [[package]] name = "sequoia-openpgp" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c847f0f148cf238c3aec88d092fd3c4301c21e906829ea9e415ea7531f7ec094" dependencies = [ "anyhow", "argon2", "base64", "buffered-reader", "chrono", "dyn-clone", "flate2", "getrandom 0.2.17", "idna", "lalrpop", "lalrpop-util", "libc", "memsec", "nettle", "regex", "regex-syntax", "sha1collisiondetection", "thiserror 2.0.18", "xxhash-rust", ] [[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 = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1collisiondetection" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" dependencies = [ "digest", "generic-array 1.4.1", ] [[package]] name = "sha3" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest", "keccak", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" dependencies = [ "rustc_version", "simdutf8", ] [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared 0.11.3", "precomputed-hash", "serde", ] [[package]] name = "string_cache" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared 0.13.1", "precomputed-hash", ] [[package]] name = "string_cache_codegen" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ "phf_generator", "phf_shared 0.11.3", "proc-macro2", "quote", ] [[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 = "system-configuration" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "tendril" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", "utf-8", ] [[package]] name = "term" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ "windows-sys 0.61.2", ] [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" 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.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[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 = "tokio-util" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "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.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", "pin-project-lite", "tower", "tower-layer", "tower-service", "url", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unit-prefix" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[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", "serde_derive", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[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 = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wadl" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30392766f64a5e989f9455dfb1d999a7cc5af8be9d3db601bd4953ef993e61b7" dependencies = [ "form_urlencoded", "html2md", "iri-string", "lazy_static", "log", "mime", "proc-macro2", "quote", "reqwest 0.13.3", "serde_json", "syn", "url", "xmltree", ] [[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.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" 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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] [[package]] name = "widestring" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[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-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[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.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.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.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.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.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.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.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.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" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xml" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xml5ever" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", "mac", "markup5ever", ] [[package]] name = "xmltree" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc04313cab124e498ab1724e739720807b6dc405b9ed0edc5860164d2e4ff70" dependencies = [ "xml", ] [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "z-base-32" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bf7b4a78668416e1e8a332334e26fb2f377afe707f0c6feaf6ed5f9100133b" [[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", "syn", "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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" apt-sources-0.3.0/Cargo.toml0000644000000061411046102023000113230ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "apt-sources" version = "0.3.0" authors = [ "Michał Fita <4925040+michalfita@users.noreply.github.com>", "Jelmer Vernooij ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A parser for APT source files (package repositories specification)" homepage = "https://github.com/jelmer/debian-parsers" readme = false keywords = [ "debian", "deb822", "rfc822", "edit", "apt", ] categories = ["parser-implementations"] license = "Apache-2.0" repository = "https://github.com/jelmer/debian-parsers" [features] cli = [ "dep:clap", "dep:env_logger", "dep:log", ] default = [ "lossless", "legacy", ] key-management = [ "dep:sequoia-openpgp", "dep:sequoia-net", "dep:tokio", ] launchpad = [ "dep:launchpadlib", "dep:reqwest", "dep:serde_json", ] legacy = ["dep:regex"] lossless = [] tracing = ["dep:tracing"] [lib] name = "apt_sources" path = "src/lib.rs" [[bin]] name = "apt-add-repository" path = "src/bin/apt-add-repository.rs" required-features = [ "key-management", "launchpad", "cli", ] [[bin]] name = "ppa-info" path = "src/bin/ppa-info.rs" required-features = [ "launchpad", "cli", ] [[example]] name = "source-for-docker" path = "examples/source-for-docker.rs" [dependencies.clap] version = "4" features = ["derive"] optional = true [dependencies.colored] version = ">=2.1,<4" [dependencies.deb822-fast] version = ">=0.1, <0.3" features = ["derive"] [dependencies.env_logger] version = "0.11" optional = true [dependencies.indicatif] version = ">=0.17,<0.19" [dependencies.itertools] version = "0.14.0" [dependencies.launchpadlib] version = "0.5.5" features = [ "blocking", "api-v1_0", "packages", "keyring", ] optional = true default-features = false [dependencies.log] version = "0.4" optional = true [dependencies.regex] version = "1.11.1" optional = true [dependencies.reqwest] version = ">=0.12,<0.14" features = [ "blocking", "json", ] optional = true [dependencies.sequoia-net] version = "0.30" optional = true default-features = false [dependencies.sequoia-openpgp] version = "2.0" features = [ "compression-deflate", "crypto-nettle", ] optional = true default-features = false [dependencies.serde_json] version = "1.0" optional = true [dependencies.tokio] version = "1" features = [ "rt", "macros", ] optional = true [dependencies.tracing] version = "0.1" optional = true [dependencies.url] version = "2.5.4" features = ["serde"] [dev-dependencies.indoc] version = "2.0.6" [dev-dependencies.tempfile] version = "3" apt-sources-0.3.0/Cargo.toml.orig000064400000000000000000000041041046102023000147570ustar 00000000000000[package] name = "apt-sources" authors = [ "Michał Fita <4925040+michalfita@users.noreply.github.com>", "Jelmer Vernooij ", ] edition = "2021" version = "0.3.0" license = "Apache-2.0" description = "A parser for APT source files (package repositories specification)" repository = { workspace = true } homepage = { workspace = true } keywords = ["debian", "deb822", "rfc822", "edit", "apt"] categories = ["parser-implementations"] [features] default = ["lossless", "legacy"] lossless = [] legacy = ["dep:regex"] key-management = ["dep:sequoia-openpgp", "dep:sequoia-net", "dep:tokio"] tracing = ["dep:tracing"] launchpad = ["dep:launchpadlib", "dep:reqwest", "dep:serde_json"] cli = ["dep:clap", "dep:env_logger", "dep:log"] [dependencies] deb822-fast = { version = ">=0.1, <0.3", path = "../deb822-fast", features = ["derive"] } url = { version = "2.5.4", features = ["serde"] } tracing = { version = "0.1", optional = true } sequoia-net = { version = "0.30", default-features = false, optional = true } tokio = { version = "1", features = ["rt", "macros"], optional = true } sequoia-openpgp = { version = "2.0", default-features = false, features = [ "compression-deflate", "crypto-nettle", ], optional = true } regex = { version = "1.11.1", optional = true } itertools = { version = "0.14.0" } launchpadlib = { version = "0.5.5", optional = true, default-features = false, features = ["blocking", "api-v1_0", "packages", "keyring"] } reqwest = { version = ">=0.12,<0.14", features = ["blocking", "json"], optional = true } clap = { version = "4", features = ["derive"], optional = true } env_logger = { version = "0.11", optional = true } log = { version = "0.4", optional = true } serde_json = { version = "1.0", optional = true } indicatif = ">=0.17,<0.19" colored = ">=2.1,<4" [[bin]] name = "apt-add-repository" path = "src/bin/apt-add-repository.rs" required-features = ["key-management", "launchpad", "cli"] [[bin]] name = "ppa-info" path = "src/bin/ppa-info.rs" required-features = ["launchpad", "cli"] [dev-dependencies] indoc = { version = "2.0.6" } tempfile = "3" apt-sources-0.3.0/examples/source-for-docker.rs000064400000000000000000000006401046102023000176060ustar 00000000000000use apt_sources::Repositories; use indoc::indoc; pub const TEXT: &str = indoc! {r#" Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: noble Components: stable Architectures: amd64 Signed-By: /usr/share/keyrings/docker.gpg "#}; pub fn main() { let repos = TEXT.parse::().unwrap(); let suites = repos[0].suites(); println!("{}", suites.join(" ")); } apt-sources-0.3.0/src/bin/apt-add-repository.rs000064400000000000000000003265071046102023000175420ustar 00000000000000use apt_sources::key_management::create_inline_signature; use apt_sources::launchpad::{ download_ppa_signing_key, validate_ppa, validate_ppa_components, PpaInfo, PpaValidationResult, LAUNCHPAD_PPA_URL, }; use apt_sources::{ distribution::{get_system_info, Distribution}, legacy::LegacyRepositories, signature::Signature, sources_manager::{ FileFormat as SourcesFileFormat, SourcesManager, DEFAULT_KEYRING_PATH, DEFAULT_SOURCES_PATH, }, utils::strip_auth_from_url, Repositories, Repository, RepositoryType, }; use clap::Parser; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, error, info, warn}; use reqwest; #[allow(unused_imports)] // Required for Cert::from_str use sequoia_openpgp::parse::Parse; use sequoia_openpgp::Cert; use serde_json; use std::collections::HashSet; use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use url::Url; /// Create HTTP client for making requests fn create_http_client() -> Result { reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e)) } /// Format network errors in a user-friendly way fn format_network_error(error: &reqwest::Error) -> String { if error.is_timeout() { "Connection timeout - please check your internet connection".to_string() } else if error.is_connect() { "Failed to connect - please check your internet connection".to_string() } else { error.to_string() } } #[derive(Parser, Debug, Clone)] #[command( name = "apt-add-repository", about = "Add APT repository to sources.list.d", long_about = None, after_help = "Examples: apt-add-repository ppa:user/ppa-name apt-add-repository http://ppa.launchpad.net/example/ppa/ubuntu apt-add-repository \"deb http://archive.ubuntu.com/ubuntu focal main\" apt-add-repository -s http://archive.ubuntu.com/ubuntu apt-add-repository -s # Enable existing disabled deb-src entries apt-add-repository -ss # Enable + create missing deb-src entries apt-add-repository \"ppa:user/ppa1 ppa:user/ppa2\" # Add multiple repositories echo \"ppa:user/ppa-name\" | apt-add-repository - cat repos.txt | apt-add-repository -" )] struct Args { /// Repository specification (PPA, URL, or full line, or '-' to read from stdin) #[arg(required_unless_present_any = ["list", "component", "enable_source"])] repository: Option, /// Don't update package cache after adding #[arg(short = 'n', long = "no-update")] no_update: bool, /// Enable source repositories (-s to enable existing, -ss to create missing) #[arg(short = 's', long = "source", action = clap::ArgAction::Count)] enable_source: u8, /// Assume yes to all queries #[arg(short = 'y', long = "yes")] assume_yes: bool, /// Remove the repository instead of adding #[arg(short = 'r', long = "remove")] remove: bool, /// Directory for sources files #[arg(short = 'd', long = "directory", default_value = DEFAULT_SOURCES_PATH)] directory: String, /// Directory for keyring files #[arg(long = "keyring-dir", default_value = DEFAULT_KEYRING_PATH)] keyring_dir: String, /// Add repository for specified pocket #[arg(short = 'p', long = "pocket")] pocket: Option, /// Use specified keyserver URL #[arg(short = 'k', long = "keyserver")] keyserver: Option, /// Add component to all matching repositories #[arg(long = "component", conflicts_with = "remove")] component: Option, /// Output format (deb822 or legacy) #[arg(long = "format", default_value = "deb822", value_parser = clap::value_parser!(OutputFormat))] format: OutputFormat, /// Preview changes without applying them #[arg(long = "dry-run")] dry_run: bool, /// Login to Launchpad to access private PPAs #[arg(long = "login")] login: bool, /// List all configured APT repositories #[arg(long = "list", conflicts_with_all = ["repository", "remove", "component", "pocket"])] list: bool, /// Refresh signing keys for all PPAs #[arg(long = "refresh-keys", conflicts_with_all = ["repository", "remove", "component", "pocket", "list"])] refresh_keys: bool, /// Use inline Signed-By instead of separate keyring files (recommended) #[arg(long = "inline-key")] inline_key: bool, } #[derive(Debug, Clone)] struct ParsedRepository { repository: Repository, filename: String, ppa_info: Option, } #[derive(Debug, Clone, Copy, PartialEq)] enum OutputFormat { Deb822, Legacy, } impl From for SourcesFileFormat { fn from(format: OutputFormat) -> Self { match format { OutputFormat::Deb822 => SourcesFileFormat::Deb822, OutputFormat::Legacy => SourcesFileFormat::Legacy, } } } impl std::str::FromStr for OutputFormat { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "deb822" | "sources" => Ok(OutputFormat::Deb822), "legacy" | "list" => Ok(OutputFormat::Legacy), _ => Err(format!("Invalid format: {}. Use 'deb822' or 'legacy'", s)), } } } #[derive(Debug)] enum RepositorySpec { Component(String), Pocket(String), Repository(ParsedRepository), } // PpaInfo struct is now imported from apt_sources::ppa // get_distribution_info is replaced by Distribution::current() from apt_sources::distribution // get_system_info is now imported from apt_sources::distribution fn get_distribution_specific_filename( base_name: &str, extension: &str, distribution: &Distribution, ) -> String { let dist_name = distribution.to_string().to_lowercase(); // For main repos (base_name == "custom" or contains the distribution name), use distribution name if base_name == "custom" || base_name.contains(&dist_name) { format!("{}.{}", dist_name, extension) } else { format!("{}.{}", base_name, extension) } } fn determine_repository_filepath( parsed: &ParsedRepository, args: &Args, distribution: &Distribution, ) -> Result { // Check if this is a main distribution repository let is_main_repo = distribution.is_main_repository(&parsed.repository); if is_main_repo { // For main distribution repositories, prefer to use the main distribution file let main_file = match (&distribution, args.format) { (Distribution::Ubuntu, OutputFormat::Deb822) => { Path::new(&args.directory).join("ubuntu.sources") } (Distribution::Ubuntu, OutputFormat::Legacy) => { get_main_sources_list_path(&args.directory) } (Distribution::Debian, OutputFormat::Deb822) => { Path::new(&args.directory).join("debian.sources") } (Distribution::Debian, OutputFormat::Legacy) => { get_main_sources_list_path(&args.directory) } _ => { // For other distributions, fall back to sources.list.d return Ok(Path::new(&args.directory).join(&parsed.filename)); } }; Ok(main_file) } else { // For third-party repositories, use sources.list.d Ok(Path::new(&args.directory).join(&parsed.filename)) } } fn is_main_distribution_file(path: &Path) -> bool { if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { matches!( filename, "sources.list" | "ubuntu.sources" | "ubuntu.list" | "debian.sources" | "debian.list" ) } else { false } } /// Derive the path to sources.list from the sources.list.d directory. /// For example, /etc/apt/sources.list.d -> /etc/apt/sources.list fn get_main_sources_list_path(sources_dir: &str) -> PathBuf { Path::new(sources_dir) .parent() .unwrap_or(Path::new("/etc/apt")) .join("sources.list") } // download_key_from_keyserver - wrapper with progress indicator fn download_key_from_keyserver(fingerprint: &str, keyserver_url: &str) -> Result { let spinner = ProgressBar::new_spinner(); spinner.set_style( ProgressStyle::default_spinner() .template("{spinner:.green} {msg}") .unwrap(), ); spinner.set_message(format!( "Downloading key {} from keyserver {}...", fingerprint, keyserver_url )); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); let result = apt_sources::keyserver::download_key_from_keyserver_sync(fingerprint, keyserver_url); match result { Ok(key_data) => { spinner.finish_with_message(format!("Successfully downloaded key {}", fingerprint)); Ok(key_data) } Err(e) => { spinner.finish_and_clear(); Err(e) } } } #[cfg(feature = "launchpad")] fn validate_ppa_launchpadlib( ppa_info: &PpaInfo, auth_required: bool, ) -> Result { debug!( "Validating PPA {}/{} using launchpadlib", ppa_info.user, ppa_info.name ); validate_ppa(ppa_info, auth_required).map_err(|e| e.to_string()) } /// Validate PPA and provide user-friendly error messages fn validate_ppa_with_suggestions(ppa_info: &PpaInfo) -> Result { info!( "Checking if PPA {}/{} exists...", ppa_info.user, ppa_info.name ); let result = validate_ppa(ppa_info, false).map_err(|e| e.to_string())?; if !result.exists { if result.is_private { return Err(format!( "PPA {}/{} is private.\n\ To access private PPAs:\n\ 1. Use the --login flag to authenticate with Launchpad\n\ 2. Ensure you're subscribed to this PPA\n\ 3. Check that apt-add-repository was built with launchpad support", ppa_info.user, ppa_info.name )); } else { return Err(format!( "PPA {}/{} not found.\n\ Suggestions:\n\ - Check the PPA name for typos\n\ - Verify the user '{}' exists on Launchpad\n\ - Browse available PPAs at https://launchpad.net/~{}/+ppas", ppa_info.user, ppa_info.name, ppa_info.user, ppa_info.user )); } } Ok(result) } #[cfg(feature = "launchpad")] fn get_private_ppa_url(ppa_info: &PpaInfo) -> Result { debug!( "Getting private PPA URL for {}/{}", ppa_info.user, ppa_info.name ); apt_sources::launchpad::get_private_ppa_url(ppa_info).map_err(|e| e.to_string()) } #[cfg(feature = "launchpad")] fn download_ppa_key_launchpadlib( ppa_info: &PpaInfo, auth_required: bool, ) -> Result { debug!( "Using launchpadlib to download PPA key for {}/{}", ppa_info.user, ppa_info.name ); let signing_key = download_ppa_signing_key(ppa_info, auth_required).map_err(|e| e.to_string())?; // Additionally check expiration let cert = Cert::from_str(&signing_key.key_data) .map_err(|e| format!("Failed to parse PGP key for expiration check: {}", e))?; if let Some(expiration_error) = apt_sources::key_management::check_key_expiration(&cert) { let expiration_warning = expiration_error.to_string(); if expiration_warning.contains("expired") { error!("Key expiration: {}", expiration_warning); return Err(format!("Cannot use expired key: {}", expiration_warning)); } else { warn!("Key expiration warning: {}", expiration_warning); } } info!( "Downloaded and verified PPA signing key with fingerprint: {}", signing_key.fingerprint ); Ok(signing_key.key_data) } // verify_key_fingerprint - wrapper that adds expiration check and logging fn verify_key_fingerprint(key_data: &str, expected_fingerprint: &str) -> Result<(), String> { debug!( "Verifying key fingerprint against expected: {}", expected_fingerprint ); // Use the crate's fingerprint verification apt_sources::key_management::verify_key_fingerprint(key_data, expected_fingerprint) .map_err(|e| e.to_string())?; // Additionally check expiration let cert = Cert::from_str(key_data) .map_err(|e| format!("Failed to parse PGP key for expiration check: {}", e))?; if let Some(expiration_error) = apt_sources::key_management::check_key_expiration(&cert) { let expiration_warning = expiration_error.to_string(); if expiration_warning.contains("expired") { error!("Key expiration: {}", expiration_warning); return Err(format!("Cannot use expired key: {}", expiration_warning)); } else { warn!("Key expiration warning: {}", expiration_warning); } } debug!("Key fingerprint verification successful"); Ok(()) } fn download_ppa_key(ppa_info: &PpaInfo, keyserver: Option<&str>) -> Result { info!( "Downloading signing key for PPA {}/{}...", ppa_info.user, ppa_info.name ); let key_data = if let Some(keyserver_url) = keyserver { // Use custom keyserver - need to get fingerprint first debug!("Using custom keyserver: {}", keyserver_url); let api_url = format!( "https://api.launchpad.net/1.0/~{}/+archive/ubuntu/{}", ppa_info.user, ppa_info.name ); let client = create_http_client()?; let ppa_response = client .get(&api_url) .header("Accept", "application/json") .send() .map_err(|e| format!("Failed to fetch PPA metadata: {}", format_network_error(&e)))?; if !ppa_response.status().is_success() { return Err(format!( "PPA not found: {}/{}", ppa_info.user, ppa_info.name )); } let ppa_data: serde_json::Value = ppa_response .json() .map_err(|e| format!("Failed to parse PPA metadata: {}", e))?; let fingerprint = ppa_data .get("signing_key_fingerprint") .and_then(|v| v.as_str()) .ok_or_else(|| "PPA has no signing key configured".to_string())?; let key = download_key_from_keyserver(fingerprint, keyserver_url)?; // Verify the key fingerprint matches what we expect verify_key_fingerprint(&key, fingerprint)?; key } else { // Use library function for default Launchpad download download_ppa_signing_key(ppa_info, false) .map_err(|e| e.to_string())? .key_data }; info!("Downloaded PPA signing key"); Ok(key_data) } fn save_key_to_keyring( key_data: &str, keyring_dir: &str, ppa_info: &PpaInfo, ) -> Result { // The key data from Launchpad is already in armored format, // but let's parse it to validate it's a proper key let cert = Cert::from_str(key_data).map_err(|e| format!("Failed to parse PGP key: {}", e))?; // Verify the key is valid let policy = sequoia_openpgp::policy::StandardPolicy::new(); if cert.with_policy(&policy, None).is_err() { return Err("Invalid or expired PGP key".to_string()); } // Save to keyring directory // Use .asc extension for ASCII-armored keys (apt prefers this) let keyring_path = Path::new(keyring_dir).join(ppa_info.keyring_filename()); // Check if keyring directory exists if !Path::new(keyring_dir).exists() { fs::create_dir_all(keyring_dir) .map_err(|e| format!("Failed to create keyring directory: {}", e))?; } // Write the key in ASCII-armored format // Since the data from Launchpad is already armored, we can write it directly fs::write(&keyring_path, key_data) .map_err(|e| format!("Failed to write key to file: {}", e))?; Ok(keyring_path) } /// Save the PPA key and return the appropriate Signature. /// If inline_key is true and format is Deb822, tries inline signature first, /// falling back to keyring file on failure. fn save_ppa_key( key_data: &str, ppa_info: &PpaInfo, keyring_dir: &str, use_inline: bool, ) -> Option { if use_inline { match create_inline_signature(key_data) { Ok(inline_sig) => { info!("Using inline Signed-By (embedded key)"); return Some(inline_sig); } Err(e) => { warn!("Failed to create inline signature: {}", e); warn!("Falling back to keyring file"); } } } match save_key_to_keyring(key_data, keyring_dir, ppa_info) { Ok(keyring_path) => { info!("Signing key saved to {}", keyring_path.display()); Some(Signature::KeyPath(keyring_path)) } Err(e) => { warn!("Failed to save signing key: {}", e); warn!("The repository will be added without signature verification."); None } } } fn parse_repository_line( line: &str, ) -> Result< ( Vec, Url, String, Vec, Option, ), String, > { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.is_empty() { return Err("Empty repository line".to_string()); } let mut idx = 0; let mut signed_by = None; let mut _arch = None; // Parse repository type let types = match parts[idx] { "deb" => vec![RepositoryType::Binary], "deb-src" => vec![RepositoryType::Source], _ => return Err("Repository line must start with 'deb' or 'deb-src'".to_string()), }; idx += 1; // Parse options if present if idx < parts.len() && parts[idx].starts_with('[') { let options_end = parts .iter() .position(|&p| p.ends_with(']')) .ok_or("Unclosed options bracket")?; let options_str = parts[idx..=options_end].join(" "); let options_str = options_str.trim_start_matches('[').trim_end_matches(']'); for option in options_str.split_whitespace() { if let Some((key, value)) = option.split_once('=') { match key { "arch" => _arch = Some(value.to_string()), "signed-by" => signed_by = Some(PathBuf::from(value)), _ => {} // Ignore other options for now } } } idx = options_end + 1; } if idx + 2 >= parts.len() { return Err("Invalid repository line format".to_string()); } let uri = Url::parse(parts[idx]).map_err(|e| format!("Invalid URL: {}", e))?; // Strip auth data from URL for storage let (clean_uri, auth_data) = strip_auth_from_url(&uri); if auth_data.is_some() { debug!("Repository URL contains authentication data, stripping for storage"); } let suite = parts[idx + 1].to_string(); let components = parts[idx + 2..].iter().map(|&s| s.to_string()).collect(); Ok((types, clean_uri, suite, components, signed_by)) } fn parse_repository_spec( spec: &str, enable_source_count: u8, args: &Args, distribution: &Distribution, codename: &str, ) -> Result { // Check if it's just a component name (universe, multiverse, etc.) let valid_components = match &distribution { Distribution::Ubuntu => vec!["main", "universe", "multiverse", "restricted"], Distribution::Debian => vec!["main", "contrib", "non-free", "non-free-firmware"], Distribution::Other(_) => vec![ "main", "universe", "multiverse", "restricted", "contrib", "non-free", "non-free-firmware", ], }; if valid_components.contains(&spec) { debug!( "Adding component {} to existing {} repositories", spec, distribution ); return Ok(RepositorySpec::Component(spec.to_string())); } // Check if it's a pocket specification (e.g., focal-proposed, bionic-backports) let valid_pockets = vec!["proposed", "backports", "security", "updates"]; for pocket in &valid_pockets { if spec.ends_with(&format!("-{}", pocket)) { debug!("Adding pocket {} repositories", spec); return Ok(RepositorySpec::Pocket(spec.to_string())); } } // Check if it's a PPA if spec.starts_with("ppa:") { let ppa_info = PpaInfo::parse(spec)?; let types = if enable_source_count > 0 { HashSet::from([RepositoryType::Binary, RepositoryType::Source]) } else { HashSet::from([RepositoryType::Binary]) }; // Generate PPA URL (with authentication for private PPAs if needed) let uri = if args.login { // For private PPAs, we need to get the subscription URL from launchpadlib #[cfg(feature = "launchpad")] { get_private_ppa_url(&ppa_info)? } #[cfg(not(feature = "launchpad"))] { return Err("Private PPA support requires the 'launchpad' feature".to_string()); } } else { Url::parse(&format!( "{}/{}/{}/ubuntu", LAUNCHPAD_PPA_URL, ppa_info.user, ppa_info.name )) .unwrap() }; // Validate components for PPA let components = if let Some(ref comps) = args.component { // User specified components - validate them let comp_vec = vec![comps.clone()]; validate_ppa_components(&comp_vec)?; comp_vec } else { vec!["main".to_string()] }; let repository = Repository { enabled: Some(true), types, architectures: None, uris: vec![uri], suites: vec![codename.to_string()], components: Some(components), signature: None, // Will be set after key download x_repolib_name: Some(format!("ppa:{}/{}", ppa_info.user, ppa_info.name)), ..Default::default() }; let extension = match args.format { OutputFormat::Legacy => "list", OutputFormat::Deb822 => "sources", }; let filename = format!( "{}-ubuntu-{}-{}.{}", ppa_info.user, ppa_info.name, codename, extension ); return Ok(RepositorySpec::Repository(ParsedRepository { repository, filename, ppa_info: Some(ppa_info), })); } // Check if it's a full deb line if spec.starts_with("deb ") || spec.starts_with("deb-src ") { let (parsed_types, uri, suite, components, signed_by) = parse_repository_line(spec)?; // Check if this is a PPA URL and validate components if uri .host_str() .map_or(false, |h| h.contains("ppa.launchpadcontent.net")) { validate_ppa_components(&components)?; } let mut types = HashSet::new(); for t in parsed_types { types.insert(t); } if enable_source_count > 0 { types.insert(RepositoryType::Source); } let mut repository = Repository { enabled: Some(true), types, architectures: None, uris: vec![uri.clone()], suites: vec![suite], components: Some(components), ..Default::default() }; if let Some(keypath) = signed_by { repository.signature = Some(Signature::KeyPath(keypath)); } let filename = generate_filename(uri.as_str(), args.format, &distribution); return Ok(RepositorySpec::Repository(ParsedRepository { repository, filename, ppa_info: None, })); } // Try to parse as URL let uri = Url::parse(spec).map_err(|_| { "Invalid repository specification. Expected PPA, URL, or full deb line".to_string() })?; // Strip auth data from URL for storage let (clean_uri, auth_data) = strip_auth_from_url(&uri); if auth_data.is_some() { info!("Repository URL contains authentication data, which will be stored securely"); } // Use system codename and default to 'main' component let (suite, components) = default_suite_components(&codename); let types = if enable_source_count > 0 { HashSet::from([RepositoryType::Binary, RepositoryType::Source]) } else { HashSet::from([RepositoryType::Binary]) }; let repository = Repository { enabled: Some(true), types, architectures: None, uris: vec![clean_uri.clone()], suites: vec![suite], components: Some(components), ..Default::default() }; let filename = generate_filename(uri.as_str(), args.format, &distribution); Ok(RepositorySpec::Repository(ParsedRepository { repository, filename, ppa_info: None, })) } fn default_suite_components(system_codename: &str) -> (String, Vec) { // Match original apt-add-repository behavior: // - Suite defaults to system codename // - Components default to 'main' (system_codename.to_string(), vec!["main".to_string()]) } fn generate_filename(url: &str, format: OutputFormat, distribution: &Distribution) -> String { let extension = match format { OutputFormat::Legacy => "list", OutputFormat::Deb822 => "sources", }; let Ok(parsed_url) = Url::parse(url) else { return get_distribution_specific_filename("custom", extension, distribution); }; // Check if this is a main distribution repository if distribution.is_main_repository(&Repository { uris: vec![parsed_url.clone()], ..Default::default() }) { return get_distribution_specific_filename("custom", extension, distribution); } // Use library function for non-main repos apt_sources::utils::generate_filename_from_url(&parsed_url, extension) } fn add_pocket_repository( pocket_spec: &str, args: &Args, enable_source_count: u8, distribution: &Distribution, ) -> Result<(), String> { info!("Adding pocket repository: {}", pocket_spec); // Parse the pocket specification (e.g., "focal-proposed" -> suite="focal-proposed", pocket="proposed") let parts: Vec<&str> = pocket_spec.rsplitn(2, '-').collect(); if parts.len() != 2 { return Err("Invalid pocket specification".to_string()); } let pocket = parts[0]; let _base_suite = parts[1]; // Create repository for the pocket let types = if enable_source_count > 0 { HashSet::from([RepositoryType::Binary, RepositoryType::Source]) } else { HashSet::from([RepositoryType::Binary]) }; // Determine components based on distribution // All pockets (proposed, backports, security, updates) use the same components let components: Vec = match &distribution { Distribution::Ubuntu => vec!["main", "universe", "multiverse", "restricted"], Distribution::Debian => vec!["main", "contrib", "non-free"], Distribution::Other(_) => distribution.default_components(), } .into_iter() .map(|s| s.to_string()) .collect(); // Use distribution-specific mirror URLs let mirror_url = match &distribution { Distribution::Ubuntu => "http://archive.ubuntu.com/ubuntu/", Distribution::Debian => "http://deb.debian.org/debian/", Distribution::Other(name) => { return Err(format!( "Pocket repository support not available for distribution '{}'", name )); } }; let repository = Repository { enabled: Some(true), types, architectures: None, uris: vec![Url::parse(mirror_url).unwrap()], suites: vec![pocket_spec.to_string()], components: Some(components), ..Default::default() }; let extension = match args.format { OutputFormat::Legacy => "list", OutputFormat::Deb822 => "sources", }; // Use distribution-specific filename let filename = format!( "{}-{}.{}", distribution.to_string().to_lowercase(), pocket, extension ); let parsed = ParsedRepository { repository, filename, ppa_info: None, }; add_parsed_repository(parsed, args, distribution) } fn add_component_to_existing_repos( component: &str, directory: &str, dry_run: bool, distribution: &Distribution, ) -> Result<(), String> { info!( "Adding component '{}' to existing {} repositories", component, distribution ); // Find all .sources files that contain distribution repositories let sources_dir = Path::new(directory); if !sources_dir.exists() { return Err(format!("Sources directory {} does not exist", directory)); } let mut modified_count = 0; for entry in fs::read_dir(sources_dir).map_err(|e| format!("Failed to read directory: {}", e))? { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); if path.extension().and_then(|s| s.to_str()) != Some("sources") { continue; } // Read and parse the repository file let content = fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let repos = Repositories::from_str(&content) .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?; let mut modified = false; let mut updated_repos = Vec::new(); for repo in repos.iter() { let mut repo_clone = repo.clone(); // Check if this is a main distribution repository and doesn't already have the component let dominated = distribution.is_main_repository(repo) && repo_clone .components .as_ref() .map_or(false, |c| !c.contains(&component.to_string())); if dominated { if let Some(components) = &mut repo_clone.components { if dry_run { info!("Would add component '{}' to {}", component, path.display()); } else { components.push(component.to_string()); info!("Adding component '{}' to {}", component, path.display()); } modified = true; } } updated_repos.push(repo_clone); } if modified && !dry_run { let updated_repositories = Repositories::new(updated_repos); let output = updated_repositories.to_string(); fs::write(&path, output) .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; modified_count += 1; } } if modified_count == 0 && !dry_run { warn!( "No {} repositories found to add component '{}' to", distribution, component ); } else if !dry_run { info!("Modified {} repository file(s)", modified_count); } Ok(()) } fn enable_sources_globally(args: &Args) -> Result<(), String> { info!( "Enabling source repositories globally (level: {})", args.enable_source ); let mut enabled_count = 0; let mut created_count = 0; let mut warned_count = 0; let mut errors = Vec::new(); // Scan both .sources and .list files let sources_dir = Path::new(&args.directory); if sources_dir.exists() { for entry in fs::read_dir(sources_dir).map_err(|e| format!("Failed to read directory: {}", e))? { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); if !path.is_file() { continue; } // Handle .sources files (DEB822 format) if path.extension().and_then(|s| s.to_str()) == Some("sources") { match enable_sources_in_deb822_file(&path, args.enable_source, args.dry_run) { Ok((enabled, created)) => { enabled_count += enabled; created_count += created; } Err(e) => { errors.push(format!("{}: {}", path.display(), e)); } } } // Handle .list files (legacy format) else if path.extension().and_then(|s| s.to_str()) == Some("list") { match enable_sources_in_legacy_file(&path, args.enable_source, args.dry_run) { Ok((enabled, created, warned)) => { enabled_count += enabled; created_count += created; warned_count += warned; } Err(e) => { errors.push(format!("{}: {}", path.display(), e)); } } } } } // Also handle main sources.list let main_sources = get_main_sources_list_path(&args.directory); if main_sources.exists() { match enable_sources_in_legacy_file(&main_sources, args.enable_source, args.dry_run) { Ok((enabled, created, warned)) => { enabled_count += enabled; created_count += created; warned_count += warned; } Err(e) => { errors.push(format!("sources.list: {}", e)); } } } // Report results if args.dry_run { info!("Dry run complete - would have:"); } else { info!("Source enablement complete:"); } if enabled_count > 0 { info!(" Enabled {} disabled deb-src entries", enabled_count); } if created_count > 0 { info!(" Created {} new deb-src entries", created_count); } if warned_count > 0 && args.enable_source == 1 { info!( " Found {} missing deb-src entries (use -ss to create them)", warned_count ); } if !errors.is_empty() { error!("Errors encountered:"); for err in errors { error!(" {}", err); } return Err("Some operations failed".to_string()); } if enabled_count == 0 && created_count == 0 && warned_count == 0 { info!("No source repositories needed enabling or creation"); } Ok(()) } fn enable_sources_in_deb822_file( file_path: &Path, enable_source_count: u8, dry_run: bool, ) -> Result<(u32, u32), String> { debug!("Processing DEB822 file: {}", file_path.display()); let content = fs::read_to_string(file_path).map_err(|e| format!("Failed to read file: {}", e))?; let repos = Repositories::from_str(&content) .map_err(|e| format!("Failed to parse DEB822 file: {}", e))?; let mut enabled_count = 0; let mut created_count = 0; let mut modified = false; let mut updated_repos = Vec::new(); // First pass: Enable disabled deb-src entries that have matching enabled deb entries for repo in repos.iter() { let mut repo_clone = repo.clone(); // Only process disabled source-only repos with a matching binary repo let dominated = !repo.enabled.unwrap_or(true) && repo.types.contains(&RepositoryType::Source) && !repo.types.contains(&RepositoryType::Binary) && find_matching_binary_repo(&repos, repo).is_some(); if dominated { if dry_run { info!("Would enable disabled deb-src in {}", file_path.display()); } else { repo_clone.enabled = Some(true); info!("Enabled disabled deb-src in {}", file_path.display()); } enabled_count += 1; modified = true; } updated_repos.push(repo_clone); } // Second pass: Create missing deb-src entries if enable_source > 1 if enable_source_count > 1 { let mut new_repos = Vec::new(); for repo in repos.iter() { // Only process enabled binary-only repos without a matching source repo let dominated = repo.enabled.unwrap_or(true) && repo.types.contains(&RepositoryType::Binary) && !repo.types.contains(&RepositoryType::Source) && !has_matching_source_repo(&repos, repo); if !dominated { continue; } // Create a new source repository let mut source_repo = repo.clone(); source_repo.types = HashSet::from([RepositoryType::Source]); if dry_run { info!( "Would create missing deb-src for binary repo in {}", file_path.display() ); } else { new_repos.push(source_repo); info!( "Created missing deb-src for binary repo in {}", file_path.display() ); } created_count += 1; modified = true; } // Add new repositories to updated list if !dry_run { updated_repos.extend(new_repos); } } // Write changes if modified and not dry run if modified && !dry_run { let updated_repositories = Repositories::new(updated_repos); let output = updated_repositories.to_string(); fs::write(file_path, output).map_err(|e| format!("Failed to write file: {}", e))?; } Ok((enabled_count, created_count)) } fn enable_sources_in_legacy_file( file_path: &Path, enable_source_count: u8, dry_run: bool, ) -> Result<(u32, u32, u32), String> { debug!("Processing legacy file: {}", file_path.display()); let content = fs::read_to_string(file_path).map_err(|e| format!("Failed to read file: {}", e))?; let mut lines: Vec = content.lines().map(|s| s.to_string()).collect(); let mut enabled_count = 0; let mut created_count = 0; let mut warned_count = 0; let mut modified = false; // First pass: Enable disabled deb-src lines for i in 0..lines.len() { let trimmed = lines[i].trim(); if trimmed.starts_with("# deb-src ") || trimmed.starts_with("#deb-src ") { // This is a commented-out deb-src line let uncommented = trimmed.trim_start_matches("# ").trim_start_matches("#"); // Check if there's a matching enabled deb line let lines_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect(); if has_matching_deb_line(&lines_refs, uncommented) { if dry_run { info!( "Would enable disabled deb-src line in {}", file_path.display() ); } else { lines[i] = uncommented.to_string(); info!("Enabled disabled deb-src line in {}", file_path.display()); } enabled_count += 1; modified = true; } } } // Second pass: Handle missing deb-src entries let mut new_lines = Vec::new(); for line in &lines { let trimmed = line.trim(); if trimmed.starts_with("deb ") && !trimmed.starts_with("deb-src ") { // This is an enabled deb line let source_line = trimmed.replace("deb ", "deb-src "); // Check if there's already a matching deb-src line let lines_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect(); if !has_matching_source_line(&lines_refs, &source_line) { if enable_source_count == 1 { // Just warn about missing source debug!("Missing deb-src for: {}", trimmed); warned_count += 1; } else if enable_source_count > 1 { // Create the missing deb-src line if dry_run { info!( "Would create missing deb-src line in {}", file_path.display() ); } else { new_lines.push(source_line); info!("Created missing deb-src line in {}", file_path.display()); } created_count += 1; modified = true; } } } } // Add new lines if !new_lines.is_empty() && !dry_run { lines.extend(new_lines); modified = true; } // Write changes if modified and not dry run if modified && !dry_run { let output = lines.join("\n") + "\n"; fs::write(file_path, output).map_err(|e| format!("Failed to write file: {}", e))?; } Ok((enabled_count, created_count, warned_count)) } fn find_matching_binary_repo<'a>( repos: &'a Repositories, source_repo: &Repository, ) -> Option<&'a Repository> { for repo in repos.iter() { if !repo.enabled.unwrap_or(true) { continue; // Skip disabled repositories } // Check if this is a binary repository with matching URIs, suites, and components if repo.types.contains(&RepositoryType::Binary) && repo.uris == source_repo.uris && repo.suites == source_repo.suites && repo.components == source_repo.components { return Some(repo); } } None } fn has_matching_source_repo(repos: &Repositories, binary_repo: &Repository) -> bool { for repo in repos.iter() { // Check if this is a source repository with matching URIs, suites, and components if repo.types.contains(&RepositoryType::Source) && repo.uris == binary_repo.uris && repo.suites == binary_repo.suites && repo.components == binary_repo.components { return true; } } false } fn has_matching_deb_line(lines: &[&str], deb_src_line: &str) -> bool { // Convert deb-src line to equivalent deb line let deb_line = deb_src_line.replace("deb-src ", "deb "); for line in lines { let trimmed = line.trim(); if trimmed == deb_line { return true; } } false } fn has_matching_source_line(lines: &[&str], source_line: &str) -> bool { for line in lines { let trimmed = line.trim(); // Check both enabled and disabled source lines if trimmed == source_line || trimmed == format!("# {}", source_line) || trimmed == format!("#{}", source_line) { return true; } } false } fn add_repositories_from_stdin( args: &Args, distribution: &Distribution, codename: &str, ) -> Result<(), String> { info!("Reading repository specifications from stdin..."); use std::io::{BufRead, BufReader}; let stdin = io::stdin(); let reader = BufReader::new(stdin); let mut success_count = 0; let mut error_count = 0; let mut errors = Vec::new(); for (line_num, line_result) in reader.lines().enumerate() { let line = match line_result { Ok(l) => l, Err(e) => { error!("Failed to read line {}: {}", line_num + 1, e); error_count += 1; continue; } }; // Skip empty lines and comments let trimmed = line.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } info!("Processing: {}", trimmed); // Create a temporary args with the repository from stdin let mut temp_args = args.clone(); temp_args.repository = Some(trimmed.to_string()); // Process this repository match add_single_repository(&temp_args, distribution, codename) { Ok(()) => { success_count += 1; } Err(e) => { error_count += 1; errors.push(format!("Line {}: {} - {}", line_num + 1, trimmed, e)); } } } // Report results if error_count > 0 { error!( "Added {} repositories, {} failed:", success_count, error_count ); for err in errors { error!(" {}", err); } return Err(format!("{} repositories failed to add", error_count)); } else if success_count == 0 { warn!("No repositories were added (empty input or all lines were comments)"); } else { info!("Successfully added {} repositories", success_count); } Ok(()) } fn add_single_repository( args: &Args, distribution: &Distribution, codename: &str, ) -> Result<(), String> { let repository = args .repository .as_ref() .ok_or_else(|| "Repository specification required".to_string())?; debug!("Adding repository: {}", repository); // Handle -p/--pocket flag if let Some(pocket) = &args.pocket { let pocket_spec = format!("{}-{}", codename, pocket); return add_pocket_repository(&pocket_spec, args, args.enable_source, distribution); } let spec = parse_repository_spec(repository, args.enable_source, args, distribution, codename)?; match spec { RepositorySpec::Component(component) => { add_component_to_existing_repos(&component, &args.directory, args.dry_run, distribution) } RepositorySpec::Pocket(pocket_spec) => { add_pocket_repository(&pocket_spec, args, args.enable_source, distribution) } RepositorySpec::Repository(parsed) => add_parsed_repository(parsed, args, distribution), } } fn add_repository(args: &Args, distribution: &Distribution, codename: &str) -> Result<(), String> { let repository = args .repository .as_ref() .ok_or_else(|| "Repository specification required".to_string())?; // Check if we should read from stdin if repository == "-" { return add_repositories_from_stdin(args, distribution, codename); } // Check if multiple repositories are specified (space-separated) let repositories: Vec<&str> = repository.split_whitespace().collect(); if repositories.len() > 1 { info!("Adding {} repositories...", repositories.len()); return add_multiple_repositories(&repositories, args, distribution, codename); } // Otherwise, add single repository add_single_repository(args, distribution, codename) } fn add_multiple_repositories( repositories: &[&str], args: &Args, distribution: &Distribution, codename: &str, ) -> Result<(), String> { let mut success_count = 0; let mut error_count = 0; let mut errors = Vec::new(); // Create progress bar let pb = ProgressBar::new(repositories.len() as u64); pb.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}") .unwrap() .progress_chars("#>-"), ); pb.set_message("Adding repositories..."); for (_index, repo_spec) in repositories.iter().enumerate() { pb.set_message(format!("Processing: {}", repo_spec)); // Create a temporary args with the current repository let mut temp_args = args.clone(); temp_args.repository = Some(repo_spec.to_string()); // Add this repository match add_single_repository(&temp_args, distribution, codename) { Ok(()) => { success_count += 1; pb.println(format!("✓ Successfully added: {}", repo_spec)); } Err(e) => { error_count += 1; errors.push(format!("Repository '{}': {}", repo_spec, e)); pb.println(format!("✗ Failed to add '{}': {}", repo_spec, e)); } } pb.inc(1); } pb.finish_with_message("Done"); // Summary info!( "\nSummary: {} succeeded, {} failed", success_count, error_count ); if error_count > 0 { error!("\nThe following repositories failed to add:"); for err in &errors { error!(" - {}", err); } return Err(format!( "{} out of {} repositories failed to add", error_count, repositories.len() )); } Ok(()) } /// Check a DEB822 sources file for duplicate repositories fn check_deb822_for_duplicate( path: &Path, new_uris: &HashSet, new_suites: &HashSet, new_components: &HashSet, new_types: &HashSet, ) -> Option<(PathBuf, String)> { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => { debug!("Failed to read {}: {}", path.display(), e); return None; } }; let repos = match Repositories::from_str(&content) { Ok(r) => r, Err(e) => { debug!("Failed to parse {}: {}", path.display(), e); return None; } }; for repo in repos.iter() { if is_duplicate_repository(repo, new_uris, new_suites, new_components, new_types) { return Some(( path.to_path_buf(), format!("DEB822 format in {}", path.display()), )); } } None } /// Check a legacy .list file for duplicate repositories fn check_legacy_for_duplicate( path: &Path, new_uris: &HashSet, new_suites: &HashSet, new_components: &HashSet, new_types: &HashSet, ) -> Option<(PathBuf, String)> { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => { debug!("Failed to read {}: {}", path.display(), e); return None; } }; for (line_num, line) in content.lines().enumerate() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let Ok(repos) = line.parse::() else { continue; }; for legacy_repo in repos.iter() { let repo = Repository::from(legacy_repo); if is_duplicate_repository(&repo, new_uris, new_suites, new_components, new_types) { return Some(( path.to_path_buf(), format!( "Legacy format in {} at line {}", path.display(), line_num + 1 ), )); } } } None } fn find_duplicate_repository( new_repo: &Repository, sources_dir: &str, ) -> Result, String> { debug!("Checking for duplicate repositories"); // Normalize the new repository for comparison let new_uris: HashSet = new_repo.uris.iter().map(|u| u.to_string()).collect(); let new_suites: HashSet = new_repo.suites.iter().cloned().collect(); let new_components: HashSet = new_repo .components .as_ref() .map(|c| c.iter().cloned().collect()) .unwrap_or_default(); let new_types: HashSet = new_repo.types.clone(); // Scan sources.list.d directory let sources_path = Path::new(sources_dir); if sources_path.exists() { for entry in fs::read_dir(sources_path).map_err(|e| format!("Failed to read directory: {}", e))? { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); if !path.is_file() { continue; } let ext = path.extension().and_then(|s| s.to_str()); let result = match ext { Some("sources") => check_deb822_for_duplicate( &path, &new_uris, &new_suites, &new_components, &new_types, ), Some("list") => check_legacy_for_duplicate( &path, &new_uris, &new_suites, &new_components, &new_types, ), _ => None, }; if result.is_some() { return Ok(result); } } } // Also check main sources.list let main_sources = get_main_sources_list_path(sources_dir); if main_sources.exists() { if let Some(result) = check_legacy_for_duplicate( &main_sources, &new_uris, &new_suites, &new_components, &new_types, ) { return Ok(Some(result)); } } Ok(None) } fn is_duplicate_repository( existing: &Repository, new_uris: &HashSet, new_suites: &HashSet, new_components: &HashSet, new_types: &HashSet, ) -> bool { // Check if disabled if !existing.enabled.unwrap_or(true) { return false; } // Compare URIs (must have at least one matching URI) let existing_uris: HashSet = existing.uris.iter().map(|u| u.to_string()).collect(); if existing_uris.is_disjoint(new_uris) { return false; } // Compare suites (must have at least one matching suite) let existing_suites: HashSet = existing.suites.iter().cloned().collect(); if existing_suites.is_disjoint(new_suites) { return false; } // Compare components (must have overlapping components) let existing_components: HashSet = existing .components .as_ref() .map(|c| c.iter().cloned().collect()) .unwrap_or_default(); if !existing_components.is_empty() && !new_components.is_empty() && existing_components.is_disjoint(new_components) { return false; } // Compare types (must have at least one matching type) let existing_types: HashSet = existing.types.clone(); !existing_types.is_disjoint(new_types) } fn add_parsed_repository( mut parsed: ParsedRepository, args: &Args, distribution: &Distribution, ) -> Result<(), String> { // Check for duplicate repository before proceeding debug!("Checking for duplicate repository"); match find_duplicate_repository(&parsed.repository, &args.directory) { Ok(Some((_file_path, location))) => { if args.assume_yes { warn!("Repository already exists in {}", location); warn!("Skipping duplicate repository"); return Ok(()); } else { eprint!( "Repository already exists in {}.\nAdd it anyway? [y/N] ", location ); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); if !input.trim().eq_ignore_ascii_case("y") { return Err("Repository already exists, not adding duplicate".to_string()); } info!("Adding duplicate repository as requested"); } } Ok(None) => { debug!("No duplicate repository found"); } Err(e) => { warn!("Error checking for duplicates: {}", e); // Continue anyway - better to add a potential duplicate than fail } } // Handle PPA key download if let Some(ref ppa_info) = parsed.ppa_info { // Validate PPA first info!("Checking PPA availability..."); #[cfg(feature = "launchpad")] let validation_result = if args.login || args.keyserver.is_none() { validate_ppa_launchpadlib(ppa_info, args.login)? } else { validate_ppa_with_suggestions(ppa_info)? }; #[cfg(not(feature = "launchpad"))] let validation_result = validate_ppa_with_suggestions(ppa_info)?; // Inform about debug symbols if available if validation_result.publishes_debug_symbols { info!("This PPA publishes debug symbols. To enable debug symbols, add the component 'main/debug'."); } if !args.assume_yes && !args.dry_run { eprint!( "You are about to add the following PPA: {} More info: https://launchpad.net/~{}/+archive/ubuntu/{} Press [ENTER] to continue or Ctrl-c to cancel.", validation_result.display_name, ppa_info.user, ppa_info.name ); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); } // Download and save the PPA signing key if args.dry_run { info!("Would download and verify PPA signing key"); let keyring_path = Path::new(&args.keyring_dir).join(ppa_info.keyring_filename()); info!("Would save signing key to {}", keyring_path.display()); parsed.repository.signature = Some(Signature::KeyPath(keyring_path)); } else { info!("Getting signing key for PPA..."); // Use launchpadlib if available and needed #[cfg(feature = "launchpad")] let key_result = if args.login || args.keyserver.is_none() { download_ppa_key_launchpadlib(ppa_info, args.login) } else { download_ppa_key(ppa_info, args.keyserver.as_deref()) }; #[cfg(not(feature = "launchpad"))] let key_result = download_ppa_key(ppa_info, args.keyserver.as_deref()); match key_result { Ok(key_data) => { let use_inline = args.inline_key && args.format == OutputFormat::Deb822; parsed.repository.signature = save_ppa_key(&key_data, ppa_info, &args.keyring_dir, use_inline); } Err(e) => { warn!("Failed to download PPA signing key: {}", e); warn!("The repository will be added without signature verification."); } } } } // Determine the appropriate file path let filepath = determine_repository_filepath(&parsed, args, distribution)?; // Check if directory exists let parent_dir = filepath .parent() .ok_or_else(|| "Invalid file path".to_string())?; if !parent_dir.exists() { return Err(format!( "Directory {} does not exist.\n\ Suggestions:\n\ - Create the directory with: sudo mkdir -p {}\n\ - Check if you have the correct permissions\n\ - Verify the path is correct", parent_dir.display(), parent_dir.display() )); } // Check if file already exists let file_exists = filepath.exists(); let is_main_file = is_main_distribution_file(&filepath); if file_exists && !is_main_file && !args.assume_yes { eprint!( "Repository file {} already exists. Overwrite? [y/N] ", filepath.display() ); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); if !input.trim().eq_ignore_ascii_case("y") { return Err("Aborted".to_string()); } } // Create content based on format let content = match args.format { OutputFormat::Legacy => { // Generate legacy .list format LegacyRepositories::from(&parsed.repository).to_string() } OutputFormat::Deb822 => { // Create Repositories container and serialize to DEB822 format let repos = Repositories::new(vec![parsed.repository.clone()]); repos.to_string() } }; if args.dry_run { // Show what would be written if file_exists && is_main_file { info!( "Would append repository to existing file {}", filepath.display() ); } else { info!("Would write repository to {}", filepath.display()); } info!("Repository content that would be written:"); for line in content.lines() { info!(" {}", line); } if !args.no_update { info!("Would run: apt update"); } } else { // Write to file - append if it's a main distribution file, otherwise overwrite if file_exists && is_main_file { // For main distribution files, append the repository match args.format { OutputFormat::Legacy => { // For legacy format, just append the lines let mut file = fs::OpenOptions::new() .append(true) .open(&filepath) .map_err(|e| format!("Failed to open file for appending: {}", e))?; // Add a newline if the file doesn't end with one let existing_content = fs::read_to_string(&filepath) .map_err(|e| format!("Failed to read existing file: {}", e))?; if !existing_content.ends_with('\n') { use std::io::Write; writeln!(file).map_err(|e| format!("Failed to write newline: {}", e))?; } // Write the new content write!(file, "{}", content) .map_err(|e| format!("Failed to append to file: {}", e))?; } OutputFormat::Deb822 => { // For DEB822 format, we need to merge repositories let existing_content = fs::read_to_string(&filepath) .map_err(|e| format!("Failed to read existing file: {}", e))?; let repos = Repositories::from_str(&existing_content) .map_err(|e| format!("Failed to parse existing repositories: {}", e))?; // Create a new vector with existing repos plus the new one let mut all_repos: Vec = repos.iter().cloned().collect(); all_repos.push(parsed.repository.clone()); // Write the merged content let merged_repos = Repositories::new(all_repos); fs::write(&filepath, merged_repos.to_string()) .map_err(|e| format!("Failed to write merged repositories: {}", e))?; } } info!("Repository appended to {}", filepath.display()); } else { // For non-main files, just write/overwrite fs::write(&filepath, &content).map_err(|e| format!("Failed to write file: {}", e))?; info!("Repository added to {}", filepath.display()); } // Update package cache unless disabled if !args.no_update { let spinner = ProgressBar::new_spinner(); spinner.set_style( ProgressStyle::default_spinner() .template("{spinner:.green} {msg}") .unwrap(), ); spinner.set_message("Updating package cache..."); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); let status = process::Command::new("apt") .arg("update") .status() .map_err(|e| { spinner.finish_and_clear(); format!("Failed to run apt update: {}", e) })?; if !status.success() { spinner.finish_with_message("apt update failed"); warn!("apt update failed"); } else { spinner.finish_with_message("Package cache updated successfully"); } } } Ok(()) } /// Warn about key expiration if applicable fn warn_key_expiration(key_data: &str, ppa_info: &PpaInfo) { let Ok(cert) = Cert::from_str(key_data) else { return; }; let policy = sequoia_openpgp::policy::StandardPolicy::new(); let Ok(valid_cert) = cert.with_policy(&policy, None) else { return; }; let Some(expiration) = valid_cert.primary_key().key_expiration_time() else { return; }; let now = std::time::SystemTime::now(); if expiration < now { warn!("Key for {}/{} has expired!", ppa_info.user, ppa_info.name); } else if let Ok(duration) = expiration.duration_since(now) { let days = duration.as_secs() / 86400; if days < 30 { warn!( "Key for {}/{} expires in {} days", ppa_info.user, ppa_info.name, days ); } } } /// Collect PPAs from DEB822 sources file fn collect_ppas_from_deb822( path: &Path, ppa_repos: &mut Vec<(PathBuf, PpaInfo, Option)>, ) { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => { warn!("Failed to read {}: {}", path.display(), e); return; } }; let repos = match Repositories::from_str(&content) { Ok(r) => r, Err(e) => { warn!("Failed to parse {}: {}", path.display(), e); return; } }; for repo in repos.iter() { if let Some(ppa_info) = PpaInfo::from_repository(repo) { ppa_repos.push((path.to_path_buf(), ppa_info, repo.signature.clone())); } } } /// Collect PPAs from legacy .list file fn collect_ppas_from_legacy( path: &Path, keyring_dir: &str, ppa_repos: &mut Vec<(PathBuf, PpaInfo, Option)>, ) { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => { warn!("Failed to read {}: {}", path.display(), e); return; } }; for line in content.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let Ok(repos) = line.parse::() else { continue; }; for legacy_repo in repos.iter() { let repo = Repository::from(legacy_repo); if let Some(ppa_info) = PpaInfo::from_repository(&repo) { let keyring_path = Path::new(keyring_dir).join(ppa_info.keyring_filename()); let signature = if keyring_path.exists() { Some(Signature::KeyPath(keyring_path)) } else { None }; ppa_repos.push((path.to_path_buf(), ppa_info, signature)); break; // Only one PPA per line } } } } fn refresh_ppa_keys(args: &Args) -> Result<(), String> { info!("Refreshing signing keys for all PPAs..."); let mut ppa_repos = Vec::new(); // Scan sources.list.d directory for PPAs let sources_dir = Path::new(&args.directory); if sources_dir.exists() { for entry in fs::read_dir(sources_dir).map_err(|e| format!("Failed to read directory: {}", e))? { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); if !path.is_file() { continue; } match path.extension().and_then(|s| s.to_str()) { Some("sources") => collect_ppas_from_deb822(&path, &mut ppa_repos), Some("list") => collect_ppas_from_legacy(&path, &args.keyring_dir, &mut ppa_repos), _ => {} } } } if ppa_repos.is_empty() { println!("No PPAs found to refresh keys for."); return Ok(()); } println!("Found {} PPA(s) to refresh keys for", ppa_repos.len()); let mut updated_count = 0; let mut failed_count = 0; let mut errors = Vec::new(); for (_source_file, ppa_info, _existing_signature) in &ppa_repos { println!( "\nRefreshing key for PPA: {}/{}", ppa_info.user, ppa_info.name ); // Download the new key #[cfg(feature = "launchpad")] let key_result = if args.keyserver.is_none() { download_ppa_key_launchpadlib(ppa_info, false) } else { download_ppa_key(ppa_info, args.keyserver.as_deref()) }; #[cfg(not(feature = "launchpad"))] let key_result = download_ppa_key(ppa_info, args.keyserver.as_deref()); let key_data = match key_result { Ok(data) => data, Err(e) => { failed_count += 1; errors.push(format!("{}/{}: {}", ppa_info.user, ppa_info.name, e)); continue; } }; // Check if we have an existing keyring file let keyring_path = Path::new(&args.keyring_dir).join(ppa_info.keyring_filename()); // Check if the key has changed let key_changed = match fs::read_to_string(&keyring_path) { Ok(existing_key) => existing_key != key_data, Err(_) => true, }; if !key_changed { debug!("Key for {}/{} is up to date", ppa_info.user, ppa_info.name); continue; } if args.dry_run { info!( "Would update signing key for {}/{}", ppa_info.user, ppa_info.name ); updated_count += 1; warn_key_expiration(&key_data, ppa_info); continue; } // Save the new key match save_key_to_keyring(&key_data, &args.keyring_dir, ppa_info) { Ok(new_keyring_path) => { info!( "Updated signing key saved to {}", new_keyring_path.display() ); updated_count += 1; warn_key_expiration(&key_data, ppa_info); } Err(e) => { failed_count += 1; errors.push(format!( "{}/{}: Failed to save key - {}", ppa_info.user, ppa_info.name, e )); } } } // Report results if args.dry_run { println!("\nDry run - Key refresh summary:"); println!(" Would update: {}", updated_count); } else { println!("\nKey refresh complete:"); println!(" Updated: {}", updated_count); } println!( " Up to date: {}", ppa_repos.len() - updated_count - failed_count ); println!( " {}: {}", if args.dry_run { "Failed to check" } else { "Failed" }, failed_count ); if !errors.is_empty() { error!("\nErrors encountered:"); for err in errors { error!(" {}", err); } } Ok(()) } /// Collect repositories from a legacy sources.list file fn collect_repos_from_sources_list( path: &Path, all_repos: &mut Vec<(String, PathBuf, RepoFormat, Repository)>, ) { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => { warn!("Failed to read {}: {}", path.display(), e); return; } }; for (line_num, line) in content.lines().enumerate() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let Ok(repos) = line.parse::() else { debug!( "Failed to parse line {} in {}", line_num + 1, path.display() ); continue; }; for legacy_repo in repos.iter() { all_repos.push(( format!("sources.list:{}", line_num + 1), path.to_path_buf(), RepoFormat::Legacy, Repository::from(legacy_repo), )); } } } /// Print details of a single repository fn print_repository_details(source: &str, path: &Path, format: RepoFormat, repo: &Repository) { let format_str = match format { RepoFormat::Deb822 => "DEB822".green(), RepoFormat::Legacy => "Legacy".yellow(), }; println!( "{} {} [{}]", "Source:".bold(), source.bright_blue(), format_str ); println!( " {}: {}", "File".dimmed(), path.display().to_string().bright_cyan() ); let enabled_str = if repo.enabled.unwrap_or(true) { "enabled".green() } else { "disabled".red() }; println!(" {}: {}", "Status".dimmed(), enabled_str); let types: Vec<&str> = repo .types .iter() .map(|t| match t { RepositoryType::Binary => "deb", RepositoryType::Source => "deb-src", }) .collect(); println!(" Types: {}", types.join(", ")); for uri in &repo.uris { println!(" URI: {}", uri); } println!(" Suites: {}", repo.suites.join(", ")); if let Some(components) = &repo.components { println!(" Components: {}", components.join(" ")); } if !repo.architectures().is_empty() { println!(" Architectures: {}", repo.architectures().join(" ")); } if let Some(signature) = &repo.signature { match signature { Signature::KeyPath(p) => println!(" Signed-By: {}", p.display()), Signature::KeyBlock(_) => println!(" Signed-By: [embedded key]"), } } println!(); // Empty line between repositories } fn list_repositories(args: &Args) -> Result<(), String> { info!("Listing all configured APT repositories"); let mut all_repos = Vec::new(); let sources_manager = SourcesManager::new(&args.directory, &args.keyring_dir); // Scan all repository files using SourcesManager let repo_files = sources_manager.scan_all_repositories()?; for (path, repositories) in repo_files { let filename = path.file_name().unwrap_or_default().to_string_lossy(); let format = if path.extension().and_then(|s| s.to_str()) == Some("sources") { RepoFormat::Deb822 } else { RepoFormat::Legacy }; for repo in repositories.iter() { all_repos.push((filename.to_string(), path.clone(), format, repo.clone())); } } // Also check sources.list if it exists let sources_list = get_main_sources_list_path(&args.directory); if sources_list.exists() { collect_repos_from_sources_list(&sources_list, &mut all_repos); } // Display the repositories if all_repos.is_empty() { println!("No repositories configured."); } else { println!("\n{}\n", "Configured APT repositories:".bold()); for (source, path, format, repo) in &all_repos { print_repository_details(source, path, *format, repo); } } Ok(()) } #[derive(Debug, Clone, Copy)] enum RepoFormat { Deb822, Legacy, } fn remove_repository( args: &Args, distribution: &Distribution, codename: &str, ) -> Result<(), String> { let repository = args .repository .as_ref() .ok_or_else(|| "Repository specification required".to_string())?; info!("Removing repository: {}", repository); let spec = parse_repository_spec(repository, 0, args, distribution, codename)?; let parsed = match spec { RepositorySpec::Component(_) => { return Err( "Cannot remove individual components. Remove the entire repository instead." .to_string(), ) } RepositorySpec::Pocket(_) => { return Err( "Cannot remove individual pockets. Remove the entire repository instead." .to_string(), ) } RepositorySpec::Repository(parsed) => parsed, }; // Determine where the repository should be located let filepath = determine_repository_filepath(&parsed, args, distribution)?; if !filepath.exists() { // If the expected file doesn't exist, also check the standard location let fallback_path = Path::new(&args.directory).join(&parsed.filename); if fallback_path.exists() && fallback_path != filepath { return remove_repository_from_file(&fallback_path, &parsed, args); } return Err(format!( "Repository not found in {} or {}", filepath.display(), fallback_path.display() )); } remove_repository_from_file(&filepath, &parsed, args) } /// Log what keyring files would be or were removed for a PPA fn handle_ppa_keyring_removal(ppa_info: &PpaInfo, keyring_dir: &str, dry_run: bool) { for ext in ["asc", "gpg"] { let keyring_filename = format!("{}-{}-keyring.{}", ppa_info.user, ppa_info.name, ext); let keyring_path = Path::new(keyring_dir).join(&keyring_filename); if keyring_path.exists() { if dry_run { info!( "Would remove associated keyring: {}", keyring_path.display() ); } else { fs::remove_file(&keyring_path).ok(); // Ignore errors info!("Associated keyring removed from {}", keyring_path.display()); } } } } /// Run apt update with a spinner fn run_apt_update() { let spinner = ProgressBar::new_spinner(); spinner.set_style( ProgressStyle::default_spinner() .template("{spinner:.green} {msg}") .unwrap(), ); spinner.set_message("Updating package cache..."); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); match process::Command::new("apt").arg("update").status() { Ok(status) if status.success() => { spinner.finish_with_message("Package cache updated successfully"); } _ => { spinner.finish_with_message("apt update failed"); warn!("apt update failed"); } } } /// Remove repository entry from a DEB822 format file fn remove_from_deb822_file(filepath: &Path, target: &Repository) -> Result<(), String> { let content = fs::read_to_string(filepath).map_err(|e| format!("Failed to read file: {}", e))?; let repos = Repositories::from_str(&content) .map_err(|e| format!("Failed to parse repositories: {}", e))?; let all_repos: Vec = repos.iter().cloned().collect(); let initial_count = all_repos.len(); let filtered_repos: Vec = all_repos .into_iter() .filter(|repo| !repositories_match(repo, target)) .collect(); if filtered_repos.len() == initial_count { return Err("Repository not found in file".to_string()); } let remaining_repos = Repositories::new(filtered_repos); fs::write(filepath, remaining_repos.to_string()) .map_err(|e| format!("Failed to write file: {}", e))?; info!("Repository removed from {}", filepath.display()); Ok(()) } /// Remove repository entry from a legacy format file fn remove_from_legacy_file(filepath: &Path, target: &Repository) -> Result<(), String> { let content = fs::read_to_string(filepath).map_err(|e| format!("Failed to read file: {}", e))?; let to_remove = LegacyRepositories::from(target).to_string(); let remove_lines: HashSet = to_remove.lines().map(|s| s.trim().to_string()).collect(); let mut new_lines = Vec::new(); let mut removed = false; for line in content.lines() { if remove_lines.contains(line.trim()) { removed = true; } else { new_lines.push(line); } } if !removed { return Err("Repository not found in file".to_string()); } let new_content = new_lines.join("\n") + "\n"; fs::write(filepath, new_content).map_err(|e| format!("Failed to write file: {}", e))?; info!("Repository removed from {}", filepath.display()); Ok(()) } fn remove_repository_from_file( filepath: &Path, parsed: &ParsedRepository, args: &Args, ) -> Result<(), String> { let is_main_file = is_main_distribution_file(filepath); // Confirm with user if needed if !args.assume_yes && !args.dry_run { let prompt = if is_main_file { format!("Remove repository from {}? [y/N] ", filepath.display()) } else { format!("Remove repository file {}? [y/N] ", filepath.display()) }; eprint!("{}", prompt); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); if !input.trim().eq_ignore_ascii_case("y") { return Err("Aborted".to_string()); } } // Handle dry run if args.dry_run { if is_main_file { info!("Would remove repository entry from {}", filepath.display()); info!("Repository to remove:"); let content = match args.format { OutputFormat::Legacy => LegacyRepositories::from(&parsed.repository).to_string(), OutputFormat::Deb822 => { Repositories::new(vec![parsed.repository.clone()]).to_string() } }; for line in content.lines() { info!(" {}", line); } } else { info!("Would remove repository file: {}", filepath.display()); } if let Some(ref ppa_info) = parsed.ppa_info { handle_ppa_keyring_removal(ppa_info, &args.keyring_dir, true); } if !args.no_update { info!("Would run: apt update"); } return Ok(()); } // Actual removal if is_main_file { let extension = filepath.extension().and_then(|s| s.to_str()).unwrap_or(""); match extension { "sources" => remove_from_deb822_file(filepath, &parsed.repository)?, _ => remove_from_legacy_file(filepath, &parsed.repository)?, } } else { fs::remove_file(filepath).map_err(|e| format!("Failed to remove file: {}", e))?; info!("Repository file removed: {}", filepath.display()); } if let Some(ref ppa_info) = parsed.ppa_info { handle_ppa_keyring_removal(ppa_info, &args.keyring_dir, false); } if !args.no_update { run_apt_update(); } Ok(()) } fn repositories_match(repo1: &Repository, repo2: &Repository) -> bool { // Compare repositories for equality repo1.types == repo2.types && repo1.uris == repo2.uris && repo1.suites == repo2.suites && repo1.components == repo2.components && repo1.architectures == repo2.architectures } fn main() { env_logger::init(); let args = Args::parse(); // Check if login is requested without launchpad feature #[cfg(not(feature = "launchpad"))] if args.login { error!("Private PPA support requires building with the 'launchpad' feature"); error!("Rebuild with: cargo build --features launchpad"); process::exit(1); } // Detect distribution and codename once at startup let distribution = Distribution::current().unwrap_or(Distribution::Debian); let (codename, _arch) = get_system_info().unwrap_or_else(|| ("stable".to_string(), String::new())); let result = if args.list { list_repositories(&args) } else if args.refresh_keys { refresh_ppa_keys(&args) } else if let Some(component) = &args.component { // Handle --component flag add_component_to_existing_repos(component, &args.directory, args.dry_run, &distribution) } else if args.enable_source > 0 && args.repository.is_none() { // Handle global source enablement when no repository is specified enable_sources_globally(&args) } else if args.remove { remove_repository(&args, &distribution, &codename) } else { add_repository(&args, &distribution, &codename) }; if let Err(e) = result { error!("{}", e); process::exit(1); } } #[cfg(test)] mod tests { use super::*; use sequoia_openpgp::cert::CertBuilder; use std::time::Duration; #[test] fn test_create_inline_signature() { // Use a minimal valid PGP key structure let key_data = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFXbjPUBCADRje\n=ABCD\n-----END PGP PUBLIC KEY BLOCK-----"; // The create_inline_signature now validates the key, so we expect it to fail // with this truncated test key let result = create_inline_signature(key_data); // For now, just check that the function exists and can be called // In real usage, it would have a valid key assert!(result.is_err() || result.is_ok()); } #[test] fn test_format_network_error() { // We can't easily create actual reqwest errors, but we can test the function exists // and returns reasonable strings for the error types we can simulate // In a real test environment, you would mock the reqwest errors // For now, we just verify the function compiles // Note: We can't call the function without a real reqwest::Error instance } #[test] fn test_is_main_distribution_file() { assert!(is_main_distribution_file(Path::new( "/etc/apt/sources.list" ))); assert!(is_main_distribution_file(Path::new( "/etc/apt/sources.list.d/ubuntu.sources" ))); assert!(is_main_distribution_file(Path::new( "/etc/apt/sources.list.d/debian.sources" ))); assert!(is_main_distribution_file(Path::new("/path/to/ubuntu.list"))); assert!(is_main_distribution_file(Path::new("/path/to/debian.list"))); assert!(!is_main_distribution_file(Path::new( "/etc/apt/sources.list.d/ppa.sources" ))); assert!(!is_main_distribution_file(Path::new( "/etc/apt/sources.list.d/custom.list" ))); } #[test] fn test_repositories_match() { let repo1 = Repository { types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), architectures: Some(vec!["amd64".to_string()]), ..Default::default() }; let repo2 = repo1.clone(); assert!(repositories_match(&repo1, &repo2)); // Different URIs let mut repo3 = repo1.clone(); repo3.uris = vec![Url::parse("http://different.com/repo").unwrap()]; assert!(!repositories_match(&repo1, &repo3)); // Different components let mut repo4 = repo1.clone(); repo4.components = Some(vec!["universe".to_string()]); assert!(!repositories_match(&repo1, &repo4)); } #[test] fn test_get_distribution_info() { // This test will depend on the system it's run on // We just verify it returns a valid Distribution enum when available let dist = Distribution::current(); // On systems with /etc/os-release (Linux), we should get Some // On systems without it (macOS), we get None - both are acceptable if std::path::Path::new("/etc/os-release").exists() { assert!(dist.is_some()); match dist.unwrap() { Distribution::Ubuntu | Distribution::Debian | Distribution::Other(_) => { // All valid variants } } } else { // On macOS or other systems without /etc/os-release, None is expected assert!(dist.is_none()); } } #[test] fn test_check_key_expiration() { // Create a cert that's valid for 60 days let (cert, _) = CertBuilder::new() .set_validity_period(Duration::from_secs(60 * 24 * 60 * 60)) .generate() .unwrap(); let warning = apt_sources::key_management::check_key_expiration(&cert); assert!(warning.is_none()); // Note: Testing expired certs would require creating a cert with // a past expiration date, which CertBuilder doesn't easily support // The function only checks if a cert is currently expired, not if it's expiring soon } #[test] fn test_generate_filename() { let dist = Distribution::Ubuntu; // Test PPA URL let filename = generate_filename( "https://ppa.launchpad.net/user/ppa/ubuntu", OutputFormat::Deb822, &dist, ); assert!(filename.ends_with(".sources")); // Test main Ubuntu repository let filename = generate_filename( "http://archive.ubuntu.com/ubuntu", OutputFormat::Deb822, &dist, ); assert_eq!(filename, "ubuntu.sources"); // Test main Ubuntu repository with legacy format let filename = generate_filename( "http://archive.ubuntu.com/ubuntu", OutputFormat::Legacy, &dist, ); assert_eq!(filename, "ubuntu.list"); // Test third-party repository let filename = generate_filename("https://example.com/repo", OutputFormat::Deb822, &dist); assert!(filename.contains("example.com")); assert!(filename.ends_with(".sources")); } #[test] fn test_default_suite_components() { let (suite, components) = default_suite_components("focal"); assert_eq!(suite, "focal"); assert_eq!(components, vec!["main".to_string()]); } fn create_test_args() -> Args { Args { repository: None, no_update: false, enable_source: 0, assume_yes: false, remove: false, directory: DEFAULT_SOURCES_PATH.to_string(), keyring_dir: DEFAULT_KEYRING_PATH.to_string(), pocket: None, keyserver: None, component: None, list: false, login: false, dry_run: false, format: OutputFormat::Legacy, refresh_keys: false, inline_key: false, } } #[test] fn test_parse_repository_spec() { let args = create_test_args(); let distribution = Distribution::Ubuntu; let codename = "noble"; // Test PPA format let spec = parse_repository_spec("ppa:user/repo", 0, &args, &distribution, codename).unwrap(); match spec { RepositorySpec::Repository(parsed) => { assert!(parsed.ppa_info.is_some()); let ppa = parsed.ppa_info.unwrap(); assert_eq!(ppa.user, "user"); assert_eq!(ppa.name, "repo"); } _ => panic!("Expected Repository spec with PPA info"), } // NOTE: Component and pocket tests require real system info from get_system_info() // These tests are skipped as they depend on the runtime environment let distribution = Distribution::Debian; let codename = "stable"; // Test full deb line let spec = parse_repository_spec( "deb http://example.com focal main", 0, &args, &distribution, codename, ) .unwrap(); match spec { RepositorySpec::Repository(parsed) => { assert!(parsed.repository.uris[0] .as_str() .contains("http://example.com")); assert_eq!(parsed.repository.suites, vec!["focal"]); assert_eq!(parsed.repository.components, Some(vec!["main".to_string()])); } _ => panic!("Expected Repository spec"), } } #[test] fn test_parse_repository_line() { // Test basic deb line let (types, uri, suite, components, signed_by) = parse_repository_line("deb http://example.com focal main universe").unwrap(); assert!(types.contains(&RepositoryType::Binary)); assert_eq!(uri.as_str(), "http://example.com/"); assert_eq!(suite, "focal"); assert_eq!(components, vec!["main", "universe"]); assert!(signed_by.is_none()); // Test deb-src line let (types, uri, suite, components, signed_by) = parse_repository_line("deb-src http://example.com focal main").unwrap(); assert!(types.contains(&RepositoryType::Source)); assert_eq!(uri.as_str(), "http://example.com/"); assert_eq!(suite, "focal"); assert_eq!(components, vec!["main"]); assert!(signed_by.is_none()); // Test with trailing slash let (types, uri, suite, components, signed_by) = parse_repository_line("deb http://example.com/ focal main").unwrap(); assert!(types.contains(&RepositoryType::Binary)); assert_eq!(uri.as_str(), "http://example.com/"); assert_eq!(suite, "focal"); assert_eq!(components, vec!["main"]); assert!(signed_by.is_none()); // Test invalid line assert!(parse_repository_line("invalid line").is_err()); assert!(parse_repository_line("deb").is_err()); assert!(parse_repository_line("deb http://example.com").is_err()); } #[test] fn test_find_matching_binary_repo() { // Create a binary repository let binary_repo = Repository { types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; // Create a source repository let source_repo = Repository { types: HashSet::from([RepositoryType::Source]), uris: vec![Url::parse("http://example.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; let repos = Repositories::new(vec![binary_repo.clone(), source_repo.clone()]); // Test finding matching binary repo let match_found = find_matching_binary_repo(&repos, &source_repo); assert!(match_found.is_some()); assert_eq!( match_found.unwrap().types, HashSet::from([RepositoryType::Binary]) ); // Test with non-matching source repo let non_matching = Repository { types: HashSet::from([RepositoryType::Source]), uris: vec![Url::parse("http://different.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; let match_found = find_matching_binary_repo(&repos, &non_matching); assert!(match_found.is_none()); } #[test] fn test_is_duplicate_repository() { let repo1 = Repository { types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; // Test exact duplicate let uris: HashSet = HashSet::from(["http://example.com/repo".to_string()]); let suites: HashSet = HashSet::from(["focal".to_string()]); let components: HashSet = HashSet::from(["main".to_string()]); let types = HashSet::from([RepositoryType::Binary]); assert!(is_duplicate_repository( &repo1, &uris, &suites, &components, &types )); // Different types let different_types = HashSet::from([RepositoryType::Source]); assert!(!is_duplicate_repository( &repo1, &uris, &suites, &components, &different_types )); // Different URIs let different_uris = HashSet::from(["http://different.com/repo".to_string()]); assert!(!is_duplicate_repository( &repo1, &different_uris, &suites, &components, &types )); // Different suites let different_suites = HashSet::from(["jammy".to_string()]); assert!(!is_duplicate_repository( &repo1, &uris, &different_suites, &components, &types )); } #[test] fn test_determine_repository_filepath() { let args = create_test_args(); let distribution = Distribution::Debian; // Test PPA repository let ppa_repo = ParsedRepository { repository: Repository { enabled: Some(true), types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://ppa.launchpad.net/user/ppa/ubuntu").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }, filename: "user-ubuntu-ppa-focal.list".to_string(), ppa_info: Some(PpaInfo { user: "user".to_string(), name: "ppa".to_string(), }), }; let path = determine_repository_filepath(&ppa_repo, &args, &distribution).unwrap(); assert!(path.to_str().unwrap().ends_with(".list")); // Legacy format is default in test args assert!(path.to_str().unwrap().contains("user")); // Test non-PPA repository let url_repo = ParsedRepository { repository: Repository { enabled: Some(true), types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("https://example.com/repo").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }, filename: "example.com.list".to_string(), ppa_info: None, }; let path = determine_repository_filepath(&url_repo, &args, &distribution).unwrap(); assert!(path.to_str().unwrap().ends_with(".list")); // Legacy format is default in test args assert!(path.to_str().unwrap().contains("example.com")); // Test with deb822 format let mut args_deb822 = create_test_args(); args_deb822.format = OutputFormat::Deb822; let mut url_repo_deb822 = url_repo.clone(); url_repo_deb822.filename = "example.com.sources".to_string(); let path = determine_repository_filepath(&url_repo_deb822, &args_deb822, &distribution).unwrap(); assert!(path.to_str().unwrap().ends_with(".sources")); } #[test] fn test_verify_key_fingerprint() { // This test needs a real PGP key to work properly // For unit testing, we'll verify the function handles invalid input correctly let invalid_key = "not a valid key"; let result = verify_key_fingerprint(invalid_key, "1234567890ABCDEF"); assert!(result.is_err()); // Test with empty fingerprint let result = verify_key_fingerprint(invalid_key, ""); assert!(result.is_err()); } #[test] fn test_get_distribution_specific_filename() { // Test Ubuntu let ubuntu_file = get_distribution_specific_filename("ubuntu", "sources", &Distribution::Ubuntu); assert_eq!(ubuntu_file, "ubuntu.sources"); let ubuntu_legacy = get_distribution_specific_filename("ubuntu", "list", &Distribution::Ubuntu); assert_eq!(ubuntu_legacy, "ubuntu.list"); // Test custom base name for Ubuntu let ubuntu_custom = get_distribution_specific_filename("custom", "sources", &Distribution::Ubuntu); assert_eq!(ubuntu_custom, "ubuntu.sources"); // Test Debian let debian_file = get_distribution_specific_filename("debian", "sources", &Distribution::Debian); assert_eq!(debian_file, "debian.sources"); let debian_legacy = get_distribution_specific_filename("debian", "list", &Distribution::Debian); assert_eq!(debian_legacy, "debian.list"); // Test custom base name for Debian let debian_custom = get_distribution_specific_filename("custom", "sources", &Distribution::Debian); assert_eq!(debian_custom, "debian.sources"); // Test Other let other_file = get_distribution_specific_filename( "myrepo", "sources", &Distribution::Other("custom".to_string()), ); assert_eq!(other_file, "myrepo.sources"); } #[test] fn test_output_format_from_str() { // Test valid formats assert_eq!( OutputFormat::from_str("deb822").unwrap(), OutputFormat::Deb822 ); assert_eq!( OutputFormat::from_str("sources").unwrap(), OutputFormat::Deb822 ); assert_eq!( OutputFormat::from_str("legacy").unwrap(), OutputFormat::Legacy ); assert_eq!( OutputFormat::from_str("list").unwrap(), OutputFormat::Legacy ); // Test case insensitive assert_eq!( OutputFormat::from_str("DEB822").unwrap(), OutputFormat::Deb822 ); assert_eq!( OutputFormat::from_str("LEGACY").unwrap(), OutputFormat::Legacy ); // Test invalid formats assert!(OutputFormat::from_str("invalid").is_err()); assert!(OutputFormat::from_str("").is_err()); } #[test] fn test_get_system_info() { // This test might fail on non-Linux systems if std::path::Path::new("/etc/os-release").exists() { if let Some((codename, arch)) = get_system_info() { assert!(!codename.is_empty()); // arch is now empty - APT will use system default assert!(arch.is_empty()); } // It's okay if this returns None on systems without VERSION_CODENAME } } #[test] fn test_has_matching_source_line() { let lines = vec![ "deb http://example.com focal main", "deb-src http://example.com focal main", "deb http://other.com focal universe", ]; // Test exact match assert!(has_matching_source_line( &lines, "deb-src http://example.com focal main" )); // Test no match assert!(!has_matching_source_line( &lines, "deb-src http://other.com focal main" )); assert!(!has_matching_source_line( &lines, "deb-src http://example.com focal universe" )); } #[test] fn test_has_matching_deb_line() { let lines = vec![ "deb http://example.com focal main", "deb-src http://example.com focal main", "deb http://other.com focal universe", ]; // Test conversion from deb-src to deb assert!(has_matching_deb_line( &lines, "deb-src http://example.com focal main" )); // Test no match assert!(!has_matching_deb_line( &lines, "deb-src http://other.com focal main" )); assert!(!has_matching_deb_line( &lines, "deb-src http://example.com focal universe" )); } #[test] fn test_has_matching_source_repo() { // Create binary repo let binary_repo = Repository { types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; // Create matching source repo let source_repo = Repository { types: HashSet::from([RepositoryType::Source]), uris: vec![Url::parse("http://example.com").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; // Create repos with both let repos = Repositories::new(vec![binary_repo.clone(), source_repo]); // Test match found assert!(has_matching_source_repo(&repos, &binary_repo)); // Test no match with different components let different_repo = Repository { types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["universe".to_string()]), ..Default::default() }; assert!(!has_matching_source_repo(&repos, &different_repo)); } #[test] fn test_ppa_info() { let ppa = PpaInfo { user: "test-user".to_string(), name: "test-ppa".to_string(), }; // Test Debug trait implementation let debug_str = format!("{:?}", ppa); assert!(debug_str.contains("test-user")); assert!(debug_str.contains("test-ppa")); } #[test] fn test_repository_spec_enum() { // Test Component variant let comp_spec = RepositorySpec::Component("main".to_string()); match comp_spec { RepositorySpec::Component(c) => assert_eq!(c, "main"), _ => panic!("Expected Component variant"), } // Test Pocket variant let pocket_spec = RepositorySpec::Pocket("security".to_string()); match pocket_spec { RepositorySpec::Pocket(p) => assert_eq!(p, "security"), _ => panic!("Expected Pocket variant"), } } #[test] fn test_create_http_client() { // Test without proxy env vars std::env::remove_var("HTTP_PROXY"); std::env::remove_var("http_proxy"); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("https_proxy"); let client_result = create_http_client(); assert!(client_result.is_ok()); // Test with invalid proxy std::env::set_var("HTTP_PROXY", "not-a-valid-url"); let client_result = create_http_client(); assert!(client_result.is_ok()); // Should still succeed, just warn about invalid proxy // Clean up std::env::remove_var("HTTP_PROXY"); } #[test] fn test_validate_fingerprint() { // Valid fingerprints (40 hex chars) let valid = "1234567890ABCDEF1234567890ABCDEF12345678"; assert!(valid.len() == 40); assert!(valid.chars().all(|c| c.is_ascii_hexdigit())); // Invalid fingerprints let invalid_short = "1234567890ABCDEF"; assert!(invalid_short.len() < 40); let invalid_chars = "1234567890ABCDEF1234567890ABCDEF12345XYZ"; assert!(!invalid_chars.chars().all(|c| c.is_ascii_hexdigit())); } #[test] fn test_parsed_repository_struct() { let repo = Repository::default(); let parsed = ParsedRepository { repository: repo, filename: "test.sources".to_string(), ppa_info: Some(PpaInfo { user: "test".to_string(), name: "ppa".to_string(), }), }; // Test Debug trait let debug_str = format!("{:?}", parsed); assert!(debug_str.contains("test.sources")); assert!(debug_str.contains("ppa_info")); } #[test] fn test_empty_component_handling() { // Test that empty components are handled properly let empty_vec: Vec = vec![]; assert!(validate_ppa_components(&empty_vec).is_ok()); // Test with empty string in components let vec_with_empty = vec!["".to_string()]; assert!(validate_ppa_components(&vec_with_empty).is_err()); } } apt-sources-0.3.0/src/bin/ppa-info.rs000064400000000000000000000077551046102023000155250ustar 00000000000000//! A simple tool to query PPA information from Launchpad. use apt_sources::launchpad::{self, Error, PpaInfo, PpaSigningKey, PpaValidationResult}; use clap::{Parser, Subcommand}; use std::process::ExitCode; #[derive(Parser)] #[command(name = "ppa-info")] #[command(about = "Query PPA information from Launchpad")] #[command(version)] struct Cli { /// Enable verbose output #[arg(short, long)] verbose: bool, /// Require authentication (for private PPAs) #[arg(short, long)] login: bool, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Show information about a PPA Info { /// PPA specification (e.g., "ppa:user/name" or "user/name") ppa: String, }, /// Get the signing key for a PPA Key { /// PPA specification (e.g., "ppa:user/name" or "user/name") ppa: String, /// Output only the key fingerprint #[arg(long)] fingerprint_only: bool, }, /// Get the subscription URL for a private PPA SubscriptionUrl { /// PPA specification (e.g., "ppa:user/name" or "user/name") ppa: String, }, } fn parse_ppa(ppa: &str) -> Result { PpaInfo::parse(ppa) } fn show_info(ppa_info: &PpaInfo, auth_required: bool) -> Result<(), Error> { let result: PpaValidationResult = launchpad::validate_ppa(ppa_info, auth_required)?; if !result.exists { println!("PPA {}/{} does not exist", ppa_info.user, ppa_info.name); return Ok(()); } println!("PPA: {}/{}", ppa_info.user, ppa_info.name); println!("Display Name: {}", result.display_name); println!("Private: {}", if result.is_private { "yes" } else { "no" }); println!( "Debug Symbols: {}", if result.publishes_debug_symbols { "yes" } else { "no" } ); Ok(()) } fn show_key(ppa_info: &PpaInfo, auth_required: bool, fingerprint_only: &bool) -> Result<(), Error> { let signing_key: PpaSigningKey = launchpad::download_ppa_signing_key(ppa_info, auth_required)?; if *fingerprint_only { println!("{}", signing_key.fingerprint); } else { println!("Fingerprint: {}", signing_key.fingerprint); println!(); println!("{}", signing_key.key_data); } Ok(()) } fn show_subscription_url(ppa_info: &PpaInfo) -> Result<(), Error> { let url = launchpad::get_private_ppa_url(ppa_info)?; println!("{}", url); Ok(()) } fn main() -> ExitCode { let cli = Cli::parse(); if cli.verbose { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); } else { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); } let result = match &cli.command { Commands::Info { ppa } => { let ppa_info = match parse_ppa(&ppa) { Ok(info) => info, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; show_info(&ppa_info, cli.login) } Commands::Key { ppa, fingerprint_only, } => { let ppa_info = match parse_ppa(&ppa) { Ok(info) => info, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; show_key(&ppa_info, cli.login, fingerprint_only) } Commands::SubscriptionUrl { ppa } => { let ppa_info = match parse_ppa(&ppa) { Ok(info) => info, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; show_subscription_url(&ppa_info) } }; match result { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("Error: {}", e); ExitCode::FAILURE } } } apt-sources-0.3.0/src/distribution.rs000064400000000000000000000177271046102023000157630ustar 00000000000000use std::fs; /// Represents a Linux distribution #[derive(Debug, Clone, PartialEq)] pub enum Distribution { /// Ubuntu Linux Ubuntu, /// Debian Linux Debian, /// Other distribution Other(String), } impl std::fmt::Display for Distribution { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Distribution::Ubuntu => write!(f, "Ubuntu"), Distribution::Debian => write!(f, "Debian"), Distribution::Other(name) => write!(f, "{}", name), } } } impl Distribution { /// Get the current system's distribution information /// /// Returns None if /etc/os-release is not present or cannot be parsed. pub fn current() -> Option { let content = fs::read_to_string("/etc/os-release").ok()?; for line in content.lines() { if line.starts_with("ID=") { let id = line .trim_start_matches("ID=") .trim_matches('"') .to_lowercase(); return Some(match id.as_str() { "ubuntu" => Distribution::Ubuntu, "debian" => Distribution::Debian, other => Distribution::Other(other.to_owned()), }); } } None } /// Get default components for this distribution pub fn default_components(&self) -> Vec<&'static str> { match self { Distribution::Ubuntu => vec!["main", "universe"], Distribution::Debian => vec!["main"], Distribution::Other(_) => vec!["main"], } } /// Get the base name for the main sources file for this distribution /// /// For example, returns "ubuntu" for Ubuntu, "debian" for Debian. /// This can be used to construct filenames like "ubuntu.sources". pub fn sources_basename(&self) -> Option<&'static str> { match self { Distribution::Ubuntu => Some("ubuntu"), Distribution::Debian => Some("debian"), Distribution::Other(_) => None, } } /// Check if a repository is a main distribution repository pub fn is_main_repository(&self, repo: &crate::Repository) -> bool { for uri in &repo.uris { if let Some(host) = uri.host_str() { match self { Distribution::Ubuntu => { if host.contains("ubuntu.com") || host.contains("canonical.com") || host == "archive.ubuntu.com" || host == "security.ubuntu.com" || host == "ports.ubuntu.com" { return true; } } Distribution::Debian => { if host.contains("debian.org") || host == "deb.debian.org" || host == "security.debian.org" { return true; } } _ => {} } } } false } } /// Get system codename from /etc/os-release /// /// Returns None if /etc/os-release is not present or doesn't contain VERSION_CODENAME. /// The architecture is always an empty string (APT will use the system's native architecture). pub fn get_system_info() -> Option<(String, String)> { let content = fs::read_to_string("/etc/os-release").ok()?; let codename = content .lines() .find(|line| line.starts_with("VERSION_CODENAME=")) .map(|line| { line.trim_start_matches("VERSION_CODENAME=") .trim_matches('"') .to_string() })?; // Return empty string for architecture - APT will use the system's native architecture // when the Architectures field is omitted from the sources file Some((codename, String::new())) } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_main_repository() { let dist = Distribution::Ubuntu; let repo = crate::Repository { uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()], suites: vec!["jammy".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; assert!(dist.is_main_repository(&repo)); let non_main_repo = crate::Repository { uris: vec![url::Url::parse("http://example.com/ubuntu").unwrap()], suites: vec!["jammy".to_string()], components: Some(vec!["main".to_string()]), ..Default::default() }; assert!(!dist.is_main_repository(&non_main_repo)); } #[test] fn test_is_main_repository_all_ubuntu_hosts() { let dist = Distribution::Ubuntu; // Test each Ubuntu host individually let ubuntu_hosts = [ "http://archive.ubuntu.com/ubuntu", "http://security.ubuntu.com/ubuntu", "http://ports.ubuntu.com/ubuntu-ports", "http://us.archive.ubuntu.com/ubuntu", // contains ubuntu.com "http://mirrors.canonical.com/ubuntu", // contains canonical.com ]; for host in &ubuntu_hosts { let repo = crate::Repository { uris: vec![url::Url::parse(host).unwrap()], ..Default::default() }; assert!(dist.is_main_repository(&repo), "Failed for host: {}", host); } } #[test] fn test_is_main_repository_all_debian_hosts() { let dist = Distribution::Debian; // Test each Debian host individually let debian_hosts = [ "http://deb.debian.org/debian", "http://security.debian.org/debian-security", "http://ftp.debian.org/debian", // contains debian.org "http://mirrors.debian.org/debian", // contains debian.org ]; for host in &debian_hosts { let repo = crate::Repository { uris: vec![url::Url::parse(host).unwrap()], ..Default::default() }; assert!(dist.is_main_repository(&repo), "Failed for host: {}", host); } } #[test] fn test_is_main_repository_other_distribution() { let dist = Distribution::Other("mint".to_string()); // Other distributions should not match any repository let repo = crate::Repository { uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()], ..Default::default() }; assert!(!dist.is_main_repository(&repo)); let repo2 = crate::Repository { uris: vec![url::Url::parse("http://deb.debian.org/debian").unwrap()], ..Default::default() }; assert!(!dist.is_main_repository(&repo2)); } #[test] fn test_is_main_repository_empty_uris() { let dist = Distribution::Ubuntu; let repo = crate::Repository { uris: vec![], ..Default::default() }; assert!(!dist.is_main_repository(&repo)); } #[test] fn test_is_main_repository_multiple_uris() { let dist = Distribution::Ubuntu; let repo = crate::Repository { uris: vec![ url::Url::parse("http://example.com/ubuntu").unwrap(), url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap(), ], ..Default::default() }; // Should return true if ANY URI matches assert!(dist.is_main_repository(&repo)); } #[test] fn test_default_components() { assert_eq!( Distribution::Ubuntu.default_components(), vec!["main", "universe"] ); assert_eq!(Distribution::Debian.default_components(), vec!["main"]); assert_eq!( Distribution::Other("mint".to_string()).default_components(), vec!["main"] ); } } apt-sources-0.3.0/src/error.rs000064400000000000000000000157001046102023000143620ustar 00000000000000//! A module for handling errors in `apt-sources` crate of `deb822-rs` project. //! It intends to address error handling in meaningful manner, less vague than just passing //! `String` as error. /// Errors for APT sources parsing and conversion to/from [`super::Repository`] or [`super::legacy::LegacyRepository`] #[derive(Debug)] pub enum RepositoryError { /// Invalid repository format InvalidFormat, /// Invalid repository URI InvalidUri, /// Missing repository URI - mandatory MissingUri, /// Unrecognized repository type InvalidType, /// The `Signed-By` field is incorrect InvalidSignature, /// The Yes/No/Force field has invalid/unexpected value YesNoForceFieldInvalid, /// The Yes/No field has invalid/unexpected value YesNoFieldInvalid, /// The field in the parsed data is not recognized (check `man sources.list`) UnrecognizedFieldName(String), /// Errors in lossy serializer or deserializer Lossy(deb822_fast::Error), /// I/O Error Io(std::io::Error), /// URL Error Url(url::ParseError), } /// Errors that can occur when loading repositories from directories #[derive(Debug)] pub enum LoadError { /// Failed to read a file Io { /// The path that failed to be read path: std::path::PathBuf, /// The underlying I/O error error: std::io::Error, }, /// Failed to parse a file Parse { /// The path that failed to be parsed path: std::path::PathBuf, /// The parsing error message error: String, }, /// Failed to read directory entries DirectoryRead { /// The directory path that failed to be read path: std::path::PathBuf, /// The underlying I/O error error: std::io::Error, }, #[cfg(not(feature = "legacy"))] /// The support for `legacy` format hadn't been enabled at build time UnsupportedLegacyFormat, } impl From for RepositoryError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } impl From for RepositoryError { fn from(e: url::ParseError) -> Self { Self::Url(e) } } impl std::fmt::Display for RepositoryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { // Spare longer messages to split lines by `rustfmt` const YNFERRMSG: &str = "The field requiring only `Yes`/`No`/`Force` values is incorrect"; const YNERRMSG: &str = "The field requiring only `Yes`/`No` values is incorrect"; match self { Self::InvalidFormat => write!(f, "Invalid repository format"), Self::InvalidUri => write!(f, "Invalid repository URI"), Self::MissingUri => write!(f, "Missing repository URI"), Self::InvalidType => write!(f, "Invalid repository type"), Self::InvalidSignature => write!(f, "The field `Signed-By` is incorrect"), Self::YesNoForceFieldInvalid => f.write_str(YNFERRMSG), Self::YesNoFieldInvalid => f.write_str(YNERRMSG), Self::UnrecognizedFieldName(name) => write!( f, "Unrecognized field name: {name} (check `man sources.list`)" ), Self::Lossy(e) => write!(f, "Lossy parser error: {e}"), Self::Io(e) => write!(f, "IO error: {e}"), Self::Url(e) => write!(f, "URL parse error: {e}"), } } } impl std::fmt::Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self { Self::Io { path, error } => write!(f, "Failed to read {}: {}", path.display(), error), Self::Parse { path, error } => { write!(f, "Failed to parse {}: {}", path.display(), error) } Self::DirectoryRead { path, error } => { write!(f, "Failed to read directory {}: {}", path.display(), error) } #[cfg(not(feature = "legacy"))] Self::UnsupportedLegacyFormat => { write!( f, "The support for `legacy` format hadn't been enabled at build time" ) } } } } impl std::error::Error for RepositoryError {} impl std::error::Error for LoadError {} #[cfg(test)] mod tests { use super::*; #[test] fn test_repository_error_display() { // Test each error variant assert_eq!( RepositoryError::InvalidFormat.to_string(), "Invalid repository format" ); assert_eq!( RepositoryError::InvalidUri.to_string(), "Invalid repository URI" ); assert_eq!( RepositoryError::MissingUri.to_string(), "Missing repository URI" ); assert_eq!( RepositoryError::InvalidType.to_string(), "Invalid repository type" ); assert_eq!( RepositoryError::InvalidSignature.to_string(), "The field `Signed-By` is incorrect" ); // Test unrecognized field name includes the field name assert_eq!( RepositoryError::UnrecognizedFieldName("foo-bar".to_string()).to_string(), "Unrecognized field name: foo-bar (check `man sources.list`)" ); // Test lossy error let lossy_err = deb822_fast::Error::UnexpectedEof; let repo_err = RepositoryError::Lossy(lossy_err); assert!(repo_err.to_string().contains("Lossy parser error:")); // Test IO error let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); let repo_err = RepositoryError::from(io_err); assert!(repo_err.to_string().contains("IO error:")); } #[test] fn test_load_error_display() { use std::path::PathBuf; // Test IO error let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); let load_err = LoadError::Io { path: PathBuf::from("/test/path"), error: io_err, }; assert!(load_err.to_string().contains("Failed to read /test/path")); assert!(load_err.to_string().contains("file not found")); // Test Parse error let parse_err = LoadError::Parse { path: PathBuf::from("/test/file.list"), error: "Invalid format".to_string(), }; assert!(parse_err .to_string() .contains("Failed to parse /test/file.list")); assert!(parse_err.to_string().contains("Invalid format")); // Test DirectoryRead error let dir_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"); let load_err = LoadError::DirectoryRead { path: PathBuf::from("/test/dir"), error: dir_err, }; assert!(load_err .to_string() .contains("Failed to read directory /test/dir")); assert!(load_err.to_string().contains("access denied")); } } apt-sources-0.3.0/src/key_management.rs000064400000000000000000000154431046102023000162210ustar 00000000000000use crate::signature::Signature; use sequoia_openpgp::cert::CertParser; use sequoia_openpgp::parse::Parse; use sequoia_openpgp::Cert; use std::fmt; use std::time::SystemTime; /// Errors that can occur during key management operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum KeyManagementError { /// Failed to parse key data ParseError(String), /// No valid certificates found in key data NoCertificates, /// Key has expired KeyExpired, /// Key is not yet valid or has expired KeyNotValid, /// Key expires soon (includes days until expiration) KeyExpiringSoon(u64), /// Fingerprint mismatch FingerprintMismatch { /// Expected fingerprint expected: String, /// Actual fingerprint found actual: String, }, /// Invalid fingerprint format InvalidFingerprint(String), /// Invalid fingerprint length InvalidFingerprintLength { /// Actual length of the fingerprint length: usize, /// Expected length description expected: String, }, } impl fmt::Display for KeyManagementError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { KeyManagementError::ParseError(msg) => write!(f, "Failed to parse key data: {}", msg), KeyManagementError::NoCertificates => { write!(f, "No valid certificates found in key data") } KeyManagementError::KeyExpired => write!(f, "Key has expired"), KeyManagementError::KeyNotValid => write!(f, "Key has expired or is not yet valid"), KeyManagementError::KeyExpiringSoon(days) => write!(f, "Key expires in {} days", days), KeyManagementError::FingerprintMismatch { expected, actual } => { write!( f, "Fingerprint mismatch! Expected: {}, Got: {}", expected, actual ) } KeyManagementError::InvalidFingerprint(msg) => write!(f, "{}", msg), KeyManagementError::InvalidFingerprintLength { length, expected } => { write!( f, "Invalid fingerprint length: {}. Expected {}", length, expected ) } } } } impl std::error::Error for KeyManagementError {} /// Check if a GPG key is expired or about to expire pub fn check_key_expiration(cert: &Cert) -> Option { use sequoia_openpgp::policy::StandardPolicy; let policy = StandardPolicy::new(); let now = SystemTime::now(); // Check if the certificate is alive according to the policy match cert.with_policy(&policy, now) { Ok(valid_cert) => { // Check if primary key has expiration if let Some(expiration) = valid_cert.primary_key().key_expiration_time() { if expiration <= now { return Some(KeyManagementError::KeyExpired); } // Warn if expiring within 30 days if let Ok(duration_until) = expiration.duration_since(now) { let days_until = duration_until.as_secs() / 86400; if days_until < 30 { return Some(KeyManagementError::KeyExpiringSoon(days_until)); } } } None } Err(_) => { // Certificate or one of its components has expired or is not valid Some(KeyManagementError::KeyNotValid) } } } /// Verify that a key's fingerprint matches the expected fingerprint pub fn verify_key_fingerprint( key_data: &str, expected_fingerprint: &str, ) -> Result<(), KeyManagementError> { let parser = CertParser::from_bytes(key_data.as_bytes()) .map_err(|e| KeyManagementError::ParseError(e.to_string()))?; let certs: Result, _> = parser.collect(); let certs = certs.map_err(|e| KeyManagementError::ParseError(e.to_string()))?; if certs.is_empty() { return Err(KeyManagementError::NoCertificates); } let cert = &certs[0]; let fingerprint = cert.fingerprint().to_hex(); // Normalize fingerprints for comparison (remove spaces and make uppercase) let normalized_actual = fingerprint.replace(" ", "").to_uppercase(); let normalized_expected = expected_fingerprint.replace(" ", "").to_uppercase(); if normalized_actual != normalized_expected { return Err(KeyManagementError::FingerprintMismatch { expected: expected_fingerprint.to_string(), actual: fingerprint, }); } Ok(()) } /// Create an inline signature from GPG key data pub fn create_inline_signature(key_data: &str) -> Result { // First, let's parse the key to validate it let parser = CertParser::from_bytes(key_data.as_bytes()) .map_err(|e| KeyManagementError::ParseError(e.to_string()))?; let certs: Result, _> = parser.collect(); let certs = certs.map_err(|e| KeyManagementError::ParseError(e.to_string()))?; if certs.is_empty() { return Err(KeyManagementError::NoCertificates); } // Check for expiration if let Some(error) = check_key_expiration(&certs[0]) { eprintln!("Warning: {}", error); } // Create inline signature with the key data (using KeyBlock variant) Ok(Signature::KeyBlock(key_data.to_string())) } /// Validate a GPG fingerprint format pub fn validate_fingerprint(fingerprint: &str) -> Result { // Remove spaces and convert to uppercase let cleaned = fingerprint.replace(" ", "").to_uppercase(); // Check if it's a valid hex string if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) { return Err(KeyManagementError::InvalidFingerprint( "Fingerprint must contain only hexadecimal characters".to_string(), )); } // GPG fingerprints should be 40 characters (160 bits) for SHA-1 // or 64 characters (256 bits) for SHA-256 if cleaned.len() != 40 && cleaned.len() != 64 { return Err(KeyManagementError::InvalidFingerprintLength { length: cleaned.len(), expected: "40 or 64 characters".to_string(), }); } Ok(cleaned) } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_fingerprint() { // Valid SHA-1 fingerprint assert!(validate_fingerprint("1234567890ABCDEF1234567890ABCDEF12345678").is_ok()); // Valid with spaces assert!(validate_fingerprint("1234 5678 90AB CDEF 1234 5678 90AB CDEF 1234 5678").is_ok()); // Invalid characters assert!(validate_fingerprint("XXXX567890ABCDEF1234567890ABCDEF12345678").is_err()); // Invalid length assert!(validate_fingerprint("1234567890ABCDEF").is_err()); } } apt-sources-0.3.0/src/keyserver.rs000064400000000000000000000246751046102023000152630ustar 00000000000000//! Keyserver operations for downloading and verifying GPG keys //! //! This module provides functionality for interacting with keyservers using Sequoia. //! It requires the `key-management` feature to be enabled. #![cfg(feature = "key-management")] use sequoia_net::KeyServer; use sequoia_openpgp::{parse::Parse, serialize::Marshal, Cert, KeyHandle}; /// Download a GPG key from a keyserver /// /// # Arguments /// * `fingerprint` - The fingerprint of the key to download (hex format) /// * `keyserver_url` - The URL of the keyserver (e.g., "hkps://keys.openpgp.org") /// /// # Returns /// The key in ASCII-armored format pub async fn download_key_from_keyserver( fingerprint: &str, keyserver_url: &str, ) -> Result { // Create a KeyHandle from the fingerprint let key_handle: KeyHandle = fingerprint .parse() .map_err(|e| format!("Invalid fingerprint format: {}", e))?; // Create a keyserver client let keyserver = KeyServer::new(keyserver_url) .map_err(|e| format!("Failed to create keyserver client: {}", e))?; // Download the certificates (keyserver may return multiple) let certs = keyserver .get(key_handle) .await .map_err(|e| format!("Failed to download key from keyserver: {}", e))?; // Get the first certificate let cert = certs .into_iter() .next() .ok_or_else(|| "No certificates found for the given fingerprint".to_string())? .map_err(|e| format!("Failed to parse certificate: {}", e))?; // Serialize the certificate to armored format let mut armored = Vec::new(); cert.armored() .serialize(&mut armored) .map_err(|e| format!("Failed to serialize key: {}", e))?; String::from_utf8(armored).map_err(|e| format!("Failed to convert key to string: {}", e)) } /// Download a GPG key from a keyserver (synchronous wrapper) /// /// This is a convenience function for synchronous code that internally /// creates a Tokio runtime to execute the async operation. /// /// # Arguments /// * `fingerprint` - The fingerprint of the key to download (hex format) /// * `keyserver_url` - The URL of the keyserver (e.g., "hkps://keys.openpgp.org") /// /// # Returns /// The key in ASCII-armored format pub fn download_key_from_keyserver_sync( fingerprint: &str, keyserver_url: &str, ) -> Result { // Check if we're already in a tokio runtime if let Ok(handle) = tokio::runtime::Handle::try_current() { // We're in a runtime, use block_in_place tokio::task::block_in_place(|| { handle.block_on(download_key_from_keyserver(fingerprint, keyserver_url)) }) } else { // No runtime, create one let runtime = tokio::runtime::Runtime::new() .map_err(|e| format!("Failed to create async runtime: {}", e))?; runtime.block_on(download_key_from_keyserver(fingerprint, keyserver_url)) } } /// Verify that a key has the expected fingerprint /// /// # Arguments /// * `key_data` - The key data in ASCII-armored or binary format /// * `expected_fingerprint` - The expected fingerprint (spaces and case are ignored) /// /// # Returns /// Ok(()) if the fingerprint matches, Err with details otherwise pub fn verify_key_fingerprint(key_data: &str, expected_fingerprint: &str) -> Result<(), String> { // Parse the certificate from the armored data let cert = Cert::from_bytes(key_data.as_bytes()).map_err(|e| format!("Failed to parse key: {}", e))?; // Get the fingerprint of the primary key let actual_fingerprint = cert.fingerprint().to_hex(); // Normalize both fingerprints for comparison (remove spaces, convert to uppercase) let normalize = |fp: &str| -> String { fp.chars() .filter(|c| c.is_alphanumeric()) .collect::() .to_uppercase() }; let expected_normalized = normalize(expected_fingerprint); let actual_normalized = normalize(&actual_fingerprint); // Handle both short (16 char) and long (40 char) fingerprints if expected_normalized.len() == 16 { // Short fingerprint - compare with the last 16 chars of actual if actual_normalized.len() >= 16 && expected_normalized != &actual_normalized[actual_normalized.len() - 16..] { return Err(format!( "Fingerprint mismatch: expected {}, got {}", expected_fingerprint, actual_fingerprint )); } } else if expected_normalized != actual_normalized { return Err(format!( "Fingerprint mismatch: expected {}, got {}", expected_fingerprint, actual_fingerprint )); } Ok(()) } /// Parse a key and extract its metadata /// /// # Arguments /// * `key_data` - The key data in ASCII-armored or binary format /// /// # Returns /// A tuple of (fingerprint, user_ids) where user_ids is a vector of email addresses pub fn parse_key_metadata(key_data: &str) -> Result<(String, Vec), String> { // Parse the certificate let cert = Cert::from_bytes(key_data.as_bytes()).map_err(|e| format!("Failed to parse key: {}", e))?; // Get the fingerprint let fingerprint = cert.fingerprint().to_hex(); // Extract user IDs (email addresses) let user_ids: Vec = cert .userids() .filter_map(|uid| { uid.userid() .email() .expect("Failed to get email") .map(String::from) }) .collect(); Ok((fingerprint, user_ids)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_fingerprint() { let normalize = |fp: &str| -> String { fp.chars() .filter(|c| c.is_alphanumeric()) .collect::() .to_uppercase() }; assert_eq!(normalize("1234 5678 90AB CDEF"), "1234567890ABCDEF"); assert_eq!(normalize("1234567890abcdef"), "1234567890ABCDEF"); assert_eq!(normalize("12:34:56:78:90:AB:CD:EF"), "1234567890ABCDEF"); assert_eq!(normalize("0x1234567890ABCDEF"), "0X1234567890ABCDEF"); } #[test] fn test_verify_fingerprint_matching() { // Use a real test key - this is a minimal valid OpenPGP key let key_data = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEZIYC9xYJKwYBBAHaRw8BAQdAz5feTnR7DwGfLHLkBhoHu6GTFprNle/n/Iup fTUT6Z60BlRlc3QgMYiZBBMWCgBBFiEE7t0zdTa4BwHfZTj0iL0eQBFT2xYFAmSG AvcCGwMFCQPCZwAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQiL0eQBFT 2xYkEgD/b3p0QehuzJiuJLijVKOB7WKnLbnt2g8cbW7EARDHkWYBANREqydl1OYJ c7B8N9l1cG2TCem0K3SXD8p1ELDs2aEJuDgEZIYC9xIKKwYBBAGXVQEFAQEHQOrf 4RAemEw5X5MBceW1BpYtKp+jH5ypaxpILGz7OVIfAwEIB4h+BBgWCgAmFiEE7t0z dTa4BwHfZTj0iL0eQBFT2xYFAmSGAvcCGwwFCQPCZwAACgkQiL0eQBFT2xbL8gEA 2NenoDwxr8aWnlhajSJZz8UYNkzJNJQCPG2cukPNf3YA/RYhzCxJMkMYJ3DXtiUh UqZBMYWFftpFkh5E5FGqs7kO =Rt6r -----END PGP PUBLIC KEY BLOCK-----"#; // The actual fingerprint of this test key (as parsed by Sequoia) let fingerprint = "7607A01349161EA59DC551A654F610003149BA6E"; // Note: This test uses a dummy fingerprint that won't match the actual key // The actual fingerprint would need to be determined by parsing the key // Test with spaces let fingerprint_with_spaces = fingerprint .chars() .enumerate() .map(|(i, c)| { if i > 0 && i % 4 == 0 { format!(" {}", c) } else { c.to_string() } }) .collect::(); assert!(verify_key_fingerprint(key_data, &fingerprint_with_spaces).is_ok()); // Test lowercase assert!(verify_key_fingerprint(key_data, &fingerprint.to_lowercase()).is_ok()); // Test short fingerprint (last 16 chars) assert!(verify_key_fingerprint(key_data, &fingerprint[fingerprint.len() - 16..]).is_ok()); // Test wrong fingerprint assert!( verify_key_fingerprint(key_data, "0000000000000000000000000000000000000000").is_err() ); } #[test] fn test_parse_key_metadata() { // Use the same valid test key let key_data = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEZIYC9xYJKwYBBAHaRw8BAQdAz5feTnR7DwGfLHLkBhoHu6GTFprNle/n/Iup fTUT6Z60BlRlc3QgMYiZBBMWCgBBFiEE7t0zdTa4BwHfZTj0iL0eQBFT2xYFAmSG AvcCGwMFCQPCZwAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQiL0eQBFT 2xYkEgD/b3p0QehuzJiuJLijVKOB7WKnLbnt2g8cbW7EARDHkWYBANREqydl1OYJ c7B8N9l1cG2TCem0K3SXD8p1ELDs2aEJuDgEZIYC9xIKKwYBBAGXVQEFAQEHQOrf 4RAemEw5X5MBceW1BpYtKp+jH5ypaxpILGz7OVIfAwEIB4h+BBgWCgAmFiEE7t0z dTa4BwHfZTj0iL0eQBFT2xYFAmSGAvcCGwwFCQPCZwAACgkQiL0eQBFT2xbL8gEA 2NenoDwxr8aWnlhajSJZz8UYNkzJNJQCPG2cukPNf3YA/RYhzCxJMkMYJ3DXtiUh UqZBMYWFftpFkh5E5FGqs7kO =Rt6r -----END PGP PUBLIC KEY BLOCK-----"#; let result = parse_key_metadata(key_data); assert!(result.is_ok()); let (fingerprint, user_ids) = result.unwrap(); assert_eq!( fingerprint.to_uppercase(), "7607A01349161EA59DC551A654F610003149BA6E" ); // This test key has "Test 1" as user ID but no email, so user_ids should be empty assert!(user_ids.is_empty()); } #[test] fn test_parse_key_with_email() { // A test key with an email address let key_data = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEZqWp9BYJKwYBBAHaRw8BAQdAsHf0MhUvIVpSFsEZvQnnF3IXw2lODfCU8naR U4juKjW0IVRlc3QgVXNlciA8dGVzdC51c2VyQGV4YW1wbGUuY29tPoiZBBMWCgBB FiEEJ0o1v8rRKmkqCwVhzaW9vsKi7GAFAmalqfQCGwMFCQPCZwAFCwkIBwICIgIG FQoJCAsCBBYCAwECHgcCF4AACgkQzaW9vsKi7GA8xQD/YSHd7Wrf7RG4dNQJvbol GMQX3J9XQFQsZhJzvF2PJQkA/A1MHSaoFIHPQ8nKMBje2WLMNan8vPJjVoGVOoUg 4Y0GuDgEZqWp9BIKKwYBBAGXVQEFAQEHQPOXyfn9OI/Ge8rqMAYiJJSKlbhHNuv6 7s9VhtKrJbclAwEIB4h+BBgWCgAmFiEEJ0o1v8rRKmkqCwVhzaW9vsKi7GAFAmal qfQCGwwFCQPCZwAACgkQzaW9vsKi7GCGegD8CzKOL6csQ6xRGBBb7Q5P0GlJHF4v s7jNLfTdgJ4AKEEA/i8Hj1Q4KmgyqE8lpZmqfAdof/LHDlLg4E5Ry/4CgIUN =zUh6 -----END PGP PUBLIC KEY BLOCK-----"#; let result = parse_key_metadata(key_data); assert!(result.is_ok()); let (fingerprint, user_ids) = result.unwrap(); assert_eq!( fingerprint.to_uppercase(), "7AC6142889FA53D9268C3278280EB318F8281840" ); assert_eq!(user_ids, vec!["test.user@example.com"]); } #[test] fn test_invalid_key_data() { let invalid_data = "This is not a PGP key"; assert!(verify_key_fingerprint(invalid_data, "any_fingerprint").is_err()); assert!(parse_key_metadata(invalid_data).is_err()); } } apt-sources-0.3.0/src/launchpad.rs000064400000000000000000000351231046102023000151710ustar 00000000000000//! Launchpad PPA (Personal Package Archive) integration //! //! This module provides functionality for handling Personal Package Archives (PPAs) in a //! Debian/Ubuntu context, including API integration using launchpadlib for authenticated access. use launchpadlib::blocking::v1_0::{self, ArchiveFull, PersonOrTeam}; use launchpadlib::blocking::Client; use launchpadlib::Error as WadlError; use std::fmt; use url::Url; const CONSUMER_KEY: &str = "apt-add-repository"; /// Default URL for Launchpad PPAs pub const LAUNCHPAD_PPA_URL: &str = "https://ppa.launchpadcontent.net"; /// Valid components for PPAs pub const VALID_PPA_COMPONENTS: &[&str] = &["main", "main/debug"]; /// Information about a PPA (Personal Package Archive) #[derive(Debug, Clone)] pub struct PpaInfo { /// The PPA owner's username pub user: String, /// The PPA name pub name: String, } impl PpaInfo { /// Parse a PPA specification string (e.g., "ppa:user/ppa-name") pub fn parse(ppa_spec: &str) -> Result { if !ppa_spec.starts_with("ppa:") { return Err("Not a PPA format".to_string()); } let ppa_part = &ppa_spec[4..]; let parts: Vec<&str> = ppa_part.split('/').collect(); if parts.len() != 2 { return Err("Invalid PPA format. Expected ppa:user/ppa-name".to_string()); } Ok(PpaInfo { user: parts[0].to_string(), name: parts[1].to_string(), }) } /// Try to extract PPA info from a Repository by parsing its URLs /// /// Returns `Some(PpaInfo)` if the repository URL matches the PPA URL pattern. pub fn from_repository(repo: &crate::Repository) -> Option { for uri in &repo.uris { let url_str = uri.as_str(); // Check for PPA URLs like: // https://ppa.launchpadcontent.net/user/ppa-name/ubuntu // http://ppa.launchpad.net/user/ppa-name/ubuntu if url_str.contains("ppa.launchpadcontent.net") || url_str.contains("ppa.launchpad.net") { let path = uri.path(); let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); if parts.len() >= 2 { return Some(PpaInfo { user: parts[0].to_string(), name: parts[1].to_string(), }); } } } None } /// Generate the repository URL for this PPA pub fn repository_url(&self, _codename: &str) -> Result { Url::parse(&format!( "{}/{}/{}/ubuntu", LAUNCHPAD_PPA_URL, self.user, self.name )) .map_err(|e| format!("Failed to construct PPA URL: {e}")) } /// Generate a filename for this PPA pub fn filename(&self, extension: &str) -> String { format!("{}-ubuntu-{}.{}", self.user, self.name, extension) } /// Generate a keyring filename for this PPA pub fn keyring_filename(&self) -> String { format!("{}-{}-keyring.asc", self.user, self.name) } } /// Result of validating a PPA #[derive(Debug, Clone)] pub struct PpaValidationResult { /// Whether the PPA exists pub exists: bool, /// Whether the PPA is private pub is_private: bool, /// Whether the PPA publishes debug symbols pub publishes_debug_symbols: bool, /// Display name of the PPA pub display_name: String, } /// Validate PPA components pub fn validate_ppa_components(components: &[String]) -> Result<(), String> { for component in components { if !VALID_PPA_COMPONENTS.contains(&component.as_str()) { return Err(format!( "Invalid component '{}' for PPA.\n\ Valid components are: {}\n\ Suggestion: Use 'main' for regular packages or 'main/debug' for debug symbols.", component, VALID_PPA_COMPONENTS.join(", ") )); } } Ok(()) } /// Errors that can occur when interacting with the Launchpad API #[derive(Debug)] pub enum Error { /// Failed to authenticate with Launchpad Authentication(String), /// Failed to connect to Launchpad API Api(WadlError), /// The requested user was not found UserNotFound(String), /// The requested PPA was not found PpaNotFound { /// The user or team name user: String, /// The PPA name name: String, }, /// No signing key is configured for the PPA NoSigningKey, /// Failed to download the signing key KeyDownload(reqwest::Error), /// The keyserver returned an error status KeyserverError(reqwest::StatusCode), /// Failed to read key data KeyRead(reqwest::Error), /// Key fingerprint verification failed #[cfg(feature = "key-management")] KeyVerification(String), /// Not authenticated - no current user available NotAuthenticated, /// PPA has no self link MissingSelfLink, /// Failed to get subscription URL SubscriptionUrl(WadlError), /// Invalid subscription URL returned by API InvalidSubscriptionUrl { /// The invalid URL string url: String, /// The parse error error: url::ParseError, }, /// Failed to create HTTP client HttpClient(reqwest::Error), /// Launchpad API returned unexpected response UnexpectedResponse(String), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::Authentication(msg) => { write!(f, "Failed to authenticate with Launchpad: {}", msg) } Error::Api(e) => write!(f, "Launchpad API error: {}", e), Error::UserNotFound(user) => write!(f, "User '{}' not found on Launchpad", user), Error::PpaNotFound { user, name } => { write!(f, "PPA '{}/{}' not found", user, name) } Error::NoSigningKey => write!(f, "PPA has no signing key configured"), Error::KeyDownload(e) => write!(f, "Failed to download signing key: {}", e), Error::KeyserverError(status) => { write!(f, "Keyserver returned error status: {}", status) } Error::KeyRead(e) => write!(f, "Failed to read key data: {}", e), #[cfg(feature = "key-management")] Error::KeyVerification(msg) => { write!(f, "Key fingerprint verification failed: {}", msg) } Error::NotAuthenticated => { write!(f, "Not authenticated - no current user available") } Error::MissingSelfLink => write!(f, "PPA has no self link"), Error::SubscriptionUrl(e) => write!(f, "Failed to get subscription URL: {}", e), Error::InvalidSubscriptionUrl { url, error } => { write!(f, "Invalid subscription URL '{}': {}", url, error) } Error::HttpClient(e) => write!(f, "Failed to create HTTP client: {}", e), Error::UnexpectedResponse(msg) => { write!(f, "Unexpected response from Launchpad: {}", msg) } } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::Api(e) => Some(e), Error::KeyDownload(e) => Some(e), Error::KeyRead(e) => Some(e), Error::SubscriptionUrl(e) => Some(e), Error::InvalidSubscriptionUrl { error, .. } => Some(error), Error::HttpClient(e) => Some(e), _ => None, } } } impl From for Error { fn from(e: WadlError) -> Self { Error::Api(e) } } /// Result of downloading a PPA signing key #[derive(Debug, Clone)] pub struct PpaSigningKey { /// The armored GPG key data pub key_data: String, /// The key fingerprint pub fingerprint: String, } /// Get a PPA by user and name fn get_ppa(client: &Client, user: &str, name: &str) -> Result { let service_root = v1_0::service_root(client)?; let people = service_root .people() .ok_or_else(|| Error::UnexpectedResponse("No people collection".to_string()))?; let person_or_team = people.get_by_name(client, user).map_err(|e| { // Check if this is a 404 (user not found) or another error if matches!(e, WadlError::UnhandledStatus(s) if s.as_u16() == 404) { Error::UserNotFound(user.to_string()) } else { Error::Api(e) } })?; // Note: get_ppaby_name is on Person/Team, not PersonFull/TeamFull let ppa = match person_or_team { PersonOrTeam::Person(person) => person.get_ppaby_name(client, None, name).map_err(|e| { if matches!(e, WadlError::UnhandledStatus(s) if s.as_u16() == 404) { Error::PpaNotFound { user: user.to_string(), name: name.to_string(), } } else { Error::Api(e) } })?, PersonOrTeam::Team(team) => team.get_ppaby_name(client, None, name).map_err(|e| { if matches!(e, WadlError::UnhandledStatus(s) if s.as_u16() == 404) { Error::PpaNotFound { user: user.to_string(), name: name.to_string(), } } else { Error::Api(e) } })?, }; Ok(ppa) } /// Validate a PPA using launchpadlib /// /// This provides authenticated access to Launchpad, which is required for private PPAs. pub fn validate_ppa(ppa_info: &PpaInfo, auth_required: bool) -> Result { let client = if auth_required { Client::authenticated(None, CONSUMER_KEY) .map_err(|e| Error::Authentication(e.to_string()))? } else { Client::anonymous(CONSUMER_KEY) }; match get_ppa(&client, &ppa_info.user, &ppa_info.name) { Ok(ppa) => Ok(PpaValidationResult { exists: true, is_private: ppa.private, publishes_debug_symbols: false, // Not available in API display_name: ppa.displayname, }), Err(Error::PpaNotFound { .. }) | Err(Error::UserNotFound(_)) => Ok(PpaValidationResult { exists: false, is_private: false, publishes_debug_symbols: false, display_name: String::new(), }), Err(e) => Err(e), } } /// Get the subscription URL for a private PPA /// /// This requires authentication with Launchpad and returns the URL that includes /// the user's subscription credentials. pub fn get_private_ppa_url(ppa_info: &PpaInfo) -> Result { let client = Client::authenticated(None, CONSUMER_KEY) .map_err(|e| Error::Authentication(e.to_string()))?; // Get the service root and the current user let service_root = v1_0::service_root(&client)?; let me = service_root.me().ok_or(Error::NotAuthenticated)?; // Get the PPA let ppa = get_ppa(&client, &ppa_info.user, &ppa_info.name)?; // Get the Archive reference from the PPA let archive = ppa.self_().ok_or(Error::MissingSelfLink)?; // Get the subscription URL for this archive let subscription_url = me .get_archive_subscription_url(&client, &archive) .map_err(Error::SubscriptionUrl)?; Url::parse(&subscription_url).map_err(|e| Error::InvalidSubscriptionUrl { url: subscription_url, error: e, }) } /// Download PPA signing key using launchpadlib /// /// This provides authenticated access which may be required for private PPAs. /// Returns the key data and fingerprint for verification and logging. pub fn download_ppa_signing_key( ppa_info: &PpaInfo, auth_required: bool, ) -> Result { let client = if auth_required { Client::authenticated(None, CONSUMER_KEY) .map_err(|e| Error::Authentication(e.to_string()))? } else { Client::anonymous(CONSUMER_KEY) }; let ppa = get_ppa(&client, &ppa_info.user, &ppa_info.name)?; let fingerprint = ppa.signing_key_fingerprint; if fingerprint.is_empty() { return Err(Error::NoSigningKey); } // Download the key data from the keyserver // The key is available at: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{fingerprint} let key_url = format!( "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{}", fingerprint ); let http_client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(Error::HttpClient)?; let response = http_client .get(&key_url) .send() .map_err(Error::KeyDownload)?; if !response.status().is_success() { return Err(Error::KeyserverError(response.status())); } let key_data = response.text().map_err(Error::KeyRead)?; // Verify the key fingerprint matches #[cfg(feature = "key-management")] crate::key_management::verify_key_fingerprint(&key_data, &fingerprint) .map_err(|e| Error::KeyVerification(e.to_string()))?; Ok(PpaSigningKey { key_data, fingerprint, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_ppa_format() { // Valid PPA let ppa = PpaInfo::parse("ppa:user/repo").unwrap(); assert_eq!(ppa.user, "user"); assert_eq!(ppa.name, "repo"); // Invalid formats assert!(PpaInfo::parse("not-a-ppa").is_err()); assert!(PpaInfo::parse("ppa:invalid").is_err()); assert!(PpaInfo::parse("ppa:too/many/parts").is_err()); } #[test] fn test_validate_ppa_components() { assert!(validate_ppa_components(&["main".to_string()]).is_ok()); assert!(validate_ppa_components(&["main/debug".to_string()]).is_ok()); assert!(validate_ppa_components(&["invalid".to_string()]).is_err()); } #[test] fn test_ppa_filename() { let ppa = PpaInfo { user: "test-user".to_string(), name: "test-repo".to_string(), }; assert_eq!(ppa.filename("list"), "test-user-ubuntu-test-repo.list"); assert_eq!( ppa.filename("sources"), "test-user-ubuntu-test-repo.sources" ); // Test with empty extension assert_eq!(ppa.filename(""), "test-user-ubuntu-test-repo."); // Test with special characters (they remain as-is) let ppa_special = PpaInfo { user: "user_123".to_string(), name: "repo-name".to_string(), }; assert_eq!( ppa_special.filename("list"), "user_123-ubuntu-repo-name.list" ); } } apt-sources-0.3.0/src/legacy.rs000064400000000000000000000665361046102023000145120ustar 00000000000000//! A module for parsing and manipulating APT source files that //! use the pre-DEB822 single line format to hold package repositories specifications. //! //! # Examples //! ``` //! # use url::Url; //! # use apt_sources::legacy::LegacyRepositories; //! # use std::str::FromStr; //! let single_line = "deb http://archive.ubuntu.com/ubuntu jammy main restricted"; //! let repositories = LegacyRepositories::from_str(single_line) //! .expect("Shall not fail for correct list entry!"); //! assert_eq!(repositories.len(), 1); //! let repository = repositories.iter().nth(0).expect("Shall not fail for first line"); //! assert_eq!(repository.uri, "http://archive.ubuntu.com/ubuntu".parse::().unwrap()); //! ``` use super::RepositoryError; use super::RepositoryType; use super::Signature; use super::YesNoForce; use itertools::Itertools; use regex::Regex; use std::borrow::Cow; use std::collections::HashSet; use std::fmt::Display; use std::ops::Deref; use std::ops::Not; use std::path::PathBuf; use std::str::FromStr; use std::sync::LazyLock; use url::Url; /// A structure representing APT repository as declared by one-line-style `.list` file: /// ```text /// type [option=value option=value...] uri suite [component] [component] [...] /// ``` /// According to `sources.list(5)` man pages, only four fields are mandatory: /// * `type` either `deb` or `deb-src` /// * `uri` to repository holding valid APT structure /// * `suite` usually being distribution codename /// * `component` most of the time `main`, but it's a section of the repository; multiple values allowed /// /// The disabled field is just commented out with `#` followed by whitespaces at the beginning of the valid line /// /// The manpage specifies following optional fields /// * `arch` comma separated list of binary architectures /// * `lang` comma separated list of supported natural languages /// * `target` /// * `pdiffs` is a yes/no field /// * `by-hash` is a yes/no/force field /// * `allow-insecure` is a yes/no field, default no /// * `allow-weak` is a yes/no field, default no /// * `allow-downgrade-to-insecure` is a yes/no field, default no /// * `trusted` us a yes/no field /// * `signed-by` is a path to the key or fingerprint; optionally followed by exclamation mark /// * `check-valid-until` is a yes/no field /// * `valid-until-min` /// * `valid-until-max` /// * `check-date` is a yes/no field /// * `date-max-future` /// * `inrelease-path` relative path /// * `snapshot` either `enable` or a snapshot ID /// /// Note: this module doesn't support undocumented options. #[derive(Clone, PartialEq, /*Eq,*/ Debug)] pub struct LegacyRepository { /// This doesn't represent real field, but rather commented or uncommented line enabled: bool, /// Legacy lists format support one type per line pub typ: RepositoryType, /// Single repo address; according to Debian that's URI, but this type is more advanced than URI from `http` crate pub uri: Url, /// The distribution name as codename or suite type (like `stable` or `testing`) pub suite: String, /// (Optional) Section of the repository, usually `main`, `contrib` or `non-free` /// return `None` if repository is Flat Repository Format () pub components: Vec, /// (Optional) Architectures binaries from this repository run on pub architectures: Vec, // arch /// (Optional) Translations support to download pub languages: Vec, // lang /// (Optional) Download targets to acquire from this source pub targets: Vec, // target /// (Optional) Controls if APT should try PDiffs instead of downloading indexes entirely; if not set defaults to configuration option `Acquire::PDiffs` pub pdiffs: Option, // pdiffs /// (Optional) Controls if APT should try to acquire indexes via a URI constructed from a hashsum of the expected file pub by_hash: Option, // by-hash /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly pub allow_insecure: bool, // allow-insecure, default no /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly pub allow_weak: bool, // allow-weak, default no /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly pub allow_downgrade_to_insecure: bool, // allow-downgrade-to-insecure, default no /// (Optional) If set forces whether APT considers source as trusted or no (default not present is a third state) pub trusted: Option, // trusted /// (Optional) Contains either absolute path to GPG keyring or embedded GPG public key block, if not set APT uses all trusted keys; /// I can't find example of using with fingerprints pub signature: Option, // signed-by } impl Default for LegacyRepository { fn default() -> Self { Self { enabled: true, typ: RepositoryType::Binary, uri: "http://nowhere.com".parse().unwrap(), suite: "none".to_string(), components: vec![], architectures: vec![], languages: vec![], targets: vec![], pdiffs: None, by_hash: None, allow_insecure: false, allow_weak: false, allow_downgrade_to_insecure: false, trusted: None, signature: None, } } } impl LegacyRepository { /// In the ideal world we'd manage to use deserialization from a new format handling, but I'm not there yet to lift this for this format fn assign_option_field(&mut self, key: &str, value: &str) -> Result<(), RepositoryError> { match key { "arch" => self.architectures = value.split(',').map(|s| s.to_string()).collect(), "lang" => self.languages = value.split(',').map(|s| s.to_string()).collect(), "target" => self.targets = value.split(',').map(|s| s.to_string()).collect(), "pdiffs" => self.pdiffs = Some(super::deserialize_yesno(value)?), "by-hash" => self.by_hash = Some(YesNoForce::from_str(value)?), "allow-insecure" => self.allow_insecure = super::deserialize_yesno(value)?, // , default no "allow-weak" => self.allow_weak = super::deserialize_yesno(value)?, // , default no "allow-downgrade-to-insecure" => { self.allow_downgrade_to_insecure = super::deserialize_yesno(value)? } // , default no "trusted" => self.trusted = Some(super::deserialize_yesno(value)?), // default not present is a third state "signed-by" => self.signature = Some(Signature::KeyPath(PathBuf::from(value))), any => return Err(RepositoryError::UnrecognizedFieldName(any.to_string())), }; Ok(()) } } /// Container for multiple `LegacyRepository` specifications as single `.list` file may contain as per specification #[derive(Debug, Clone, PartialEq)] pub struct LegacyRepositories(Vec); impl LegacyRepositories { /// Creates empty container of repositories pub fn empty() -> Self { Self(Vec::new()) } /// Creates repositories from container consisting `Repository` instances pub fn new(container: Container) -> Self where Container: Into>, { Self(container.into()) } /// Provides iterator over individual repositories in the whole file pub fn repositories(&self) -> impl Iterator { // TODO: that's by ref, not compatible with lossless self.0.iter() } /// Push a new repository pub fn push(&mut self, repo: LegacyRepository) { self.0.push(repo); } /// Retain repositories matching a predicate pub fn retain(&mut self, f: F) where F: FnMut(&LegacyRepository) -> bool, { self.0.retain(f); } /// Get mutable iterator over repositories pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, LegacyRepository> { self.0.iter_mut() } /// Extend with an iterator of repositories pub fn extend(&mut self, iter: I) where I: IntoIterator, { self.0.extend(iter); } /// Check if empty pub fn is_empty(&self) -> bool { self.0.is_empty() } } static RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?xm)^ (?Pdeb|deb-src)\s+ # Catch repository type (\[(?P[^]]*)]\s+)? # Catch options (?P\S+)\s+ # Catch repository URI (?P\S+)\s+ # Catch suite/distribution (?P(?:(?P\w+)\s?)+) # Catch components (multiple) $", ) .expect("Tested correct regular expression shall not fail!") }); /// It only make sense to convert multiple lines at once as typical `.list` file has one uncommented /// and one commented (`deb-src`) entry impl FromStr for LegacyRepositories { type Err = RepositoryError; fn from_str(text: &str) -> Result { let elements = RE .captures_iter(text) .map(|caps| { let mut repository = LegacyRepository::default(); repository.typ = RepositoryType::from_str(&caps["type"])?; let options = caps.name("options").map(|o| o.as_str()).unwrap_or(""); options .trim_matches(|c| c == '[' || c == ']') .split_whitespace() .map(|o| { o.splitn(2, '=') .collect_tuple::<(&str, &str)>() .ok_or(RepositoryError::InvalidFormat) }) .collect::, _>>()? .into_iter() .try_for_each(|(k, v)| repository.assign_option_field(k, v))?; repository.uri = Url::from_str(&caps["uri"])?; repository.suite = caps["suite"].to_owned(); repository .components .extend(caps["components"].split_whitespace().map(|c| c.to_owned())); >::Ok(repository) }) .collect::, _>>()?; Ok(Self(elements)) } } impl Deref for LegacyRepositories { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl From<&LegacyRepository> for super::Repository { fn from(original: &LegacyRepository) -> Self { Self { enabled: Some(original.enabled), // TODO: more valid one would be if true -> None else `Some(false)`... types: HashSet::from([original.typ.clone()]), uris: vec![original.uri.clone()], suites: vec![original.suite.clone()], components: original.components.clone().into(), architectures: (!original.architectures.is_empty()) .then_some(original.architectures.clone()), languages: (!original.languages.is_empty()).then_some(original.languages.clone()), targets: (!original.targets.is_empty()).then_some(original.targets.clone()), pdiffs: original.pdiffs, by_hash: original.by_hash, allow_insecure: original.allow_insecure.then_some(true), allow_weak: original.allow_weak.then_some(true), allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true), trusted: original.trusted, signature: original.signature.clone(), x_repolib_name: None, description: None, } } } impl From for super::Repository { fn from(original: LegacyRepository) -> Self { Self { enabled: Some(original.enabled), // TODO: more valid one would be if true -> None else `Some(false)`... types: HashSet::from([original.typ]), uris: vec![original.uri], suites: vec![original.suite], components: original.components.into(), architectures: (!original.architectures.is_empty()).then_some(original.architectures), languages: (!original.languages.is_empty()).then_some(original.languages), targets: (!original.targets.is_empty()).then_some(original.targets), pdiffs: original.pdiffs, by_hash: original.by_hash, allow_insecure: original.allow_insecure.then_some(true), allow_weak: original.allow_weak.then_some(true), allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true), trusted: original.trusted, signature: original.signature, x_repolib_name: None, description: None, } } } impl From<&LegacyRepositories> for super::Repositories { fn from(original: &LegacyRepositories) -> Self { Self(original.iter().map(|v| v.into()).collect()) } } impl From for super::Repositories { fn from(original: LegacyRepositories) -> Self { Self(original.0.into_iter().map(|v| v.into()).collect()) } } impl From<&super::Repository> for LegacyRepositories { /// Convert a DEB822 Repository to legacy format lines. /// Since a Repository can have multiple types/uris/suites, this may produce multiple lines. fn from(repo: &super::Repository) -> Self { let mut repos = Vec::new(); for typ in &repo.types { for uri in &repo.uris { for suite in &repo.suites { repos.push(LegacyRepository { enabled: repo.enabled.unwrap_or(true), typ: typ.clone(), uri: uri.clone(), suite: suite.clone(), components: repo.components.clone().unwrap_or_default(), architectures: repo.architectures.clone().unwrap_or_default(), languages: repo.languages.clone().unwrap_or_default(), targets: repo.targets.clone().unwrap_or_default(), pdiffs: repo.pdiffs, by_hash: repo.by_hash, allow_insecure: repo.allow_insecure.unwrap_or(false), allow_weak: repo.allow_weak.unwrap_or(false), allow_downgrade_to_insecure: repo .allow_downgrade_to_insecure .unwrap_or(false), trusted: repo.trusted, signature: repo.signature.clone(), }); } } } LegacyRepositories(repos) } } fn option_output + Display>(name: &str, option: &[O]) -> Cow<'static, str> { if option.is_empty() { Cow::Borrowed("") } else { Cow::Owned(format!("{name}={}", option.iter().join(","))) } } impl Display for LegacyRepository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.typ)?; // TODO: all options if any let options = vec![ option_output("arch", &self.architectures), option_output("lang", &self.languages), option_output("target", &self.targets), self.pdiffs .map(|p| Cow::Owned(format!("pdiff={}", if p { "yes" } else { "no" }))) .unwrap_or(Cow::Borrowed("")), self.by_hash .map(|p| Cow::Owned(format!("by-hash={p}"))) .unwrap_or(Cow::Borrowed("")), if self.allow_insecure { Cow::Owned("allow-insecure=yes".to_string()) } else { Cow::Borrowed("") }, if self.allow_weak { Cow::Owned("allow-weak=yes".to_string()) } else { Cow::Borrowed("") }, if self.allow_downgrade_to_insecure { Cow::Owned("allow-downgrade-to-insecure=yes".to_string()) } else { Cow::Borrowed("") }, self.trusted .map(|t| Cow::Owned(format!("trusted={}", if t { "yes" } else { "no" }))) .unwrap_or(Cow::Borrowed("")), self.signature .as_ref() .map(|s| { if let Signature::KeyPath(ref p) = s { Cow::Owned(format!("signed-by={}", p.display())) } else { panic!("Short format not supported!") // TODO: design bug of LegacyRepository! } }) .unwrap_or(Cow::Borrowed("")), ]; let options = options.iter().filter(|s| !s.is_empty()).join(" "); options.is_empty().not().then(|| write!(f, " [{options}]")); write!(f, " {}", self.uri)?; write!(f, " {}", self.suite)?; write!(f, " {}", self.components.join(" ")) } } impl Display for LegacyRepositories { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (i, repo) in self.0.iter().enumerate() { if i > 0 { writeln!(f)?; } write!(f, "{}", repo)?; } Ok(()) } } #[cfg(test)] mod tests { use crate::Repository; use super::*; use indoc::indoc; const LONG_SAMPLE: &str = indoc!(" deb [arch=arm64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main "); const SHORT_SAMPLE: &str = indoc!( " deb http://archive.ubuntu.com/ubuntu jammy main restricted deb-src http://archive.ubuntu.com/ubuntu jammy main restricted " ); const COMMENTED_SAMPLE: &str = indoc!( " deb http://archive.ubuntu.com/ubuntu jammy main restricted # deb-src http://archive.ubuntu.com/ubuntu jammy main restricted " ); fn golden_sample() -> Repository { // TODO: qualifies for lazy_static Repository { enabled: Some(true), // TODO: looks odd, as only `Enabled: no` in meaningful types: HashSet::from([RepositoryType::Binary]), architectures: Some(vec!["arm64".to_owned()]), uris: vec![Url::from_str("http://debian.beagleboard.org/arm64/").unwrap()], suites: vec!["jammy".to_owned()], components: Some(vec!["main".to_owned()]), signature: Some(Signature::KeyPath(PathBuf::from( "/usr/share/keyrings/rcn-ee-archive-keyring.gpg", ))), x_repolib_name: None, languages: None, targets: None, pdiffs: None, ..Default::default() } } #[test] fn test_legacy_repositories_from_str() { let repositories = LegacyRepositories::from_str(LONG_SAMPLE) .expect("Shall not fail for correct list entry!"); assert_eq!(repositories.len(), 1); let repository = repositories.iter().nth(0).unwrap(); assert_eq!(repository.enabled, true); assert_eq!(repository.typ, RepositoryType::Binary); assert_eq!(repository.architectures, vec!["arm64".to_owned()]); assert_eq!( repository.signature, Some(Signature::KeyPath(PathBuf::from( "/usr/share/keyrings/rcn-ee-archive-keyring.gpg" ))) ); assert_eq!(repository.typ, RepositoryType::Binary); assert_eq!( repository.uri, "http://debian.beagleboard.org/arm64/" .parse::() .unwrap() ); assert_eq!(repository.suite, "jammy".to_owned()); assert_eq!(repository.components, vec!["main".to_owned()]); } #[test] fn test_short_legacy_repositories_from_str() { let repositories = LegacyRepositories::from_str(SHORT_SAMPLE) .expect("Shall not fail for correct list entry!"); assert_eq!(repositories.len(), 2); let bin_repository = repositories.iter().nth(0).unwrap(); let src_repository = repositories.iter().nth(1).unwrap(); assert_eq!(bin_repository.typ, RepositoryType::Binary); assert_eq!(src_repository.typ, RepositoryType::Source); assert_eq!(bin_repository.architectures.len(), 0); assert_eq!(src_repository.architectures.len(), 0); assert_eq!(bin_repository.components.len(), 2); assert_eq!(src_repository.components.len(), 2); } #[test] #[ignore = "commented lines support not yet implemented"] fn test_commented_legacy_repositories_from_str() { let repositories = LegacyRepositories::from_str(COMMENTED_SAMPLE) .expect("Shall not fail for correct list entry!"); assert_eq!(repositories.len(), 2); let bin_repository = repositories.iter().nth(0).unwrap(); let src_repository = repositories.iter().nth(1).unwrap(); assert_eq!(bin_repository.enabled, true); assert_eq!(bin_repository.enabled, false); assert_eq!(bin_repository.typ, RepositoryType::Binary); assert_eq!(src_repository.typ, RepositoryType::Source); assert_eq!(bin_repository.architectures.len(), 0); assert_eq!(src_repository.architectures.len(), 0); assert_eq!(bin_repository.components.len(), 2); assert_eq!(src_repository.components.len(), 2); } #[test] fn test_conversion_from_legacy_to_deb822() { let repositories = LegacyRepositories::from_str(LONG_SAMPLE) .expect("Shall not fail for correct list entry!"); assert_eq!(repositories.len(), 1); let legacy_repository = repositories.iter().nth(0).unwrap(); let deb822_repository = Repository::from(legacy_repository); let golden_sample = golden_sample(); assert_eq!(golden_sample, deb822_repository); } #[test] fn test_moving_conversion_from_legacy_to_deb822() { let mut repositories = LegacyRepositories::from_str(LONG_SAMPLE) .expect("Shall not fail for correct list entry!"); assert_eq!(repositories.len(), 1); let legacy_repository = repositories.0.pop().unwrap(); // TODO: To make it work for user we'd need `DerefMut` but I'm reluctant let deb822_repository = Repository::from(legacy_repository); let golden_sample = golden_sample(); assert_eq!(golden_sample, deb822_repository); } #[test] fn test_display_of_simple_legacy_repository() { let sample = LegacyRepository { enabled: true, typ: RepositoryType::Binary, uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(), suite: "jammy".to_string(), components: vec!["main".to_string()], architectures: vec![], languages: vec![], targets: vec![], pdiffs: None, by_hash: None, allow_insecure: false, allow_weak: false, allow_downgrade_to_insecure: false, trusted: None, signature: None, }; let list_text = sample.to_string(); assert_eq!( list_text, "deb http://debian.beagleboard.org/arm64/ jammy main" ) } #[test] fn test_display_of_legacy_repository_with_options() { let sample = LegacyRepository { enabled: true, typ: RepositoryType::Binary, uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(), suite: "jammy".to_string(), components: vec!["main".to_string()], architectures: vec!["amd64".to_string()], languages: vec![], targets: vec![], pdiffs: None, by_hash: None, allow_insecure: false, allow_weak: false, allow_downgrade_to_insecure: false, trusted: None, signature: Some(Signature::KeyPath(PathBuf::from( "/usr/share/keyrings/rcn-ee-archive-keyring.gpg", ))), // TODO: `.list` supports only key files, no way to fit PGP block }; let list_text = sample.to_string(); assert_eq!( list_text, "deb [arch=amd64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main" ) } #[test] fn test_conversion_from_deb822_to_legacy() { use std::collections::HashSet; let repo = Repository { enabled: Some(true), types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]), uris: vec!["http://archive.ubuntu.com/ubuntu".parse().unwrap()], suites: vec!["jammy".to_string()], components: Some(vec!["main".to_string(), "universe".to_string()]), architectures: Some(vec!["amd64".to_string()]), ..Default::default() }; let legacy = LegacyRepositories::from(&repo); assert_eq!(legacy.len(), 2); // One for deb, one for deb-src let legacy_str = legacy.to_string(); assert!(legacy_str.contains("deb [arch=amd64]")); assert!(legacy_str.contains("deb-src [arch=amd64]")); assert!(legacy_str.contains("http://archive.ubuntu.com/ubuntu")); assert!(legacy_str.contains("jammy main universe")); } #[test] fn test_legacy_repositories_display() { let repos = LegacyRepositories(vec![ LegacyRepository { enabled: true, typ: RepositoryType::Binary, uri: "http://example.com/ubuntu".parse().unwrap(), suite: "jammy".to_string(), components: vec!["main".to_string()], ..Default::default() }, LegacyRepository { enabled: true, typ: RepositoryType::Source, uri: "http://example.com/ubuntu".parse().unwrap(), suite: "jammy".to_string(), components: vec!["main".to_string()], ..Default::default() }, ]); let display = repos.to_string(); assert_eq!( display, "deb http://example.com/ubuntu jammy main\ndeb-src http://example.com/ubuntu jammy main" ); } #[test] fn test_allow_downgrade_to_insecure_parsing() { let input = "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main\n"; let repos = LegacyRepositories::from_str(input).unwrap(); assert_eq!(repos.len(), 1); let repo = repos.iter().nth(0).unwrap(); assert!(repo.allow_downgrade_to_insecure); assert!(!repo.allow_weak); } #[test] fn test_allow_downgrade_to_insecure_display() { let repo = LegacyRepository { enabled: true, typ: RepositoryType::Binary, uri: "http://example.com/ubuntu".parse().unwrap(), suite: "jammy".to_string(), components: vec!["main".to_string()], allow_downgrade_to_insecure: true, ..Default::default() }; let text = repo.to_string(); assert_eq!( text, "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main" ); } #[test] fn test_malformed_option_without_equals() { let input = "deb [badoption] http://example.com/ubuntu jammy main\n"; let result = LegacyRepositories::from_str(input); assert!(result.is_err()); } } apt-sources-0.3.0/src/lib.rs000064400000000000000000000676121046102023000140100ustar 00000000000000#![deny(missing_docs)] //! A library for parsing and manipulating APT source files that //! use the DEB822 format to hold package repositories specifications. //! //!
//! //! Currently only lossy _serialization_ is implemented, lossless support //! retaining file sequence and comments would come at later date. //! //!
//! //! # Examples //! //! ```rust //! //! use apt_sources::Repositories; //! use std::path::Path; //! //! let text = r#"Types: deb //! URIs: http://ports.ubuntu.com/ //! Suites: noble //! Components: stable //! Architectures: arm64 //! Signed-By: //! -----BEGIN PGP PUBLIC KEY BLOCK----- //! . //! mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr //! crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8 //! ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ //! wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB //! Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW //! WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg== //! =5NZE //! -----END PGP PUBLIC KEY BLOCK-----"#; //! //! let r = text.parse::().unwrap(); //! let suites = r[0].suites(); //! assert_eq!(suites[0], "noble"); //! ``` //! // TODO: Not supported yet: // See the ``lossless`` module (behind the ``lossless`` feature) for a more forgiving parser that // allows partial parsing, parsing files with errors and unknown fields and editing while // preserving formatting. use deb822_fast::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph}; use error::{LoadError, RepositoryError}; use itertools::Itertools; #[cfg(feature = "legacy")] use legacy::LegacyRepositories; use signature::Signature; use std::path::Path; use std::result::Result; use std::{collections::HashSet, ops::Deref, str::FromStr}; use url::Url; /// Distribution detection and utilities pub mod distribution; pub mod error; /// Key management utilities for GPG keys #[cfg(feature = "key-management")] pub mod key_management; #[cfg(feature = "key-management")] pub mod keyserver; /// Launchpad PPA (Personal Package Archive) integration #[cfg(feature = "launchpad")] pub mod launchpad; #[cfg(feature = "legacy")] pub mod legacy; pub mod signature; /// Module for managing APT source lists pub mod sources_manager; /// General utilities pub mod utils; /// A representation of the repository type, by role of packages it can provide, either `Binary` /// (indicated by `deb`) or `Source` (indicated by `deb-src`). #[derive(PartialEq, Eq, Hash, Debug, Clone)] pub enum RepositoryType { /// Repository with binary packages, indicated as `deb` Binary, /// Repository with source packages, indicated as `deb-src` Source, } impl FromStr for RepositoryType { type Err = RepositoryError; fn from_str(s: &str) -> Result { match s { "deb" => Ok(RepositoryType::Binary), "deb-src" => Ok(RepositoryType::Source), _ => Err(RepositoryError::InvalidType), } } } impl From<&RepositoryType> for String { fn from(value: &RepositoryType) -> Self { match value { RepositoryType::Binary => "deb".to_owned(), RepositoryType::Source => "deb-src".to_owned(), } } } impl std::fmt::Display for RepositoryType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { RepositoryType::Binary => "deb", RepositoryType::Source => "deb-src", }; write!(f, "{s}") } } #[derive(Debug, Clone, Copy, PartialEq)] /// Enumeration for fields like `By-Hash` which have third value of `force` pub enum YesNoForce { /// True Yes, /// False No, /// Forced Force, } impl FromStr for YesNoForce { type Err = RepositoryError; fn from_str(s: &str) -> Result { match s { "yes" => Ok(Self::Yes), "no" => Ok(Self::No), "force" => Ok(Self::Force), _ => Err(RepositoryError::YesNoForceFieldInvalid), } } } impl From<&YesNoForce> for String { fn from(value: &YesNoForce) -> Self { match value { YesNoForce::Yes => "yes".to_owned(), YesNoForce::No => "no".to_owned(), YesNoForce::Force => "force".to_owned(), } } } impl std::fmt::Display for YesNoForce { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { YesNoForce::Yes => "yes", YesNoForce::No => "no", YesNoForce::Force => "force", }; write!(f, "{s}") } } fn deserialize_types(text: &str) -> Result, RepositoryError> { text.split_whitespace() .map(RepositoryType::from_str) .collect::, RepositoryError>>() } fn serialize_types(files: &HashSet) -> String { files.iter().map(|rt| rt.to_string()).join("\n") } fn deserialize_uris(text: &str) -> Result, String> { // TODO: bad error type text.split_whitespace() .map(Url::from_str) .collect::, _>>() .map_err(|e| e.to_string()) // TODO: bad error type } fn serialize_uris(uris: &[Url]) -> String { uris.iter().map(|u| u.as_str()).join(" ") } fn deserialize_string_chain(text: &str) -> Result, String> { // TODO: bad error type Ok(text.split_whitespace().map(|x| x.to_string()).collect()) } fn deserialize_yesno(text: &str) -> Result { // TODO: bad error type match text { "yes" => Ok(true), "no" => Ok(false), _ => Err(RepositoryError::YesNoFieldInvalid), } } fn serializer_yesno(value: &bool) -> String { if *value { "yes".to_string() } else { "no".to_string() } } fn serialize_string_chain(chain: &[String]) -> String { chain.join(" ") } /// A structure representing APT repository as declared by DEB822 source file /// /// According to `sources.list(5)` man pages, only four fields are mandatory: /// * `Types` either `deb` or/and `deb-src` /// * `URIs` to repositories holding valid APT structure (unclear if multiple are allowed) /// * `Suites` usually being distribution codenames /// * `Component` most of the time `main`, but it's a section of the repository /// /// The manpage specifies following optional fields /// * `Enabled` is a yes/no field, default yes /// * `Architectures` /// * `Languages` /// * `Targets` /// * `PDiffs` is a yes/no field /// * `By-Hash` is a yes/no/force field /// * `Allow-Insecure` is a yes/no field, default no /// * `Allow-Weak` is a yes/no field, default no /// * `Allow-Downgrade-To-Insecure` is a yes/no field, default no /// * `Trusted` us a yes/no field /// * `Signed-By` is either path to the key or PGP key block /// * `Check-Valid-Until` is a yes/no field /// * `Valid-Until-Min` /// * `Valid-Until-Max` /// * `Check-Date` is a yes/no field /// * `Date-Max-Future` /// * `InRelease-Path` relative path /// * `Snapshot` either `enable` or a snapshot ID /// /// The unit tests of APT use: /// * `Description` /// /// The RepoLib tool uses: /// * `X-Repolib-Name` identifier for own reference, meaningless for APT /// /// Note: Multivalues `*-Add` & `*-Remove` semantics aren't supported. #[derive(FromDeb822, ToDeb822, Clone, PartialEq, /*Eq,*/ Debug, Default)] pub struct Repository { /// If `no` (false) the repository is ignored by APT #[deb822(field = "Enabled", deserialize_with = deserialize_yesno, serialize_with = serializer_yesno)] // TODO: support for `default` if omitted is missing pub enabled: Option, /// The value `RepositoryType::Binary` (`deb`) or/and `RepositoryType::Source` (`deb-src`) #[deb822(field = "Types", deserialize_with = deserialize_types, serialize_with = serialize_types)] pub types: HashSet, // consider alternative, closed set /// The address of the repository #[deb822(field = "URIs", deserialize_with = deserialize_uris, serialize_with = serialize_uris)] pub uris: Vec, // according to Debian that's URI, but this type is more advanced than URI from `http` crate /// The distribution name as codename or suite type (like `stable` or `testing`) #[deb822(field = "Suites", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)] pub suites: Vec, /// (Optional) Section of the repository, usually `main`, `contrib` or `non-free` /// return `None` if repository is Flat Repository Format () #[deb822(field = "Components", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)] pub components: Option>, /// (Optional) Architectures binaries from this repository run on #[deb822(field = "Architectures", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)] pub architectures: Option>, /// (Optional) Translations support to download #[deb822(field = "Languages", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)] pub languages: Option>, // TODO: Option is redundant to empty vectors /// (Optional) Download targets to acquire from this source #[deb822(field = "Targets", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)] pub targets: Option>, /// (Optional) Controls if APT should try PDiffs instead of downloading indexes entirely; if not set defaults to configuration option `Acquire::PDiffs` #[deb822(field = "PDiffs", deserialize_with = deserialize_yesno)] pub pdiffs: Option, /// (Optional) Controls if APT should try to acquire indexes via a URI constructed from a hashsum of the expected file #[deb822(field = "By-Hash")] pub by_hash: Option, /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly #[deb822(field = "Allow-Insecure")] pub allow_insecure: Option, // TODO: redundant option, not present = default no /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly #[deb822(field = "Allow-Weak")] pub allow_weak: Option, // TODO: redundant option, not present = default no /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly #[deb822(field = "Allow-Downgrade-To-Insecure")] pub allow_downgrade_to_insecure: Option, // TODO: redundant option, not present = default no /// (Optional) If set forces whether APT considers source as rusted or no (default not present is a third state) #[deb822(field = "Trusted")] pub trusted: Option, /// (Optional) Contains either absolute path to GPG keyring or embedded GPG public key block, if not set APT uses all trusted keys; /// I can't find example of using with fingerprints #[deb822(field = "Signed-By")] pub signature: Option, /// (Optional) Field ignored by APT but used by RepoLib to identify repositories, Ubuntu sources contain them #[deb822(field = "X-Repolib-Name")] pub x_repolib_name: Option, // this supports RepoLib still used by PopOS, even if removed from Debian/Ubuntu /// (Optional) Field not present in the man page, but used in APT unit tests, potentially to hold the repository description #[deb822(field = "Description")] pub description: Option, // options: HashMap // My original parser kept remaining optional fields in the hash map, is this right approach? } impl Repository { /// Returns slice of strings containing suites for which this repository provides pub fn suites(&self) -> &[String] { self.suites.as_slice() } /// Returns the repository types (deb/deb-src) pub fn types(&self) -> &HashSet { &self.types } /// Returns the repository URIs pub fn uris(&self) -> &[Url] { &self.uris } /// Returns the repository components pub fn components(&self) -> Option<&[String]> { self.components.as_deref() } /// Returns the repository architectures pub fn architectures(&self) -> &[String] { self.architectures.as_deref().unwrap_or(&[]) } } /// Container for multiple `Repository` specifications as single `.sources` file may contain as per specification #[derive(Debug, Clone, PartialEq)] pub struct Repositories(Vec); impl Default for Repositories { /// Creates a default instance by loading repositories from /etc/apt/ /// /// Errors are logged as warnings (if the `tracing` feature is enabled) but /// don't prevent loading valid repositories (like APT does) fn default() -> Self { let (repos, errors) = Self::load_from_directory(std::path::Path::new("/etc/apt")); // Log any errors encountered, but continue (like APT) #[cfg(feature = "tracing")] for error in errors { tracing::warn!("Failed to load APT source: {}", error); } // Without tracing feature, errors are silently ignored #[cfg(not(feature = "tracing"))] let _ = errors; repos } } impl Repositories { /// Creates empty container of repositories pub fn empty() -> Self { Repositories(Vec::new()) } /// Creates repositories from container consisting `Repository` instances pub fn new(container: Container) -> Self where Container: Into>, { Repositories(container.into()) } /// Load repositories from a directory (e.g., /etc/apt/) /// /// This will load: /// - sources.list file from the directory /// - All *.list files from sources.list.d/ subdirectory (in lexicographical order) /// - All *.sources files from sources.list.d/ subdirectory (in lexicographical order) /// /// Returns a tuple of (successfully loaded repositories, errors encountered). /// This method is resilient like APT - errors in individual files don't prevent /// loading other valid repositories. /// ///
/// This loads all repositories from all files, but information about which file they're /// loaded from is **lost** in the process.u ///
pub fn load_from_directory(path: &Path) -> (Self, Vec) { use std::fs; let mut all_repositories = Repositories::empty(); let mut errors = Vec::new(); // Process main sources.list file if it exists let main_sources = path.join("sources.list"); #[cfg(not(feature = "legacy"))] eprintln!( "WARNING! `{}` hasn't been read as `legacy` support hadn't been enabled during build.", main_sources.display() ); #[cfg(feature = "legacy")] if main_sources.exists() { match fs::read_to_string(&main_sources) { Ok(content) => { match LegacyRepositories::from_str(&content)/*Self::parse_legacy_format(&content)*/ { Ok(repos) => { // let legacy_repos = .collect::, _>>(); all_repositories.extend(repos.repositories().map(|l| l.into())) }, Err(e) => errors.push(LoadError::Parse { path: main_sources, error: e.to_string(), // TODO [MF]: doesn't look right, we shall have error type for every kind }), } } Err(e) => errors.push(LoadError::Io { path: main_sources, error: e, }), } } // Process files from sources.list.d/ directory let sources_d = path.join("sources.list.d"); if !sources_d.is_dir() { return (all_repositories, errors); } let entries = match fs::read_dir(&sources_d) { Ok(entries) => entries, Err(e) => { errors.push(LoadError::DirectoryRead { path: sources_d, error: e, }); return (all_repositories, errors); } }; // Collect and sort entries lexicographically like APT does let mut entry_paths: Vec<_> = entries .filter_map(|entry| entry.ok()) .map(|entry| entry.path()) .filter(|p| p.is_file()) .filter(|p| { p.file_name() .and_then(|n| n.to_str()) .map(|n| n.ends_with(".list") || n.ends_with(".sources")) .unwrap_or(false) }) .collect(); entry_paths.sort(); for file_path in entry_paths { let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let content = match fs::read_to_string(&file_path) { Ok(content) => content, Err(e) => { errors.push(LoadError::Io { path: file_path, error: e, }); continue; } }; let parse_result = if file_name.ends_with(".list") { #[cfg(not(feature = "legacy"))] { eprintln!("WARNING! `{file_name}` hasn't been read as `legacy` support hadn't been enabled during build."); Err(LoadError::UnsupportedLegacyFormat) } #[cfg(feature = "legacy")] LegacyRepositories::from_str(&content) .map(|repos| repos.repositories().map(|l| l.into()).collect()) .map_err(|e| LoadError::Parse { path: file_path, error: e.to_string(), // TODO [MF]: looks like it's time for `thiserror` }) } else if file_name.ends_with(".sources") { content .parse::() .map(|repos| repos.0) .map_err(|e| LoadError::Parse { path: file_path, error: e, }) } else { continue; }; match parse_result { Ok(repos) => all_repositories.extend(repos), Err(e) => errors.push(e), } } (all_repositories, errors) } /// Load repositories from a directory, failing on first error /// /// Use this when you want strict error handling and need the loading /// to stop at the first problem encountered. pub fn load_from_directory_strict(path: &Path) -> Result { let (repos, errors) = Self::load_from_directory(path); if let Some(error) = errors.into_iter().next() { Err(error) } else { Ok(repos) } } /// Push a new repository pub fn push(&mut self, repo: Repository) { self.0.push(repo); } /// Retain repositories matching a predicate pub fn retain(&mut self, f: F) where F: FnMut(&Repository) -> bool, { self.0.retain(f); } /// Get iterator over repositories pub fn iter(&self) -> std::slice::Iter<'_, Repository> { self.0.iter() } /// Get mutable iterator over repositories pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Repository> { self.0.iter_mut() } /// Extend with an iterator of repositories pub fn extend(&mut self, iter: I) where I: IntoIterator, { self.0.extend(iter); } /// Check if empty pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl std::str::FromStr for Repositories { type Err = String; fn from_str(s: &str) -> Result { let deb822: deb822_fast::Deb822 = s.parse().map_err(|e: deb822_fast::Error| e.to_string())?; let repos = deb822 .iter() .map(Repository::from_paragraph) .collect::, Self::Err>>()?; Ok(Repositories(repos)) } } impl std::fmt::Display for Repositories { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let result = self .0 .iter() .map(|r| { let p: deb822_fast::Paragraph = r.to_paragraph(); p.to_string() }) .collect::>() .join("\n"); f.write_str(&result) } } impl Deref for Repositories { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(test)] mod tests { use std::{collections::HashSet, str::FromStr}; use indoc::indoc; use url::Url; use crate::{signature::Signature, Repositories, Repository, RepositoryType}; #[test] fn test_not_machine_readable() { let s = indoc!( r#" deb [arch=arm64 signed-by=/usr/share/keyrings/docker.gpg] http://ports.ubuntu.com/ noble stable "# ); let ret = s.parse::(); assert!(ret.is_err()); assert_eq!(ret.unwrap_err(), "missing field: Types".to_string()); } #[test] fn test_parse_flat_repo() { let s = indoc! {r#" Types: deb URIs: http://ports.ubuntu.com/ Suites: ./ Architectures: arm64 "#}; let repos = s .parse::() .expect("Shall be parsed flawlessly"); assert!(repos[0].types.contains(&super::RepositoryType::Binary)); } #[test] fn test_parse_without_architectures() { // Architectures is optional; Debian's own default .sources omits it. let s = indoc! {r#" Types: deb URIs: http://deb.debian.org/debian Suites: trixie Components: main Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp "#}; let repos = s .parse::() .expect("Shall be parsed flawlessly"); assert_eq!(repos[0].architectures, None); assert_eq!(repos[0].architectures(), &[] as &[String]); } #[test] fn test_parse_w_keyblock() { let s = indoc!( r#" Types: deb URIs: http://ports.ubuntu.com/ Suites: noble Components: stable Architectures: arm64 Signed-By: -----BEGIN PGP PUBLIC KEY BLOCK----- . mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8 ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg== =5NZE -----END PGP PUBLIC KEY BLOCK----- "# ); let repos = s .parse::() .expect("Shall be parsed flawlessly"); assert!(repos[0].types.contains(&super::RepositoryType::Binary)); assert!(matches!(repos[0].signature, Some(Signature::KeyBlock(_)))); } #[test] fn test_parse_w_keypath() { let s = indoc!( r#" Types: deb URIs: http://ports.ubuntu.com/ Suites: noble Components: stable Architectures: arm64 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg "# ); let reps = s .parse::() .expect("Shall be parsed flawlessly"); assert!(reps[0].types.contains(&super::RepositoryType::Binary)); assert!(matches!(reps[0].signature, Some(Signature::KeyPath(_)))); } #[test] fn test_serialize() { //let repos = Repositories::empty(); let repos = Repositories::new([Repository { enabled: Some(true), // TODO: looks odd, as only `Enabled: no` in meaningful types: HashSet::from([RepositoryType::Binary]), architectures: Some(vec!["arm64".to_owned()]), uris: vec![Url::from_str("https://deb.debian.org/debian").unwrap()], suites: vec!["jammy".to_owned()], components: Some(vec!["main".to_owned()]), signature: None, x_repolib_name: None, languages: None, targets: None, pdiffs: None, ..Default::default() }]); let text = repos.to_string(); assert_eq!( text, indoc! {r#" Enabled: yes Types: deb URIs: https://deb.debian.org/debian Suites: jammy Components: main Architectures: arm64 "#} ); } #[test] fn test_yesnoforce_to_string() { let yes = crate::YesNoForce::Yes; assert_eq!(yes.to_string(), "yes"); let no = crate::YesNoForce::No; assert_eq!(no.to_string(), "no"); let force = crate::YesNoForce::Force; assert_eq!(force.to_string(), "force"); } #[test] fn test_repository_type_display() { assert_eq!(RepositoryType::Binary.to_string(), "deb"); assert_eq!(RepositoryType::Source.to_string(), "deb-src"); } #[test] fn test_yesnoforce_display() { assert_eq!(crate::YesNoForce::Yes.to_string(), "yes"); assert_eq!(crate::YesNoForce::No.to_string(), "no"); assert_eq!(crate::YesNoForce::Force.to_string(), "force"); } #[test] fn test_repositories_is_empty() { let empty_repos = Repositories::empty(); assert!(empty_repos.is_empty()); let mut repos = Repositories::empty(); repos.push(Repository::default()); assert!(!repos.is_empty()); } #[test] fn test_repository_getters() { let repo = Repository { types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]), uris: vec![Url::parse("http://example.com/debian").unwrap()], suites: vec!["stable".to_string()], components: Some(vec!["main".to_string(), "contrib".to_string()]), architectures: Some(vec!["amd64".to_string(), "arm64".to_string()]), ..Default::default() }; // Test types getter assert_eq!( repo.types(), &HashSet::from([RepositoryType::Binary, RepositoryType::Source]) ); // Test uris getter assert_eq!(repo.uris().len(), 1); assert_eq!(repo.uris()[0].to_string(), "http://example.com/debian"); // Test suites getter (existing) assert_eq!(repo.suites(), vec!["stable"]); // Test components getter assert_eq!( repo.components(), Some(vec!["main".to_string(), "contrib".to_string()].as_slice()) ); // Test architectures getter assert_eq!(repo.architectures(), vec!["amd64", "arm64"]); } #[test] fn test_repositories_iter() { let mut repos = Repositories::empty(); repos.push(Repository { suites: vec!["stable".to_string()], ..Default::default() }); repos.push(Repository { suites: vec!["testing".to_string()], ..Default::default() }); // Test iter() let suites: Vec<_> = repos.iter().map(|r| r.suites()).collect(); assert_eq!(suites.len(), 2); assert_eq!(suites[0], vec!["stable"]); assert_eq!(suites[1], vec!["testing"]); // Test iter_mut() - modifying through mutable iterator for repo in repos.iter_mut() { repo.enabled = Some(false); } for repo in repos.iter() { assert_eq!(repo.enabled, Some(false)); } } } apt-sources-0.3.0/src/signature.rs000064400000000000000000000051471046102023000152360ustar 00000000000000//! A module implementing [`Signature`] type that holds info about variants of the signature key used by the repository use std::path::PathBuf; use crate::error::RepositoryError; /// A type to store variants of cryptographic public key to validate the [`super::Repository`]. #[derive(Debug, PartialEq, Clone)] pub enum Signature { /// The PGP key is stored inside the `.sources` files KeyBlock(String), // TODO: shall we validate PGP Public Key? /// The public key is store in a file of the given path KeyPath(PathBuf), // TODO: man page specifies fingerprints, but there's no example } impl std::str::FromStr for Signature { type Err = RepositoryError; fn from_str(text: &str) -> Result { // Normal examples say PGP line shall start next line after `Signed-By` field // but all my files have it starting after a space in the same line and that works. // It's quite confusing, but let it be... we have to deal with reality. if text.contains("\n") { // If text is multiline, we assume PGP Public Key block Ok(Signature::KeyBlock(text.to_string())) } else { // otherwise one-liner is a path Ok(Signature::KeyPath(text.into())) } // if let Some((name, rest)) = text.split_once('\n') { // if name.is_empty() { // println!("& Name = {}", name); // Ok(Signature::KeyBlock(rest.to_string())) // } else { // println!("& Name = {}", name); // Err(RepositoryError::InvalidSignature) // } // } else { // println!("& No name"); // Ok(Signature::KeyPath(text.into())) // } } } impl std::fmt::Display for Signature { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Signature::KeyBlock(text) => write!(f, "\n{text}"), Signature::KeyPath(path) => f.write_str(path.to_string_lossy().as_ref()), } } } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn test_signature_display() { // Test KeyPath display let path_sig = Signature::KeyPath(PathBuf::from("/etc/apt/trusted.gpg")); assert_eq!(path_sig.to_string(), "/etc/apt/trusted.gpg"); // Test KeyBlock display let key_block = "-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key\n-----END PGP PUBLIC KEY BLOCK-----"; let block_sig = Signature::KeyBlock(key_block.to_string()); assert_eq!(block_sig.to_string(), format!("\n{}", key_block)); } } apt-sources-0.3.0/src/sources_manager.rs000064400000000000000000000670561046102023000164210ustar 00000000000000use crate::{Repositories, Repository, RepositoryType}; use std::collections::HashSet; use std::fs; use std::io; use std::io::Write; use std::path::{Path, PathBuf}; /// Default path for APT sources files pub const DEFAULT_SOURCES_PATH: &str = "/etc/apt/sources.list.d"; /// Default path for APT keyring files pub const DEFAULT_KEYRING_PATH: &str = "/etc/apt/trusted.gpg.d"; /// Manager for APT sources and keyrings #[derive(Debug, Clone)] pub struct SourcesManager { sources_dir: PathBuf, keyring_dir: PathBuf, } impl Default for SourcesManager { fn default() -> Self { Self { sources_dir: PathBuf::from(DEFAULT_SOURCES_PATH), keyring_dir: PathBuf::from(DEFAULT_KEYRING_PATH), } } } impl SourcesManager { /// Create a new SourcesManager with custom directories pub fn new(sources_dir: impl Into, keyring_dir: impl Into) -> Self { Self { sources_dir: sources_dir.into(), keyring_dir: keyring_dir.into(), } } /// Get the path to the sources directory pub fn sources_dir(&self) -> &Path { &self.sources_dir } /// Get the path to the keyring directory pub fn keyring_dir(&self) -> &Path { &self.keyring_dir } /// Generate a filename for a repository pub fn generate_filename(&self, name: &str, format: FileFormat) -> String { let sanitized = name.replace(['/', ':', ' '], "-").to_lowercase(); match format { FileFormat::Deb822 => format!("{sanitized}.sources"), FileFormat::Legacy => format!("{sanitized}.list"), } } /// Get the full path for a repository file pub fn get_repository_path(&self, filename: &str) -> PathBuf { self.sources_dir.join(filename) } /// Get the full path for a keyring file pub fn get_keyring_path(&self, filename: &str) -> PathBuf { self.keyring_dir.join(filename) } /// Write repositories to a file pub fn write_repositories(&self, path: &Path, repositories: &Repositories) -> io::Result<()> { let mut file = fs::File::create(path)?; write!(file, "{repositories}") } /// Read repositories from a file pub fn read_repositories(&self, path: &Path) -> Result { let content = fs::read_to_string(path) .map_err(|e| format!("Failed to read file {}: {e}", path.display()))?; content .parse::() .map_err(|e| format!("Failed to parse repositories: {e}")) } /// List all repository files in the sources directory pub fn list_repository_files(&self) -> io::Result> { let mut files = Vec::new(); if self.sources_dir.exists() { for entry in fs::read_dir(&self.sources_dir)? { let entry = entry?; let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension() { if ext == "sources" || ext == "list" { files.push(path); } } } } } Ok(files) } /// Scan all repository files and return their contents pub fn scan_all_repositories(&self) -> Result, String> { let mut results = Vec::new(); let files = self .list_repository_files() .map_err(|e| format!("Failed to list repository files: {}", e))?; for file in files { match self.read_repositories(&file) { Ok(repos) => results.push((file, repos)), Err(e) => { // Log error but continue scanning eprintln!("Warning: Failed to read {}: {}", file.display(), e); } } } Ok(results) } /// Check if a repository already exists in any file pub fn repository_exists(&self, repository: &Repository) -> Result, String> { let all_repos = self.scan_all_repositories()?; for (path, repos) in all_repos { for repo in repos.iter() { if repos_match(repo, repository) { return Ok(Some(path)); } } } Ok(None) } /// Ensure the sources and keyring directories exist pub fn ensure_directories(&self) -> io::Result<()> { fs::create_dir_all(&self.sources_dir)?; fs::create_dir_all(&self.keyring_dir)?; Ok(()) } /// Add a repository to a file, creating the file if it doesn't exist pub fn add_repository(&self, repository: &Repository, filename: &str) -> Result<(), String> { let path = self.get_repository_path(filename); // Check if repository already exists if let Some(existing_path) = self.repository_exists(repository)? { return Err(format!( "Repository already exists in {}", existing_path.display() )); } // Read existing repositories if file exists let mut repositories = if path.exists() { self.read_repositories(&path)? } else { Repositories::empty() }; // Add the new repository repositories.push(repository.clone()); // Write back to file self.write_repositories(&path, &repositories) .map_err(|e| format!("Failed to write repository: {e}")) } /// Remove a repository from all files pub fn remove_repository(&self, repository: &Repository) -> Result { let mut removed = false; let all_files = self.scan_all_repositories()?; for (path, mut repos) in all_files { let initial_count = repos.len(); repos.retain(|r| !repos_match(r, repository)); if repos.len() < initial_count { removed = true; if repos.is_empty() { // Remove empty file fs::remove_file(&path) .map_err(|e| format!("Failed to remove {}: {e}", path.display()))?; } else { // Write updated repositories self.write_repositories(&path, &repos) .map_err(|e| format!("Failed to update {}: {e}", path.display()))?; } } } Ok(removed) } /// Enable or disable a repository pub fn set_repository_enabled( &self, repository: &Repository, enabled: bool, ) -> Result { let mut modified = false; let all_files = self.scan_all_repositories()?; for (path, mut repos) in all_files { let mut changed = false; for repo in repos.iter_mut() { if repos_match(repo, repository) && repo.enabled != Some(enabled) { repo.enabled = Some(enabled); changed = true; modified = true; } } if changed { self.write_repositories(&path, &repos) .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?; } } Ok(modified) } /// Add a component to all matching repositories pub fn add_component_to_repositories( &self, component: &str, filter: impl Fn(&Repository) -> bool, ) -> Result { let mut modified_count = 0; let all_files = self.scan_all_repositories()?; for (path, mut repos) in all_files { let mut changed = false; for repo in repos.iter_mut() { if filter(repo) { if let Some(components) = &mut repo.components { if !components.contains(&component.to_string()) { components.push(component.to_string()); changed = true; modified_count += 1; } } else { repo.components = Some(vec![component.to_string()]); changed = true; modified_count += 1; } } } if changed { self.write_repositories(&path, &repos) .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?; } } Ok(modified_count) } /// Enable source repositories pub fn enable_source_repositories( &self, create_if_missing: bool, ) -> Result<(u32, u32), String> { let mut enabled_count = 0; let mut created_count = 0; let all_files = self.scan_all_repositories()?; for (path, mut repos) in all_files { let mut changed = false; let mut new_repos = Vec::new(); for repo in repos.iter_mut() { // Check if this repo has binary type but not source if repo.types.contains(&RepositoryType::Binary) && !repo.types.contains(&RepositoryType::Source) { if repo.enabled == Some(false) { // Just enable existing disabled source repo repo.types.insert(RepositoryType::Source); repo.enabled = Some(true); enabled_count += 1; changed = true; } else if create_if_missing { // Create a new source repository entry let mut source_repo = repo.clone(); source_repo.types = HashSet::from([RepositoryType::Source]); new_repos.push(source_repo); created_count += 1; changed = true; } } } // Add any new repositories repos.extend(new_repos); if changed { self.write_repositories(&path, &repos) .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?; } } Ok((enabled_count, created_count)) } /// List all repositories with their file paths pub fn list_all_repositories(&self) -> Result, String> { let files = self.scan_all_repositories()?; // Pre-calculate capacity to avoid reallocations let total_repos: usize = files.iter().map(|(_, repos)| repos.len()).sum(); let mut all_repos = Vec::with_capacity(total_repos); for (path, repos) in files { for repo in repos.iter() { all_repos.push((path.clone(), repo.clone())); } } Ok(all_repos) } /// Generate a keyring filename for a repository pub fn generate_keyring_filename(&self, repository_name: &str) -> String { let sanitized = repository_name.replace(['/', ':', ' '], "-").to_lowercase(); format!("{sanitized}.gpg") } /// Save a GPG key to the keyring directory pub fn save_key(&self, key_data: &[u8], filename: &str) -> io::Result { let key_path = self.get_keyring_path(filename); fs::write(&key_path, key_data)?; Ok(key_path) } } /// File format for APT source list files #[derive(Debug, Clone, Copy, PartialEq)] pub enum FileFormat { /// Deb822 format (new style) Deb822, /// Legacy format (one-line style) Legacy, } /// Check if two repositories match (have the same URIs, suites, and components) fn repos_match(repo1: &Repository, repo2: &Repository) -> bool { // Compare types if repo1.types != repo2.types { return false; } // Compare URIs let uris1: HashSet<_> = repo1.uris.iter().collect(); let uris2: HashSet<_> = repo2.uris.iter().collect(); if uris1 != uris2 { return false; } // Compare suites let suites1: HashSet<_> = repo1.suites.iter().collect(); let suites2: HashSet<_> = repo2.suites.iter().collect(); if suites1 != suites2 { return false; } // Compare components let components1: HashSet<_> = repo1.components.iter().collect(); let components2: HashSet<_> = repo2.components.iter().collect(); if components1 != components2 { return false; } true } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; use url::Url; fn create_test_manager() -> (SourcesManager, TempDir) { let temp_dir = TempDir::new().unwrap(); let sources_dir = temp_dir.path().join("sources.list.d"); let keyring_dir = temp_dir.path().join("trusted.gpg.d"); let manager = SourcesManager::new(&sources_dir, &keyring_dir); (manager, temp_dir) } fn create_test_repository() -> Repository { Repository { enabled: Some(true), types: HashSet::from([RepositoryType::Binary]), uris: vec![Url::parse("http://example.com/ubuntu").unwrap()], suites: vec!["focal".to_string()], components: Some(vec!["main".to_string()]), architectures: Some(vec!["amd64".to_string()]), ..Default::default() } } #[test] fn test_ensure_directories() { let (manager, _temp_dir) = create_test_manager(); assert!(!manager.sources_dir().exists()); assert!(!manager.keyring_dir().exists()); manager.ensure_directories().unwrap(); assert!(manager.sources_dir().exists()); assert!(manager.keyring_dir().exists()); } #[test] fn test_generate_filename() { let (manager, _) = create_test_manager(); assert_eq!( manager.generate_filename("test-repo", FileFormat::Deb822), "test-repo.sources" ); assert_eq!( manager.generate_filename("Test/Repo:Name", FileFormat::Legacy), "test-repo-name.list" ); } #[test] fn test_add_repository() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo = create_test_repository(); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Verify it was added let path = manager.get_repository_path("test.sources"); assert!(path.exists()); let repos = manager.read_repositories(&path).unwrap(); assert_eq!(repos.len(), 1); assert_eq!(repos[0].uris[0].as_str(), "http://example.com/ubuntu"); // Try to add duplicate - should fail let result = manager.add_repository(&repo, "test2.sources"); assert!(result.is_err()); assert!(result.unwrap_err().contains("already exists")); } #[test] fn test_remove_repository() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo = create_test_repository(); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Remove it let removed = manager.remove_repository(&repo).unwrap(); assert!(removed); // Verify file was removed let path = manager.get_repository_path("test.sources"); assert!(!path.exists()); // Try to remove again - should return false let removed = manager.remove_repository(&repo).unwrap(); assert!(!removed); } #[test] fn test_set_repository_enabled() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let mut repo = create_test_repository(); repo.enabled = Some(true); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Disable it let modified = manager.set_repository_enabled(&repo, false).unwrap(); assert!(modified); // Verify it was disabled let path = manager.get_repository_path("test.sources"); let repos = manager.read_repositories(&path).unwrap(); assert_eq!(repos[0].enabled, Some(false)); // Enable it again let modified = manager.set_repository_enabled(&repo, true).unwrap(); assert!(modified); // Verify it was enabled let repos = manager.read_repositories(&path).unwrap(); assert_eq!(repos[0].enabled, Some(true)); } #[test] fn test_add_component_to_repositories() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo = create_test_repository(); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Add component to repositories from example.com let count = manager .add_component_to_repositories("universe", |r| { r.uris.iter().any(|u| u.host_str() == Some("example.com")) }) .unwrap(); assert_eq!(count, 1); // Verify component was added let path = manager.get_repository_path("test.sources"); let repos = manager.read_repositories(&path).unwrap(); assert!(repos[0] .components .as_ref() .unwrap() .contains(&"universe".to_string())); // Try to add same component again - should not modify let count = manager .add_component_to_repositories("universe", |r| { r.uris.iter().any(|u| u.host_str() == Some("example.com")) }) .unwrap(); assert_eq!(count, 0); } #[test] fn test_enable_source_repositories() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo = create_test_repository(); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Enable source repositories (create if missing) let (enabled, created) = manager.enable_source_repositories(true).unwrap(); assert_eq!(enabled, 0); assert_eq!(created, 1); // Verify source repo was created let path = manager.get_repository_path("test.sources"); let repos = manager.read_repositories(&path).unwrap(); assert_eq!(repos.len(), 2); // Find the source repo let source_repo = repos .iter() .find(|r| r.types.contains(&RepositoryType::Source)) .unwrap(); assert!(source_repo.types.contains(&RepositoryType::Source)); assert!(!source_repo.types.contains(&RepositoryType::Binary)); } #[test] fn test_list_repository_files() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); // Initially empty let files = manager.list_repository_files().unwrap(); assert_eq!(files.len(), 0); // Add some files let repo1 = create_test_repository(); let mut repo2 = create_test_repository(); repo2.suites = vec!["jammy".to_string()]; // Make it different manager.add_repository(&repo1, "test1.sources").unwrap(); manager.add_repository(&repo2, "test2.list").unwrap(); // Should find both files let files = manager.list_repository_files().unwrap(); assert_eq!(files.len(), 2); // Create a non-repository file let non_repo = manager.get_repository_path("test.txt"); fs::write(&non_repo, "not a repo").unwrap(); // Should still only find 2 repository files let files = manager.list_repository_files().unwrap(); assert_eq!(files.len(), 2); } #[test] fn test_scan_all_repositories() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo1 = create_test_repository(); let mut repo2 = create_test_repository(); repo2.uris = vec![Url::parse("http://example2.com/ubuntu").unwrap()]; // Add repositories to different files manager.add_repository(&repo1, "test1.sources").unwrap(); manager.add_repository(&repo2, "test2.sources").unwrap(); // Scan all let all_repos = manager.scan_all_repositories().unwrap(); assert_eq!(all_repos.len(), 2); // Each file should have one repository for (_, repos) in all_repos { assert_eq!(repos.len(), 1); } } #[test] fn test_repository_exists() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let repo = create_test_repository(); // Should not exist initially assert!(manager.repository_exists(&repo).unwrap().is_none()); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Should exist now let existing_path = manager.repository_exists(&repo).unwrap(); assert!(existing_path.is_some()); assert!(existing_path.unwrap().ends_with("test.sources")); } #[test] fn test_save_key() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let key_data = b"-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key\n-----END PGP PUBLIC KEY BLOCK-----"; let key_path = manager.save_key(key_data, "test.gpg").unwrap(); assert!(key_path.exists()); assert_eq!(key_path.file_name().unwrap(), "test.gpg"); let saved_data = fs::read(&key_path).unwrap(); assert_eq!(saved_data, key_data); } #[test] fn test_repos_match() { let repo1 = create_test_repository(); let mut repo2 = repo1.clone(); // Should match identical repos assert!(repos_match(&repo1, &repo2)); // Different types repo2.types.insert(RepositoryType::Source); assert!(!repos_match(&repo1, &repo2)); repo2.types = repo1.types.clone(); // Different URIs repo2.uris.push(Url::parse("http://extra.com").unwrap()); assert!(!repos_match(&repo1, &repo2)); repo2.uris = repo1.uris.clone(); // Different suites repo2.suites.push("bionic".to_string()); assert!(!repos_match(&repo1, &repo2)); repo2.suites = repo1.suites.clone(); // Different components if let Some(ref mut components) = repo2.components { components.push("universe".to_string()); } assert!(!repos_match(&repo1, &repo2)); } #[test] fn test_generate_keyring_filename() { let (manager, _) = create_test_manager(); // Test basic filename assert_eq!( manager.generate_keyring_filename("test-repo"), "test-repo.gpg" ); // Test with special characters that should be sanitized assert_eq!( manager.generate_keyring_filename("Test/Repo:Name With Spaces"), "test-repo-name-with-spaces.gpg" ); // Test empty string assert_eq!(manager.generate_keyring_filename(""), ".gpg"); } #[test] fn test_list_all_repositories() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); // Initially empty let all_repos = manager.list_all_repositories().unwrap(); assert!(all_repos.is_empty()); // Add some repositories let repo1 = create_test_repository(); let mut repo2 = create_test_repository(); repo2.suites = vec!["jammy".to_string()]; manager.add_repository(&repo1, "test1.sources").unwrap(); manager.add_repository(&repo2, "test2.sources").unwrap(); // Should list all repositories with their paths let all_repos = manager.list_all_repositories().unwrap(); assert_eq!(all_repos.len(), 2); // Check that paths are included let paths: Vec<_> = all_repos .iter() .map(|(p, _)| p.file_name().unwrap()) .collect(); assert!(paths.contains(&std::ffi::OsStr::new("test1.sources"))); assert!(paths.contains(&std::ffi::OsStr::new("test2.sources"))); } #[test] fn test_enable_source_repositories_counter_edge_cases() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); // Add a binary repository let mut repo = create_test_repository(); repo.types = HashSet::from([RepositoryType::Binary]); manager.add_repository(&repo, "test.sources").unwrap(); // Enable source repositories with creation let (enabled, created) = manager.enable_source_repositories(true).unwrap(); assert_eq!(enabled, 0); assert_eq!(created, 1); // Check that source repo was actually created let all_repos = manager.list_all_repositories().unwrap(); let source_repos: Vec<_> = all_repos .iter() .filter(|(_, r)| { r.types.contains(&RepositoryType::Source) && !r.types.contains(&RepositoryType::Binary) }) .collect(); assert_eq!(source_repos.len(), 1); // Add a disabled binary repository let mut disabled_repo = create_test_repository(); disabled_repo.types = HashSet::from([RepositoryType::Binary]); disabled_repo.enabled = Some(false); disabled_repo.suites = vec!["jammy".to_string()]; // Make it different manager .add_repository(&disabled_repo, "test2.sources") .unwrap(); // Enable source repositories again // The first binary repo will create another source repo // The disabled binary repo will have source type added and be enabled let (enabled2, created2) = manager.enable_source_repositories(true).unwrap(); assert_eq!(enabled2, 1); // Should enable the disabled binary repo assert_eq!(created2, 1); // Will create a source repo for the new binary repo } #[test] fn test_set_repository_enabled_edge_cases() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let mut repo = create_test_repository(); repo.enabled = Some(true); // Add repository manager.add_repository(&repo, "test.sources").unwrap(); // Try to enable already enabled repo - should return false let modified = manager.set_repository_enabled(&repo, true).unwrap(); assert!(!modified); // Disable it let modified = manager.set_repository_enabled(&repo, false).unwrap(); assert!(modified); // Try to disable already disabled repo - should return false let modified = manager.set_repository_enabled(&repo, false).unwrap(); assert!(!modified); } #[test] fn test_add_component_edge_cases() { let (manager, _) = create_test_manager(); manager.ensure_directories().unwrap(); let mut repo = create_test_repository(); repo.components = Some(vec!["main".to_string()]); manager.add_repository(&repo, "test.sources").unwrap(); // Add component that already exists - should not increment counter let count = manager .add_component_to_repositories("main", |_| true) .unwrap(); assert_eq!(count, 0); // Add new component let count = manager .add_component_to_repositories("universe", |_| true) .unwrap(); assert_eq!(count, 1); // Add to repo with no components initially let mut repo2 = create_test_repository(); repo2.components = None; manager.add_repository(&repo2, "test2.sources").unwrap(); let count = manager .add_component_to_repositories("restricted", |r| r.components.is_none()) .unwrap(); assert_eq!(count, 1); } } apt-sources-0.3.0/src/utils.rs000064400000000000000000000044301046102023000143670ustar 00000000000000use url::Url; /// Strip authentication information from a URL and return it separately pub fn strip_auth_from_url(url: &Url) -> (Url, Option<(String, String)>) { let auth = if !url.username().is_empty() { Some(( url.username().to_string(), url.password().unwrap_or("").to_string(), )) } else { None }; let mut clean_url = url.clone(); if auth.is_some() { clean_url.set_username("").ok(); clean_url.set_password(None).ok(); } (clean_url, auth) } /// Generate a sanitized filename from a URL pub fn generate_filename_from_url(url: &Url, extension: &str) -> String { let host = url.host_str().unwrap_or("unknown"); let path = url.path(); // Remove leading/trailing slashes and replace internal slashes let path_part = path .trim_start_matches('/') .trim_end_matches('/') .replace('/', "-"); let base = if path_part.is_empty() { host.to_string() } else { format!("{}-{}", host, path_part) }; // Sanitize the filename let sanitized = base.replace(':', "-").replace(' ', "-").to_lowercase(); format!("{}.{}", sanitized, extension) } #[cfg(test)] mod tests { use super::*; #[test] fn test_strip_auth_from_url() { let url = Url::parse("https://user:pass@example.com/path").unwrap(); let (clean_url, auth) = strip_auth_from_url(&url); assert_eq!(clean_url.as_str(), "https://example.com/path"); assert_eq!(auth, Some(("user".to_string(), "pass".to_string()))); // URL without auth let url = Url::parse("https://example.com/path").unwrap(); let (clean_url, auth) = strip_auth_from_url(&url); assert_eq!(clean_url.as_str(), "https://example.com/path"); assert_eq!(auth, None); } #[test] fn test_generate_filename_from_url() { let url = Url::parse("https://example.com/ubuntu").unwrap(); assert_eq!( generate_filename_from_url(&url, "sources"), "example.com-ubuntu.sources" ); let url = Url::parse("https://ppa.launchpad.net/user/repo/ubuntu").unwrap(); assert_eq!( generate_filename_from_url(&url, "list"), "ppa.launchpad.net-user-repo-ubuntu.list" ); } }