ingredients-0.2.2/.cargo_vcs_info.json0000644000000001511046102023000133670ustar { "git": { "sha1": "98d42c631e78dcd4fb475966cc9c9e754a63654a" }, "path_in_vcs": "ingredients" }ingredients-0.2.2/CHANGELOG.md000064400000000000000000000024561046102023000137600ustar 00000000000000## Release 0.2.2 This is a re-release of 0.2.1 with no changes to resolve the missing version bump in the Python PyPI package. ## Release 0.2.1 **Added**: - Added support for generating CLI completions (bash, fish, zsh, nushell). - Passing file paths that are not UTF-8 as CLI arguments is supported now. **Fixed**: - Improved detection of stripped `path`- and `git`-type dependencies. ## Release 0.2.0 **Added**: - Added CLI argument to filter items by minimum severity. - Added fallback mechanism if `path_in_vcs` is not available (crates published with old versions of `cargo`). **Changed**: - *Breaking*: Dropped CLI functionality from the "default" features. - Improved rendering of unified diffs for file content changes. - Simplified logic around the handling of `.cargo_vcs_info.json` files. - Updated `cargo_metadata` dependency to `v0.23`. **Fixed**: - Fixed formatting of metadata paths of target-specific dependencies. - Fixed logic bug in version requirement comparison. - Refactored code for report generation to avoid code duplication. - Fixed detection of stripped `git`-type dependencies. - Explicitly call git in non-interactive mode (improves detection of invalid repository URLs). ## Release 0.1.1 - Updated README and add some example code snippets. ## Release 0.1.0 Initial release. ingredients-0.2.2/Cargo.lock0000644000001415161046102023000113550ustar # 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 = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.60.2", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] [[package]] name = "cargo-platform" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8abf5d501fd757c2d2ee78d0cc40f606e92e3a63544420316565556ed28485e2" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror", ] [[package]] name = "cc" version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_complete" version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", ] [[package]] name = "clap_complete_nushell" version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685bc86fd34b7467e0532a4f8435ab107960d69a243785ef0275e571b35b641a" dependencies = [ "clap", "clap_complete", ] [[package]] name = "clap_derive" version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[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-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[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 = "env_filter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", "zlib-rs", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[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 = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-task", "pin-project-lite", "pin-utils", ] [[package]] name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "h2" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "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.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-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.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", ] [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "ingredients" version = "0.2.2" dependencies = [ "anstream", "anyhow", "cargo_metadata", "clap", "clap_complete", "clap_complete_nushell", "env_logger", "flate2", "owo-colors", "reqwest", "serde", "serde_json", "similar", "tar", "tempfile", "thiserror", "tokio", "tracing", "url", "walkdir", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde_core", ] [[package]] name = "jiff-static" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "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.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "owo-colors" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 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 = "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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", "futures-core", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", "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 = "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 = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "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.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", "serde_core", ] [[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.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[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 = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" 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 = "tar" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", "xattr", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", "windows-sys 0.60.2", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-http" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "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.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[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.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.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.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", ] [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zlib-rs" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" ingredients-0.2.2/Cargo.toml0000644000000057701046102023000114010ustar # 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 = "2024" rust-version = "1.85" name = "ingredients" version = "0.2.2" authors = ["Fabio Valentini "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Check ingredients of published Rust crates" readme = "README.md" keywords = [ "crate", "publish", "repository", "supply", "chain", ] categories = [ "command-line-utilities", "development-tools", "security", ] license = "MIT" repository = "https://codeberg.org/decathorpe/ingredients" [features] cli = [ "dep:anstream", "dep:anyhow", "dep:clap", "dep:clap_complete", "dep:clap_complete_nushell", "dep:env_logger", "dep:owo-colors", ] default = [] [lib] name = "ingredients" path = "src/lib.rs" [[bin]] name = "ingredients" path = "src/main.rs" required-features = ["cli"] [dependencies.anstream] version = "0.6" optional = true [dependencies.anyhow] version = "1" optional = true [dependencies.cargo_metadata] version = "0.23" [dependencies.clap] version = "4" features = [ "derive", "wrap_help", ] optional = true [dependencies.clap_complete] version = "4.5.17" optional = true [dependencies.clap_complete_nushell] version = "4" optional = true [dependencies.env_logger] version = "0.11" optional = true [dependencies.flate2] version = "1.1.8" features = ["zlib-rs"] default-features = false [dependencies.owo-colors] version = "4" optional = true default-features = false [dependencies.reqwest] version = "0.12" features = [ "http2", "native-tls", ] default-features = false [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.similar] version = "2" [dependencies.tar] version = "0.4" [dependencies.tempfile] version = "3" [dependencies.thiserror] version = "2" [dependencies.tokio] version = "1" features = [ "fs", "macros", "process", "rt-multi-thread", ] [dependencies.tracing] version = "0.1" features = ["log"] [dependencies.url] version = "2" default-features = false [dependencies.walkdir] version = "2" [lints.clippy] allow_attributes = "warn" dbg_macro = "warn" expect_used = "deny" fallible_impl_from = "deny" indexing_slicing = "deny" missing_const_for_fn = "warn" missing_errors_doc = "warn" missing_panics_doc = "warn" print_stderr = "deny" print_stdout = "deny" todo = "warn" unneeded_field_pattern = "deny" unwrap_used = "deny" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] missing_docs = "warn" unnameable_types = "warn" ingredients-0.2.2/Cargo.toml.orig000064400000000000000000000033441046102023000150330ustar 00000000000000[package] name = "ingredients" description = "Check ingredients of published Rust crates" license = "MIT" readme = "README.md" categories = ["command-line-utilities", "development-tools", "security"] keywords = ["crate", "publish", "repository", "supply", "chain"] version.workspace = true edition.workspace = true rust-version.workspace = true repository.workspace = true authors.workspace = true [[bin]] name = "ingredients" path = "src/main.rs" required-features = ["cli"] [features] default = [] cli = [ "dep:anstream", "dep:anyhow", "dep:clap", "dep:clap_complete", "dep:clap_complete_nushell", "dep:env_logger", "dep:owo-colors", ] [dependencies] cargo_metadata = { version = "0.23" } flate2 = { version = "1.1.8", features = ["zlib-rs"], default-features = false } reqwest = { version = "0.12", features = ["http2", "native-tls"], default-features = false } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } similar = { version = "2" } tar = { version = "0.4" } tempfile = { version = "3" } thiserror = { version = "2" } tokio = { version = "1", features = ["fs", "macros", "process", "rt-multi-thread"] } tracing = { version = "0.1", features = ["log"] } url = { version = "2", default-features = false } walkdir = { version = "2" } # optional dependencies anstream = { version = "0.6", optional = true } anyhow = { version = "1", optional = true } clap = { version = "4", features = ["derive", "wrap_help"], optional = true } clap_complete = { version = "4.5.17", optional = true } clap_complete_nushell = { version = "4", optional = true } env_logger = { version = "0.11", optional = true } owo-colors = { version = "4", default-features = false, optional = true } [lints] workspace = true ingredients-0.2.2/LICENSE000064400000000000000000000020141046102023000131420ustar 00000000000000MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ingredients-0.2.2/README.md000064400000000000000000000104331046102023000134200ustar 00000000000000# Check ingredients of published Rust crates This crate implements two modes for checking ingredients of Rust crates: * `report`: Comparing published crate sources from [crates.io] with the associated contents of the project's version control system. * `diff`: Compare contents of two different, published versions of the same crate. ## `report` mode This comparison mode reads crate metadata (from `Cargo.toml` and from the `.cargo_vcs_info.json` file that is embedded by cargo during the publishing process), fetches the corresponding `git` repository, and compares the contents of the published crate with the contents of the project repository at the `ref` that is recorded in `.cargo_vcs_info.json`. Any actual differences in file contents or crate metadata are considered to be *errors*. Issues that prevent checking for differences (like missing metadata or an invalid git repository) are considered *fatal*. Example using the Rust API: ```rust,no_run use ingredients::Crate; #[tokio::main] async fn main() { let krate = Crate::download("syn", "2.0.111").await.unwrap(); let report = krate.report().await.unwrap(); println!("{report}"); } ``` Example using the CLI: ```sh ingredients report syn 2.0.111 ``` Some differences are "expected" and are not reported: * The `Cargo.toml` file is processed and rewritten during the publishing process. Instead, the `Cargo.toml` contents from the repository is compared to `Cargo.toml.orig`, which contains the original, unmodified file contents. * Instead, crate metadata is compared by parsing `Cargo.toml` contents and checking for semantic equivalence instead of byte-for-byte equivalence. * Dependencies that are "path"-based are stripped by cargo during the publishing process. Differences in crate dependencies that are solely due to "path"-based dependencies having been stripped are ignored and not reported. * Symbolic links present in the project repository are resolved during the publishing process, cargo includes actual files in published crates instead. ## `diff` mode This mode compares crate metadata and contents between two source archives that were published to [crates.io]. It is much less strict than the "report" mode (because differences are expected when comparing two different versions of a crate), but will report differences with more granularity than just reporting an error on *any* difference. As such, the only difference that actually triggers a lint with "error" severity is if the crate *name* is different. This should only ever happen when comparing two *different* crates (but which might be useful to do in cases where a crate is "renamed", i.e. new versions are published under a different name). Example using the Rust API: ```rust,no_run use ingredients::Crate; #[tokio::main] async fn main() { let old_krate = Crate::download("syn", "2.0.110").await.unwrap(); let new_krate = Crate::download("syn", "2.0.111").await.unwrap(); let diff = old_krate.diff(&new_krate).unwrap(); println!("{diff}"); } ``` Example using the CLI: ```sh ingredients diff syn 2.0.110 2.0.111 ``` ## Features * `cli` (disabled by default) Almost all functionality from the crate API is also available from the command-line interface. To build the `ingredients` command-line program, compile with the `cli` feature enabled. ## Installation From [crates.io]: `cargo install -F cli ingredients` ## External dependencies **Building**: * `cargo` (refer to `package.rust-version` in `Cargo.toml` for the minimum supported Rust version) * `openssl` development headers must be available (for `reqwest/native-tls`) **Runtime**: * `cargo` must be available in `$PATH` Loading and parsing crate metadata is implemented based on the `cargo_metadata` crate, which calls `cargo metadata` internally. * `git` must be available in `$PATH` for "report" mode The `git` command is used for checking out git repositories in `Crate::report`, and in turn, it is also required by the `ingredients report` subcommand. Moving to a solution for cloning / checkout out git repositories that does not rely on an external `git` command is planned, but currently blocked by missing support for shallow clones / cloning non-branch refs in `gitoxide` (see ), among other issues. [crates.io]: https://crates.io ingredients-0.2.2/src/compare/common.rs000064400000000000000000000014111046102023000162100ustar 00000000000000use cargo_metadata::Dependency; // custom "PartialEq" implementation for Dependency: // ignores fields that are mangled by publishing (.source, .path) pub fn dependency_partial_eq(kdep: &Dependency, udep: &Dependency) -> bool { if kdep.name != udep.name { return false; } if kdep.req != udep.req { return false; } if kdep.kind != udep.kind { return false; } if kdep.optional != udep.optional { return false; } if kdep.uses_default_features != udep.uses_default_features { return false; } if kdep.features != udep.features { return false; } if kdep.target != udep.target { return false; } if kdep.rename != udep.rename { return false; } true } ingredients-0.2.2/src/compare/crate_crate.rs000064400000000000000000000341251046102023000172040ustar 00000000000000use std::cmp::Ordering; use std::collections::BTreeMap; use std::path::Path; use cargo_metadata::{Dependency, Target}; use tracing::{debug, trace}; use crate::diff::DiffItem; use crate::error::Error; use crate::format::{make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target}; use crate::krate::Crate; use crate::metadata::Metadata; use crate::utils::file_mode; use super::common::dependency_partial_eq; use super::version_req::compare_version_req; pub struct CrateComparator<'a> { old: &'a Crate, new: &'a Crate, } impl<'a> CrateComparator<'a> { pub const fn new(old: &'a Crate, new: &'a Crate) -> Self { Self { old, new } } pub fn compare(&self) -> Result, Error> { let mut items = Vec::new(); let old_metadata = &self.old.metadata; let new_metadata = &self.new.metadata; let old_contents = self.old.file_contents()?; let new_contents = self.new.file_contents()?; debug!("Comparing crate contents"); items.extend(compare_metadata(old_metadata, new_metadata)); for file in &new_contents.files { if !old_contents.files.contains(file) { items.push(DiffItem::FileAdded { path: file.to_string_lossy().to_string(), }); continue; } trace!("Comparing files at path: {}", file.to_string_lossy()); items.extend(self.compare_contents(&file, &file)?); items.extend(self.compare_modes(&file, &file)?); } for file in &old_contents.files { if !new_contents.files.contains(file) { items.push(DiffItem::FileRemoved { path: file.to_string_lossy().to_string(), }); } } Ok(items) } fn path_in_crate_old(&self, path: &str) -> String { let name = self.old.metadata.inner.name.as_str(); let version = self.old.metadata.inner.version.to_string(); format!("{name}-{version}/{path}") } fn path_in_crate_new(&self, path: &str) -> String { let name = self.new.metadata.inner.name.as_str(); let version = self.new.metadata.inner.version.to_string(); format!("{name}-{version}/{path}") } fn compare_contents>(&self, old_path: P, new_path: P) -> Result, Error> { let mut items = Vec::new(); let old_file_content = self.old.read_entry_to_bytes(&old_path)?; let new_file_content = self.new.read_entry_to_bytes(&new_path)?; if old_file_content != new_file_content { let old_fc_lf: Vec<_> = old_file_content.iter().filter(|c| **c != b'\r').collect(); let new_fc_lf: Vec<_> = new_file_content.iter().filter(|c| **c != b'\r').collect(); if old_fc_lf == new_fc_lf { // file contents differ only because of line endings items.push(DiffItem::LineEndingsChange { path: old_path.as_ref().to_string_lossy().to_string(), }); } else { let diff = if let (Ok(old_file_utf8), Ok(new_file_utf8)) = ( self.old.read_entry_to_string(&old_path), self.new.read_entry_to_string(&new_path), ) { Some(make_diff( &old_file_utf8, &new_file_utf8, Some(( &self.path_in_crate_old(&old_path.as_ref().to_string_lossy()), &self.path_in_crate_new(&new_path.as_ref().to_string_lossy()), )), )) } else { // file is probably not a text file None }; items.push(DiffItem::FileChanged { path: old_path.as_ref().to_string_lossy().to_string(), diff, }); } } Ok(items) } fn compare_modes>(&self, old_path: P, new_path: P) -> Result, Error> { let mut items = Vec::new(); let old_file_mode = file_mode(self.old.root.join(&old_path))?; let new_file_mode = file_mode(self.new.root.join(&new_path))?; if old_file_mode != new_file_mode { items.push(DiffItem::PermissionChange { path: old_path.as_ref().to_string_lossy().to_string(), old: old_file_mode, new: new_file_mode, }); } Ok(items) } } #[expect(clippy::too_many_lines)] fn compare_metadata(old_metadata: &Metadata, new_metadata: &Metadata) -> Vec { let mut items = Vec::new(); let old_md = &old_metadata.inner; let new_md = &new_metadata.inner; // package.id: not stable // package.source: always None // package.manifest_path: only present in "cargo metadata" output, not in Cargo.toml // package.publish: not relevant for crates from crates.io if old_md.name != new_md.name { // this should never happen? items.push(DiffItem::NameChange { old: old_md.name.to_string(), new: new_md.name.to_string(), }); } if old_md.version != new_md.version { items.push(DiffItem::VersionChange { old: old_md.version.to_string(), new: new_md.version.to_string(), }); } if old_md.edition != new_md.edition { items.push(DiffItem::EditionChange { old: old_md.edition.to_string(), new: new_md.edition.to_string(), }); } if old_md.rust_version != new_md.rust_version { items.push(DiffItem::RustVersionChange { old: old_md.rust_version.as_ref().map(ToString::to_string), new: new_md.rust_version.as_ref().map(ToString::to_string), }); } if old_md.authors != new_md.authors { let (added, removed) = compare_str_list(&old_md.authors, &new_md.authors); items.push(DiffItem::AuthorsChange { added, removed }); } if old_md.description != new_md.description { items.push(DiffItem::DescriptionChange { old: old_md.description.clone(), new: new_md.description.clone(), }); } if old_md.license != new_md.license { items.push(DiffItem::LicenseChange { old: old_md.license.clone(), new: new_md.license.clone(), }); } if old_md.license_file != new_md.license_file { items.push(DiffItem::LicenseFileChange { old: old_md.license_file.as_ref().map(ToString::to_string), new: new_md.license_file.as_ref().map(ToString::to_string), }); } if old_md.readme != new_md.readme { items.push(DiffItem::ReadmeChange { old: old_md.readme.as_ref().map(ToString::to_string), new: new_md.readme.as_ref().map(ToString::to_string), }); } if old_md.categories != new_md.categories { let (added, removed) = compare_str_list(&old_md.categories, &new_md.categories); items.push(DiffItem::CategoriesChange { added, removed }); } if old_md.keywords != new_md.keywords { let (added, removed) = compare_str_list(&old_md.keywords, &new_md.keywords); items.push(DiffItem::KeywordsChange { added, removed }); } if old_md.repository != new_md.repository { items.push(DiffItem::RepositoryChange { old: old_md.repository.clone(), new: new_md.repository.clone(), }); } if old_md.homepage != new_md.homepage { items.push(DiffItem::HomepageChange { old: old_md.homepage.clone(), new: new_md.homepage.clone(), }); } if old_md.documentation != new_md.documentation { items.push(DiffItem::DocumentationChange { old: old_md.documentation.clone(), new: new_md.documentation.clone(), }); } if old_md.links != new_md.links { items.push(DiffItem::LinksChange { old: old_md.links.clone(), new: new_md.links.clone(), }); } if old_md.default_run != new_md.default_run { items.push(DiffItem::DefaultRunChange { old: old_md.default_run.clone(), new: new_md.default_run.clone(), }); } if old_md.metadata != new_md.metadata { items.push(DiffItem::MetadataChange { old: old_md.metadata.clone(), new: new_md.metadata.clone(), }); } items.extend(compare_dependencies(&old_md.dependencies, &new_md.dependencies)); items.extend(compare_targets(&old_md.targets, &new_md.targets)); items.extend(compare_features(&old_md.features, &new_md.features)); items } fn compare_dependencies(old_deps: &[Dependency], new_deps: &[Dependency]) -> Vec { if old_deps.len() == new_deps.len() && old_deps.iter().zip(new_deps).all(|(k, u)| dependency_partial_eq(k, u)) { return Vec::new(); } let mut items = Vec::new(); // path_from_dep is specific to dependency kind and target let old_deps_map: BTreeMap = old_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect(); let new_deps_map: BTreeMap = new_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect(); for (path, old_dep) in &old_deps_map { if let Some(new_dep) = new_deps_map.get(path) { let (lower_bound, upper_bound) = compare_version_req(&old_dep.req, &new_dep.req); if lower_bound == Ordering::Greater || upper_bound == Ordering::Greater { items.push(DiffItem::DependencyUpgraded { path: path_from_dep(new_dep), old: old_dep.req.to_string(), new: new_dep.req.to_string(), }); } if lower_bound == Ordering::Less || upper_bound == Ordering::Less { items.push(DiffItem::DependencyDowngraded { path: path_from_dep(new_dep), old: old_dep.req.to_string(), new: new_dep.req.to_string(), }); } let (added_features, removed_features) = compare_str_list(&old_dep.features, &new_dep.features); if !added_features.is_empty() || !removed_features.is_empty() { items.push(DiffItem::DependencyFeatures { path: path_from_dep(new_dep), added: added_features, removed: removed_features, }); } if old_dep.optional != new_dep.optional { items.push(DiffItem::DependencyOptionality { path: path_from_dep(new_dep), old: old_dep.optional, new: new_dep.optional, }); } } else { items.push(DiffItem::DependencyRemoved { path: path_from_dep(old_dep), value: value_from_dep(old_dep), }); } } for (path, new_dep) in &new_deps_map { if !old_deps_map.contains_key(path) { items.push(DiffItem::DependencyAdded { path: path_from_dep(new_dep), value: value_from_dep(new_dep), }); } } items } fn compare_targets(old_targets: &[Target], new_targets: &[Target]) -> Vec { if old_targets == new_targets { return Vec::new(); } let mut items = Vec::new(); let old_target_map: BTreeMap = old_targets .iter() .map(|target| (path_from_target(target), target)) .collect(); let new_target_map: BTreeMap = new_targets .iter() .map(|target| (path_from_target(target), target)) .collect(); for (path, old_target) in &old_target_map { if let Some(new_target) = new_target_map.get(path) { if old_target != new_target { items.push(DiffItem::TargetChanged { path: path.clone(), old: value_from_target(old_target), new: value_from_target(new_target), }); } } else { items.push(DiffItem::TargetRemoved { path: path.clone(), target: value_from_target(old_target), }); } } for (path, new_target) in &new_target_map { if !old_target_map.contains_key(path) { items.push(DiffItem::TargetAdded { path: path.clone(), target: value_from_target(new_target), }); } } items } type Features = BTreeMap>; fn compare_features(old_features: &Features, new_features: &Features) -> Vec { if old_features == new_features { return Vec::new(); } let mut items = Vec::new(); for (old_key, old_values) in old_features { match new_features.get(old_key) { Some(new_values) => { if old_values == new_values { continue; } let (added, removed) = compare_str_list(old_values, new_values); items.push(DiffItem::FeatureChanged { name: String::from(old_key), added, removed, }); }, None => { items.push(DiffItem::FeatureRemoved { name: String::from(old_key), }); }, } } for new_key in new_features.keys() { if old_features.get(new_key).is_none() { items.push(DiffItem::FeatureAdded { name: String::from(new_key), }); } } items } fn compare_str_list(old: &[String], new: &[String]) -> (Vec, Vec) { let mut added = Vec::new(); let mut removed = Vec::new(); for new_str in new { if !old.contains(new_str) { added.push(new_str.clone()); } } for old_str in old { if !new.contains(old_str) { removed.push(old_str.clone()); } } (added, removed) } ingredients-0.2.2/src/compare/crate_urepo.rs000064400000000000000000000356011046102023000172400ustar 00000000000000use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt::Display; use std::path::{Path, PathBuf}; use cargo_metadata::camino::Utf8PathBuf; use cargo_metadata::{Dependency, Target}; use tracing::{debug, trace}; use crate::error::Error; use crate::format::{format_list, make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target}; use crate::krate::Crate; use crate::metadata::Metadata; use crate::report::ReportItem; use crate::upstream::Repository; use crate::utils::file_mode; use super::common::dependency_partial_eq; pub struct RepoComparator<'a> { krate: &'a Crate, urepo: &'a Repository<'a>, } impl<'a> RepoComparator<'a> { pub const fn new(krate: &'a Crate, urepo: &'a Repository) -> Self { Self { krate, urepo } } fn path_in_krate(&self, path: &str) -> String { let name = self.krate.metadata.inner.name.as_str(); let version = self.krate.metadata.inner.version.to_string(); format!("{name}-{version}/{path}") } fn path_in_urepo(&self, path: &str) -> String { let shortref = &self.urepo.id[0..7]; let path_in_vcs = &self.urepo.path_in_vcs; format!("git#{shortref}/{path_in_vcs}/{path}") } pub fn compare(&self) -> Result, Error> { let mut items = Vec::new(); let krate_cargo_toml = std::fs::read_to_string(self.krate.cargo_toml_orig())?; let urepo_cargo_toml = std::fs::read_to_string(self.urepo.cargo_toml())?; let krate_metadata = &self.krate.metadata; let urepo_metadata = &self.urepo.metadata; let krate_contents = self.krate.file_contents()?; let urepo_contents = self.urepo.file_contents()?; debug!("Comparing crate contents with repository contents"); items.extend(compare_metadata(krate_metadata, urepo_metadata, &krate_contents.files)); if krate_cargo_toml != urepo_cargo_toml { let kct_lf: Vec<_> = krate_cargo_toml.bytes().filter(|c| *c != b'\r').collect(); let rct_lf: Vec<_> = urepo_cargo_toml.bytes().filter(|c| *c != b'\r').collect(); if kct_lf == rct_lf { // file contents differ only because of line endings items.push(ReportItem::LineEndings { path: String::from("Cargo.toml.orig"), }); } else { let diff = Some(make_diff( &krate_cargo_toml, &urepo_cargo_toml, Some(( &self.path_in_krate("Cargo.toml.orig"), &self.path_in_urepo("Cargo.toml"), )), )); items.push(ReportItem::ContentMismatch { path: String::from("Cargo.toml.orig"), diff, }); } } for file in &krate_contents.broken_links { items.push(ReportItem::BrokenSymlinkInCrate { path: file.to_string_lossy().to_string(), }); } for file in &urepo_contents.broken_links { items.push(ReportItem::BrokenSymlinkInRepo { path: file.to_string_lossy().to_string(), }); } for file in &krate_contents.outside_base { items.push(ReportItem::BrokenSymlinkInCrate { path: file.to_string_lossy().to_string(), }); } for file in &urepo_contents.outside_base { items.push(ReportItem::BrokenSymlinkInRepo { path: file.to_string_lossy().to_string(), }); } for file in &krate_contents.files { if !urepo_contents.files.contains(file) { items.push(ReportItem::MissingFile { path: file.to_string_lossy().to_string(), }); continue; } trace!("Comparing files at path: {}", file.to_string_lossy()); items.extend(self.compare_contents(&file, &file)?); items.extend(self.compare_modes(&file, &file)?); } Ok(items) } fn compare_contents>(&self, krate_path: P, urepo_path: P) -> Result, Error> { let mut items = Vec::new(); let krate_file_content = self.krate.read_entry_to_bytes(&krate_path)?; let urepo_file_content = self.urepo.read_entry_to_bytes(&urepo_path)?; if krate_file_content != urepo_file_content { let kfc_lf: Vec<_> = krate_file_content.iter().filter(|c| **c != b'\r').collect(); let rfc_lf: Vec<_> = urepo_file_content.iter().filter(|c| **c != b'\r').collect(); if kfc_lf == rfc_lf { // file contents differ only because of line endings items.push(ReportItem::LineEndings { path: krate_path.as_ref().to_string_lossy().to_string(), }); } else { let diff = if let (Ok(ktc), Ok(utc)) = ( self.krate.read_entry_to_string(&krate_path), self.urepo.read_entry_to_string(&urepo_path), ) { Some(make_diff( &ktc, &utc, Some(( &self.path_in_krate(&krate_path.as_ref().to_string_lossy()), &self.path_in_urepo(&urepo_path.as_ref().to_string_lossy()), )), )) } else { // file is probably not a text file None }; items.push(ReportItem::ContentMismatch { path: krate_path.as_ref().to_string_lossy().to_string(), diff, }); } } Ok(items) } fn compare_modes>(&self, krate_path: P, urepo_path: P) -> Result, Error> { let mut items = Vec::new(); let krate_file_mode = file_mode(self.krate.root.join(&krate_path))?; let urepo_file_mode = file_mode(self.urepo.root.join(&self.urepo.path_in_vcs).join(&urepo_path))?; if krate_file_mode != urepo_file_mode { items.push(ReportItem::Permissions { path: krate_path.as_ref().to_string_lossy().to_string(), krate: krate_file_mode, urepo: urepo_file_mode, }); } Ok(items) } } fn compare_metadata(krate_metadata: &Metadata, urepo_metadata: &Metadata, krate_files: &[PathBuf]) -> Vec { let mut items = Vec::new(); let kmd = &krate_metadata.inner; let umd = &urepo_metadata.inner; items.extend(compare_displayable("package.name", &kmd.name, &umd.name)); items.extend(compare_displayable("package.version", &kmd.version, &umd.version)); items.extend(compare_displayable_list("package.authors", &kmd.authors, &umd.authors)); // package.id: not stable // package.source: always None items.extend(compare_displayable_option( "package.description", kmd.description.as_ref(), umd.description.as_ref(), )); items.extend(compare_dependencies(&kmd.dependencies, &umd.dependencies)); items.extend(compare_displayable_option( "package.license", kmd.license.as_ref(), umd.license.as_ref(), )); items.extend(compare_path_option( "package.license-file", kmd.license_file.as_ref(), umd.license_file.as_ref(), )); items.extend(compare_targets(&kmd.targets, &umd.targets, krate_files)); items.extend(compare_features(&kmd.features, &umd.features)); // manifest_path: only present in "cargo metadata" output, not in Cargo.toml items.extend(compare_displayable_list( "package.categories", &kmd.categories, &umd.categories, )); items.extend(compare_displayable_list( "package.keywords", &kmd.keywords, &umd.keywords, )); items.extend(compare_path_option( "package.readme", kmd.readme.as_ref(), umd.readme.as_ref(), )); items.extend(compare_displayable_option( "package.repository", kmd.repository.as_ref(), umd.repository.as_ref(), )); items.extend(compare_displayable_option( "package.homepage", kmd.homepage.as_ref(), umd.homepage.as_ref(), )); items.extend(compare_displayable_option( "package.documentation", kmd.documentation.as_ref(), umd.documentation.as_ref(), )); items.extend(compare_displayable("package.edition", &kmd.edition, &umd.edition)); items.extend(compare_displayable("package.metadata", &kmd.metadata, &umd.metadata)); items.extend(compare_displayable_option( "package.links", kmd.links.as_ref(), umd.links.as_ref(), )); items.extend(compare_displayable_list_option( "package.publish", kmd.publish.as_deref(), umd.publish.as_deref(), )); items.extend(compare_displayable_option( "package.default-run", kmd.default_run.as_ref(), umd.default_run.as_ref(), )); items.extend(compare_displayable_option( "package.rust-version", kmd.rust_version.as_ref(), umd.rust_version.as_ref(), )); items } fn compare_dependencies(kdeps: &[Dependency], udeps: &[Dependency]) -> Vec { if kdeps.len() == udeps.len() && kdeps.iter().zip(udeps).all(|(k, u)| dependency_partial_eq(k, u)) { return Vec::new(); } let mut items = Vec::new(); for kdep in kdeps { if udeps.iter().any(|udep| dependency_partial_eq(kdep, udep)) { continue; } let path = path_from_dep(kdep); items.push(ReportItem::metadata_mismatch(path, Some(value_from_dep(kdep)), None)); } for udep in udeps { if kdeps.iter().any(|kdep| dependency_partial_eq(kdep, udep)) { continue; } // non-crates.io sources (like path- or git-based dependencies) are stripped during publishing if let Some(source) = &udep.source && !source.is_crates_io() { debug!("Skipping non-crates.io / git dependency: {}", value_from_dep(udep)); continue; } if udep.path.is_some() { debug!("Skipping non-crates.io / path dependency: {}", value_from_dep(udep)); continue; } let path = path_from_dep(udep); items.push(ReportItem::metadata_mismatch(path, None, Some(value_from_dep(udep)))); } items } fn compare_targets(ktargets: &[Target], utargets: &[Target], krate_files: &[PathBuf]) -> Vec { if ktargets == utargets { return Vec::new(); } let mut items = Vec::new(); for ktarget in ktargets { if utargets.contains(ktarget) { continue; } let path = path_from_target(ktarget); items.push(ReportItem::metadata_mismatch( path, Some(value_from_target(ktarget)), None, )); } for utarget in utargets { if ktargets.contains(utarget) { continue; } if !krate_files.contains(&PathBuf::from(&utarget.src_path)) { // targets for paths that are not included when publishing are stripped continue; } let path = path_from_target(utarget); items.push(ReportItem::metadata_mismatch( path, None, Some(value_from_target(utarget)), )); } items } type Features = BTreeMap>; fn compare_features(kfeatures: &Features, ufeatures: &Features) -> Vec { if kfeatures == ufeatures { return Vec::new(); } let mut items = Vec::new(); for (kkey, kvalues) in kfeatures { match ufeatures.get(kkey) { Some(uvalues) => { if kvalues == uvalues { continue; } items.push(ReportItem::metadata_mismatch( format!("features.{kkey}"), Some(format_list(kvalues)), Some(format_list(uvalues)), )); }, None => { items.push(ReportItem::metadata_mismatch( format!("features.{kkey}"), Some(format_list(kvalues)), None, )); }, } } for (ukey, uvalues) in ufeatures { match kfeatures.get(ukey) { Some(kvalues) => { if kvalues == uvalues { continue; } items.push(ReportItem::metadata_mismatch( format!("features.{ukey}"), Some(format_list(kvalues)), Some(format_list(uvalues)), )); }, None => { items.push(ReportItem::metadata_mismatch( format!("features.{ukey}"), None, Some(format_list(uvalues)), )); }, } } items } fn compare_displayable>, D: Display + PartialEq>( field: S, kitem: &D, uitem: &D, ) -> Option { if kitem == uitem { None } else { Some(ReportItem::metadata_mismatch( field, Some(kitem.to_string()), Some(uitem.to_string()), )) } } fn compare_path_option>>( field: S, kitem: Option<&Utf8PathBuf>, uitem: Option<&Utf8PathBuf>, ) -> Option { if kitem == uitem { None } else { Some(ReportItem::metadata_mismatch( field, kitem.map(ToString::to_string), uitem.map(ToString::to_string), )) } } fn compare_displayable_option>, D: Display + PartialEq>( field: S, kitem: Option<&D>, uitem: Option<&D>, ) -> Option { if kitem == uitem { None } else { Some(ReportItem::metadata_mismatch( field, kitem.map(ToString::to_string), uitem.map(ToString::to_string), )) } } fn compare_displayable_list>, D: Display + PartialEq>( field: S, kitems: &[D], uitems: &[D], ) -> Option { if kitems == uitems { None } else { Some(ReportItem::metadata_mismatch( field, Some(format_list(kitems)), Some(format_list(uitems)), )) } } fn compare_displayable_list_option>, D: Display + PartialEq>( field: S, kitems: Option<&[D]>, uitems: Option<&[D]>, ) -> Option { if kitems == uitems { None } else { Some(ReportItem::metadata_mismatch( field, kitems.map(format_list), uitems.map(format_list), )) } } ingredients-0.2.2/src/compare/version_req.rs000064400000000000000000000324331046102023000172640ustar 00000000000000use std::cmp::Ordering; use cargo_metadata::semver::{BuildMetadata, Comparator, Op, Prerelease, Version, VersionReq}; /// Helper function to compare version requirements. /// /// Compares both the lower and upper bound (if any). pub fn compare_version_req(old: &VersionReq, new: &VersionReq) -> (Ordering, Ordering) { const fn version(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Version { Version { major, minor, patch, pre, build: BuildMetadata::EMPTY, } } fn lower_bound(c: &Comparator) -> Option { match c.op { Op::Exact | Op::GreaterEq => Some(version( c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0), c.pre.clone(), )), Op::Less => None, _ => unreachable!(), } } fn upper_bound(c: &Comparator) -> Option { match c.op { Op::Exact | Op::Less => Some(version( c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0), c.pre.clone(), )), Op::GreaterEq => None, _ => unreachable!(), } } if old == new { return (Ordering::Equal, Ordering::Equal); } let old_comps: Vec<_> = old.comparators.iter().flat_map(normalize_version_req).collect(); let new_comps: Vec<_> = new.comparators.iter().flat_map(normalize_version_req).collect(); let old_lower_bound = old_comps.iter().filter_map(lower_bound).max(); let new_lower_bound = new_comps.iter().filter_map(lower_bound).max(); let old_upper_bound = old_comps.iter().filter_map(upper_bound).min(); let new_upper_bound = new_comps.iter().filter_map(upper_bound).min(); let lower_bound_cmp = match (old_lower_bound, new_lower_bound) { (None, None) => Ordering::Equal, (Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, }; let upper_bound_cmp = match (old_upper_bound, new_upper_bound) { (None, None) => Ordering::Equal, (Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, }; (lower_bound_cmp, upper_bound_cmp) } /// Helper function to "normalize" version requirements into a more canonical form /// that only uses `Op::GreaterEq`, `Op::Less`, and `Op::Exact`. /// /// Adapted from the cargo2rpm.semver Python module. #[expect(clippy::too_many_lines)] fn normalize_version_req(comp: &Comparator) -> Vec { const fn comparator(op: Op, major: u64, minor: Option, patch: Option, pre: Prerelease) -> Comparator { Comparator { op, major, minor, patch, pre, } } let mut result = Vec::with_capacity(2); match comp.op { Op::Exact => match (comp.minor, comp.patch) { (None, None) => { result.push(comparator( Op::GreaterEq, comp.major, Some(0), Some(0), Prerelease::EMPTY, )); result.push(comparator( Op::Less, comp.major + 1, Some(0), Some(0), Prerelease::EMPTY, )); }, (Some(minor), None) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor), Some(0), Prerelease::EMPTY, )); result.push(comparator( Op::Less, comp.major, Some(minor + 1), Some(0), Prerelease::EMPTY, )); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::Exact, comp.major, Some(minor), Some(patch), comp.pre.clone(), )); }, (None, Some(_)) => unreachable!(), }, Op::Greater => match (comp.minor, comp.patch) { (None, None) => { result.push(comparator( Op::GreaterEq, comp.major + 1, Some(0), Some(0), Prerelease::EMPTY, )); }, (Some(minor), None) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor + 1), Some(0), Prerelease::EMPTY, )); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor), Some(patch + 1), Prerelease::EMPTY, )); }, (None, Some(_)) => unreachable!(), }, Op::GreaterEq => match (comp.minor, comp.patch) { (None, None) => { result.push(comparator( Op::GreaterEq, comp.major, Some(0), Some(0), Prerelease::EMPTY, )); }, (Some(minor), None) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor), Some(0), Prerelease::EMPTY, )); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor), Some(patch), comp.pre.clone(), )); }, (None, Some(_)) => unreachable!(), }, Op::Less => match (comp.minor, comp.patch) { (None, None) => { result.push(comparator(Op::Less, comp.major, Some(0), Some(0), Prerelease::EMPTY)); }, (Some(minor), None) => { result.push(comparator( Op::Less, comp.major, Some(minor), Some(0), Prerelease::EMPTY, )); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::Less, comp.major, Some(minor), Some(patch), comp.pre.clone(), )); }, (None, Some(_)) => unreachable!(), }, Op::LessEq => match (comp.minor, comp.patch) { (None, None) => { result.push(comparator( Op::Less, comp.major + 1, Some(0), Some(0), Prerelease::EMPTY, )); }, (Some(minor), None) => { result.push(comparator( Op::Less, comp.major, Some(minor + 1), Some(0), Prerelease::EMPTY, )); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::Less, comp.major, Some(minor), Some(patch + 1), Prerelease::EMPTY, )); }, (None, Some(_)) => unreachable!(), }, Op::Tilde => match (comp.minor, comp.patch) { (None, None) => { result.extend(normalize_version_req(&comparator( Op::Exact, comp.major, None, None, Prerelease::EMPTY, ))); }, (Some(minor), None) => { result.extend(normalize_version_req(&comparator( Op::Exact, comp.major, Some(minor), None, Prerelease::EMPTY, ))); }, (Some(minor), Some(patch)) => { result.push(comparator( Op::GreaterEq, comp.major, Some(minor), Some(patch), comp.pre.clone(), )); result.push(comparator( Op::Less, comp.major, Some(minor + 1), Some(0), Prerelease::EMPTY, )); }, (None, Some(_)) => unreachable!(), }, Op::Caret => match (comp.major, comp.minor, comp.patch) { (major, None, None) => { result.extend(normalize_version_req(&comparator( Op::Exact, major, None, None, Prerelease::EMPTY, ))); }, (0, Some(0), None) => { result.extend(normalize_version_req(&comparator( Op::Exact, 0, Some(0), None, Prerelease::EMPTY, ))); }, (major, Some(minor), None) => { result.extend(normalize_version_req(&comparator( Op::Caret, major, Some(minor), Some(0), Prerelease::EMPTY, ))); }, (0, Some(0), Some(patch)) => { result.extend(normalize_version_req(&comparator( Op::Exact, 0, Some(0), Some(patch), comp.pre.clone(), ))); }, (0, Some(minor), Some(patch)) => { result.push(comparator(Op::GreaterEq, 0, Some(minor), Some(patch), comp.pre.clone())); result.push(comparator(Op::Less, 0, Some(minor + 1), Some(0), Prerelease::EMPTY)); }, (major, Some(minor), Some(patch)) => { result.push(comparator( Op::GreaterEq, major, Some(minor), Some(patch), comp.pre.clone(), )); result.push(comparator(Op::Less, major + 1, Some(0), Some(0), Prerelease::EMPTY)); }, (_, None, Some(_)) => unreachable!(), }, Op::Wildcard => match (comp.minor, comp.patch) { (None, None) => { result.extend(normalize_version_req(&comparator( Op::Exact, comp.major, None, None, Prerelease::EMPTY, ))); }, (Some(minor), None) => { result.extend(normalize_version_req(&comparator( Op::Exact, comp.major, Some(minor), None, Prerelease::EMPTY, ))); }, (Some(_) | None, Some(_)) => unreachable!(), }, _ => unimplemented!(), } result } #[cfg(test)] mod tests { use super::*; #[test] fn test_compare_version_req() { let vr = |s| VersionReq::parse(s).unwrap(); assert_eq!( compare_version_req(&vr("^1.0"), &vr("^2.0")), (Ordering::Greater, Ordering::Greater) ); assert_eq!( compare_version_req(&vr("^2.0"), &vr("^1.0")), (Ordering::Less, Ordering::Less) ); assert_eq!( compare_version_req(&vr(">=1.0,<3"), &vr("^2.0")), (Ordering::Greater, Ordering::Equal) ); assert_eq!( compare_version_req(&vr("^2.0"), &vr(">=1.0,<3")), (Ordering::Less, Ordering::Equal) ); } #[test] fn test_normalize_version_req() { let ct = |s| Comparator::parse(s).unwrap(); assert_eq!(normalize_version_req(&ct("^1.0")), vec![ct(">=1.0.0"), ct("<2.0.0")]); assert_eq!(normalize_version_req(&ct("^0.1")), vec![ct(">=0.1.0"), ct("<0.2.0")]); assert_eq!(normalize_version_req(&ct("^0.0.1")), vec![ct("=0.0.1")]); assert_eq!(normalize_version_req(&ct("~1.1")), vec![ct(">=1.1.0"), ct("<1.2.0")]); assert_eq!(normalize_version_req(&ct("~0.1")), vec![ct(">=0.1.0"), ct("<0.2.0")]); assert_eq!(normalize_version_req(&ct("=1")), vec![ct(">=1.0.0"), ct("<2.0.0")]); assert_eq!(normalize_version_req(&ct("=1.1")), vec![ct(">=1.1.0"), ct("<1.2.0")]); assert_eq!(normalize_version_req(&ct("=1.1.0")), vec![ct("=1.1.0")]); } } ingredients-0.2.2/src/compare.rs000064400000000000000000000001611046102023000147210ustar 00000000000000mod common; mod version_req; mod crate_urepo; pub use crate_urepo::*; mod crate_crate; pub use crate_crate::*; ingredients-0.2.2/src/diff.rs000064400000000000000000000770131046102023000142150ustar 00000000000000use std::borrow::Cow; use std::fmt::{self, Display, Formatter, Write}; use serde::Serialize; use serde_json::Value; use crate::format::{format_option_string, make_diff}; use crate::severity::Severity; /// List of differences between two crates #[derive(Debug, Default)] pub struct Diff { items: Vec, } impl Diff { pub(crate) const fn from_items(items: Vec) -> Self { Diff { items } } /// List of differences #[must_use] pub fn items(&self) -> &[DiffItem] { &self.items } /// Get list of differences in machine-readable JSON format /// /// # Panics /// /// This function panics if there are internal errors related to serializing /// data in JSON format. #[must_use] pub fn to_json(&self) -> String { let items: Vec = self .items .iter() .map(|i| JsonDiffItem { severity: i.severity().to_string(), kind: i.kind(), data: i.data(), }) .collect(); // if this fails, something is seriously wrong - just panic #[expect(clippy::expect_used)] serde_json::to_string_pretty(&items).expect("Failed to serialize report as JSON.") } } impl Display for Diff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for item in self.items() { if f.alternate() { write!(f, "{item:#}")?; } else { write!(f, "{item}")?; } } Ok(()) } } /// A specific difference between two crates #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] // keep in sync with the Python enum pub enum DiffItem { // file content changes /// A file was added (it is present in the new version but not the old version). FileAdded { /// Path of the added file path: String, }, /// A file was removed (it is present in the old version but not the new version). FileRemoved { /// Path of the removed file path: String, }, /// Contents of a file have changed. FileChanged { /// Path of the file path: String, /// Diff between old and new contents of the file /// /// If this is `None` then the file is a binary file and / or not valid UTF-8. diff: Option, }, /// Contents of a file have changed *only* due to change in the line endings style. LineEndingsChange { /// Path of the file path: String, }, /// File mode / permissions have changed between old and new version. PermissionChange { /// Path of the file path: String, /// Old permission bits old: String, /// New permission bits new: String, }, // metadata changes /// The crate name has changed. NameChange { /// Old crate name old: String, /// New crate name new: String, }, /// The crate version has changed. VersionChange { /// Old crate version old: String, /// New crate version new: String, }, /// The crate edition has changed. EditionChange { /// Old crate Edition old: String, /// New crate Edition new: String, }, /// The crate MSRV has changed. RustVersionChange { /// Old crate MSRV old: Option, /// New crate MSRV new: Option, }, /// The list of crate authors has changed. AuthorsChange { /// Authors added in the new version added: Vec, /// Authors removed in the new version removed: Vec, }, /// The crate description has changed. DescriptionChange { /// Old crate description old: Option, /// New crate description new: Option, }, /// The crate license has changed. LicenseChange { /// Old crate license expression old: Option, /// New crate license expression new: Option, }, /// The crate license file path has changed. LicenseFileChange { /// Old license file path old: Option, /// New license file path new: Option, }, /// The crate readme file path has changed. ReadmeChange { /// Old readme file path old: Option, /// New readme file path new: Option, }, /// The list of crate categories has changed. CategoriesChange { /// Categories added in the new version added: Vec, /// Categories removed in the new version removed: Vec, }, /// The list of crate keywords has changed. KeywordsChange { /// Keywords added in the new version added: Vec, /// Keywords removed in the new version removed: Vec, }, /// The crate repository URL has changed. RepositoryChange { /// Old repository URL old: Option, /// New repository URL new: Option, }, /// The crate homepage URL has changed. HomepageChange { /// Old homepage URL old: Option, /// New homepage URL new: Option, }, /// The crate documentation page URL has changed. DocumentationChange { /// Old documentation URL old: Option, /// New documentation URL new: Option, }, /// The "native" library this crate links to has changed. LinksChange { /// Old linked native library old: Option, /// New linked native library new: Option, }, /// The default run target of the crate has changed. DefaultRunChange { /// Old default run target old: Option, /// New default run target new: Option, }, /// The additional crate metadata has changed. MetadataChange { /// Old additional metadata old: serde_json::Value, /// New additional metadata new: serde_json::Value, }, // dependency changes /// A new dependency was added to the crate. DependencyAdded { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Value of the dependency /// /// This includes information whether the crate is renamed on imported, the version /// requirement, whether the dependency is optional, whether the dependency uses /// default features, and the list of enabled crate features. value: String, }, /// A dependency was removed from the crate. DependencyRemoved { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Value of the dependency /// /// This includes information whether the crate is renamed on imported, the version /// requirement, whether the dependency is optional, whether the dependency uses /// default features, and the list of enabled crate features. value: String, }, /// A dependency version has been upgraded. DependencyUpgraded { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Old version requirement old: String, /// New version requirement new: String, }, /// A dependency version has been downgraded. DependencyDowngraded { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Old version requirement old: String, /// New version requirement new: String, }, /// The feature flags of a dependency have changed. DependencyFeatures { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Added feature flags added: Vec, /// Removed feature flags removed: Vec, }, /// A dependency of this crate is now optional or is no longer optional. DependencyOptionality { /// Path of the dependency /// /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and /// the name of the dependency. path: String, /// Old optionality old: bool, /// New optionality new: bool, }, // target changes /// A new compilation target was added to the crate. TargetAdded { /// Path of the target /// /// This includes the target "kind" and its name. path: String, /// Value of the target /// /// This includes the target path, edition, kinds (for library targets), required features, /// and flags whether the target contains doctests, tests, or documentation. target: String, }, /// A compilation target was removed from the crate. TargetRemoved { /// Path of the target /// /// This includes the target "kind" and its name. path: String, /// Value of the target /// /// This includes the target path, edition, kinds (for library targets), required features, /// and flags whether the target contains doctests, tests, or documentation. target: String, }, /// A compilation target has changed. TargetChanged { /// Path of the target /// /// This includes the target "kind" and its name. path: String, /// Old value of the target old: String, /// New value of the target new: String, }, // feature changes /// A new feature flag was added to the crate. FeatureAdded { /// Name of the feature name: String, }, /// A feature flag was removed from the crate. FeatureRemoved { /// Name of the feature name: String, }, /// The dependencies of a feature flag have changed. FeatureChanged { /// Name of the feature name: String, /// List of added feature dependencies added: Vec, /// List of removed feature dependencies removed: Vec, }, } #[derive(Serialize)] struct JsonDiffItem { severity: String, kind: &'static str, data: Value, } impl DiffItem { /// Severity associated with this diff item. #[must_use] pub const fn severity(&self) -> Severity { #[expect(clippy::match_same_arms)] match self { Self::FileAdded { .. } => Severity::Info, Self::FileRemoved { .. } => Severity::Info, Self::FileChanged { .. } => Severity::Info, Self::LineEndingsChange { .. } => Severity::Warning, Self::PermissionChange { .. } => Severity::Warning, Self::NameChange { .. } => Severity::Error, Self::VersionChange { .. } => Severity::Info, Self::EditionChange { .. } => Severity::Warning, Self::RustVersionChange { .. } => Severity::Warning, Self::AuthorsChange { .. } => Severity::Warning, Self::DescriptionChange { .. } => Severity::Info, Self::LicenseChange { .. } => Severity::Warning, Self::LicenseFileChange { .. } => Severity::Warning, Self::ReadmeChange { .. } => Severity::Info, Self::CategoriesChange { .. } => Severity::Info, Self::KeywordsChange { .. } => Severity::Info, Self::RepositoryChange { .. } => Severity::Warning, Self::HomepageChange { .. } => Severity::Info, Self::DocumentationChange { .. } => Severity::Info, Self::LinksChange { .. } => Severity::Warning, Self::DefaultRunChange { .. } => Severity::Info, Self::MetadataChange { .. } => Severity::Info, Self::DependencyAdded { .. } => Severity::Info, Self::DependencyRemoved { .. } => Severity::Info, Self::DependencyUpgraded { .. } => Severity::Info, Self::DependencyDowngraded { .. } => Severity::Warning, Self::DependencyFeatures { .. } => Severity::Info, Self::DependencyOptionality { .. } => Severity::Info, Self::TargetAdded { .. } => Severity::Info, Self::TargetRemoved { .. } => Severity::Info, Self::TargetChanged { .. } => Severity::Info, Self::FeatureAdded { .. } => Severity::Info, Self::FeatureRemoved { .. } => Severity::Warning, Self::FeatureChanged { .. } => Severity::Info, } } /// String representation / ID of the diff item kind. #[must_use] // keep in sync with the Python enum pub const fn kind(&self) -> &'static str { match self { Self::FileAdded { .. } => "FileAdded", Self::FileRemoved { .. } => "FileRemoved", Self::FileChanged { .. } => "ContentChange", Self::LineEndingsChange { .. } => "LineEndingsChange", Self::PermissionChange { .. } => "PermissionChange", Self::NameChange { .. } => "NameChange", Self::VersionChange { .. } => "VersionChange", Self::EditionChange { .. } => "EditionChange", Self::RustVersionChange { .. } => "RustVersionChange", Self::AuthorsChange { .. } => "AuthorsChange", Self::DescriptionChange { .. } => "DescriptionChange", Self::LicenseChange { .. } => "LicenseChange", Self::LicenseFileChange { .. } => "LicenseFileChange", Self::ReadmeChange { .. } => "ReadmeChange", Self::CategoriesChange { .. } => "CategoriesChange", Self::KeywordsChange { .. } => "KeywordsChange", Self::RepositoryChange { .. } => "RepositoryChange", Self::HomepageChange { .. } => "HomepageChange", Self::DocumentationChange { .. } => "DocumentationChange", Self::LinksChange { .. } => "LinksChange", Self::DefaultRunChange { .. } => "DefaultRunChange", Self::MetadataChange { .. } => "MetadataChange", Self::DependencyAdded { .. } => "DependencyAdded", Self::DependencyRemoved { .. } => "DependencyRemoved", Self::DependencyUpgraded { .. } => "DependencyUpgraded", Self::DependencyDowngraded { .. } => "DependencyDowngraded", Self::DependencyFeatures { .. } => "DependencyFeatures", Self::DependencyOptionality { .. } => "DependencyOptionality", Self::TargetAdded { .. } => "TargetAdded", Self::TargetRemoved { .. } => "TargetRemoved", Self::TargetChanged { .. } => "TargetChanged", Self::FeatureAdded { .. } => "FeatureAdded", Self::FeatureRemoved { .. } => "FeatureRemoved", Self::FeatureChanged { .. } => "FeatureChanged", } } /// Data associated with this diff item. #[expect(clippy::too_many_lines)] #[must_use] pub fn data(&self) -> Value { let mut data = serde_json::Map::new(); #[expect(clippy::match_same_arms)] match self { Self::FileAdded { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::FileRemoved { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::FileChanged { path, diff } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("diff"), Value::from(diff.clone())); }, Self::LineEndingsChange { path } => { data.insert(String::from("path"), Value::from(Some(path.clone()))); }, Self::PermissionChange { path, old, new } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::NameChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::VersionChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::EditionChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::RustVersionChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::AuthorsChange { added, removed } => { data.insert(String::from("added"), Value::from(added.clone())); data.insert(String::from("removed"), Value::from(removed.clone())); }, Self::DescriptionChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::LicenseChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::LicenseFileChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::ReadmeChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::CategoriesChange { added, removed } => { data.insert(String::from("added"), Value::from(added.clone())); data.insert(String::from("removed"), Value::from(removed.clone())); }, Self::KeywordsChange { added, removed } => { data.insert(String::from("added"), Value::from(added.clone())); data.insert(String::from("removed"), Value::from(removed.clone())); }, Self::RepositoryChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::HomepageChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::DocumentationChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::LinksChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::DefaultRunChange { old, new } => { data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::MetadataChange { old, new } => { data.insert(String::from("old"), old.clone()); data.insert(String::from("new"), new.clone()); }, Self::DependencyAdded { path, value } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("value"), Value::from(value.clone())); }, Self::DependencyRemoved { path, value } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("value"), Value::from(value.clone())); }, Self::DependencyUpgraded { path, old, new } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::DependencyDowngraded { path, old, new } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::DependencyFeatures { path, removed, added } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("removed"), Value::from(removed.clone())); data.insert(String::from("added"), Value::from(added.clone())); }, Self::DependencyOptionality { path, old, new } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("old"), Value::from(old.to_string())); data.insert(String::from("new"), Value::from(new.to_string())); }, Self::TargetAdded { path, target } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("target"), Value::from(target.clone())); }, Self::TargetRemoved { path, target } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("target"), Value::from(target.clone())); }, Self::TargetChanged { path, old, new } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("old"), Value::from(old.clone())); data.insert(String::from("new"), Value::from(new.clone())); }, Self::FeatureAdded { name } => { data.insert(String::from("name"), Value::from(name.clone())); }, Self::FeatureRemoved { name } => { data.insert(String::from("name"), Value::from(name.clone())); }, Self::FeatureChanged { name, removed, added } => { data.insert(String::from("name"), Value::from(name.clone())); data.insert(String::from("added"), Value::from(added.clone())); data.insert(String::from("removed"), Value::from(removed.clone())); }, } Value::Object(data) } /// Human-readable message associated with this diff item. /// /// This message is limited to one line of text. If there is more information, it can be /// retrieved by calling [`DiffItem::extra`]. #[must_use] pub fn message(&self) -> Cow<'static, str> { match self { Self::FileAdded { path } => format!("file added at path '{path}'").into(), Self::FileRemoved { path } => format!("file removed at path '{path}'").into(), Self::FileChanged { path, .. } => format!("file at path '{path}' changed").into(), Self::LineEndingsChange { path } => { format!("file at path '{path}' changed line endings (CRLF / LF)").into() }, Self::PermissionChange { path, old, new } => { format!("file at path '{path}' changed mode from {old} to {new}").into() }, Self::NameChange { old, new } => format!("crate name changed from '{old}' to '{new}'").into(), Self::VersionChange { old, new } => format!("crate version changed from '{old}' to '{new}'").into(), Self::EditionChange { old, new } => format!("crate edition changed from '{old}' to '{new}'").into(), Self::RustVersionChange { old, new } => { let old_msrv = format_option_string(old.as_ref()); let new_msrv = format_option_string(new.as_ref()); format!("crate MSRV changed from '{old_msrv}' to '{new_msrv}'").into() }, Self::AuthorsChange { .. } => Into::into("crate authors changed"), Self::DescriptionChange { .. } => Into::into("crate description changed"), Self::LicenseChange { old, new } => { let old_license = format_option_string(old.as_ref()); let new_license = format_option_string(new.as_ref()); format!("crate license changed from '{old_license}' to '{new_license}'").into() }, Self::LicenseFileChange { old, new } => { let old_path = format_option_string(old.as_ref()); let new_path = format_option_string(new.as_ref()); format!("crate license file changed from '{old_path}' to '{new_path}'").into() }, Self::ReadmeChange { old, new } => { let old_path = format_option_string(old.as_ref()); let new_path = format_option_string(new.as_ref()); format!("crate readme file changed from '{old_path}' to '{new_path}'").into() }, Self::CategoriesChange { .. } => Into::into("crate categories changed"), Self::KeywordsChange { .. } => Into::into("crate keywords changed"), Self::RepositoryChange { old, new } => { let old_url = format_option_string(old.as_ref()); let new_url = format_option_string(new.as_ref()); format!("crate repository URL file changed from '{old_url}' to '{new_url}'").into() }, Self::HomepageChange { old, new } => { let old_url = format_option_string(old.as_ref()); let new_url = format_option_string(new.as_ref()); format!("crate homepage URL file changed from '{old_url}' to '{new_url}'").into() }, Self::DocumentationChange { old, new } => { let old_url = format_option_string(old.as_ref()); let new_url = format_option_string(new.as_ref()); format!("crate documentation URL file changed from '{old_url}' to '{new_url}'").into() }, Self::LinksChange { old, new } => { let old_link = format_option_string(old.as_ref()); let new_link = format_option_string(new.as_ref()); format!("linked library changed from '{old_link}' to '{new_link}'").into() }, Self::DefaultRunChange { old, new } => { let old_target = format_option_string(old.as_ref()); let new_target = format_option_string(new.as_ref()); format!("default 'run' target changed from '{old_target}' to '{new_target}'").into() }, Self::MetadataChange { .. } => Into::into("additional free-form crate metadata changed"), Self::DependencyAdded { path, .. } => format!("dependency '{path}' added").into(), Self::DependencyRemoved { path, .. } => format!("dependency '{path}' removed").into(), Self::DependencyUpgraded { path, old, new } => { format!("dependency '{path}' upgraded from {old} to {new}").into() }, Self::DependencyDowngraded { path, old, new } => { format!("dependency '{path}' downgraded from {old} to {new}").into() }, Self::DependencyFeatures { path, .. } => format!("dependency '{path}' features changed").into(), Self::DependencyOptionality { path, old, new } => { if !*old && *new { format!("dependency '{path}' is now optional").into() } else { // *old && !*new format!("dependency '{path}' is no longer optional").into() } }, Self::TargetAdded { path, .. } => format!("compilation target '{path}' added").into(), Self::TargetRemoved { path, .. } => format!("compilation target '{path}' removed").into(), Self::TargetChanged { path, .. } => format!("compilation target '{path}' changed").into(), Self::FeatureAdded { name } => format!("crate feature added: '{name}'").into(), Self::FeatureRemoved { name } => format!("crate feature removed: '{name}'").into(), Self::FeatureChanged { name, .. } => format!("crate feature '{name}' dependencies changed").into(), } } /// Additional message content from this report item. /// /// This contains more verbose information that might or might not span multiple lines. #[must_use] pub fn extra(&self) -> Option { match self { Self::FileChanged { diff, .. } => diff.as_ref().map(ToOwned::to_owned), Self::AuthorsChange { added, removed } | Self::CategoriesChange { added, removed } | Self::KeywordsChange { added, removed } | Self::DependencyFeatures { added, removed, .. } | Self::FeatureChanged { added, removed, .. } => { let mut out = String::new(); if !added.is_empty() { let _ = writeln!(out, "added: {}", added.join(", ")); } if !removed.is_empty() { let _ = writeln!(out, "removed: {}", removed.join(", ")); } Some(out) }, Self::DescriptionChange { old, new } => { let old_desc = old.as_ref().map_or("", String::as_str); let new_desc = new.as_ref().map_or("", String::as_str); let header = Some(("old/package.description", "new/package.description")); Some(make_diff(old_desc, new_desc, header)) }, Self::MetadataChange { old, new } => { let mut out = String::new(); let _ = writeln!(out, "old: {old}"); let _ = writeln!(out, "new: {new}"); Some(out) }, Self::DependencyAdded { value, .. } | Self::DependencyRemoved { value, .. } => Some(value.clone()), Self::TargetAdded { target, .. } | Self::TargetRemoved { target, .. } => Some(target.clone()), Self::TargetChanged { old, new, .. } => { let mut out = String::new(); let _ = writeln!(out, "old: {old}"); let _ = writeln!(out, "new: {new}"); Some(out) }, _ => None, } } } impl Display for DiffItem { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if f.alternate() { writeln!(f, "{}: {}", self.severity(), self.message())?; if let Some(extra) = self.extra() { for line in extra.lines() { writeln!(f, " {line}")?; } writeln!(f)?; } Ok(()) } else { writeln!(f, "{}: {}", self.severity(), self.message()) } } } ingredients-0.2.2/src/error.rs000064400000000000000000000036611046102023000144340ustar 00000000000000#![allow(missing_docs)] /// Error enum representing different errors that can occur in this crate. #[derive(Debug, thiserror::Error)] pub enum Error { /// Error downloading crate sources from crates.io. #[error(transparent)] Download { #[from] inner: reqwest::Error, }, /// Error reading / writing files to / from disk or /tmp space. #[error(transparent)] IO { #[from] inner: std::io::Error, }, /// Error querying crates.io for the specified crate. #[error("Could not find crate on crates.io: {name}")] CrateNotFound { name: String }, /// Error querying crates.io for the specified crate version. #[error("Could not find crate version on crates.io: {name}@{version}")] VersionNotFound { name: String, version: String }, /// Error parsing the repository URL from crate metadata. #[error("Invalid repository URL: {repo}")] InvalidRepoUrl { repo: String }, /// Error checking out the specified git ref from the repository. #[error("Invalid git ref for repository: {repo}#{rev}")] InvalidGitRef { repo: String, rev: String }, /// Error caused by failing to find a crate inside a repository #[error("Cannot determine crate path in repository: {repo} / {name}")] PathNotDeterminable { repo: String, name: String }, /// Other errors from a subprocess (i.e. git). #[error("Process failed: [{cmd}]\n{stdout}\n{stderr}")] Subprocess { cmd: String, stdout: String, stderr: String, }, /// Error loading crate metadata. #[error("Failed to load crate metadata: {inner}")] Metadata { inner: String }, /// Error walking the source directory of a crate. #[error("Failed to walk source directory: {inner}")] Walk { inner: String }, /// Error parsing the `.cargo_vcs_info.json` file. #[error("Failed to load '.cargo_vcs_info.json' file: {inner}")] VcsInfo { inner: String }, } ingredients-0.2.2/src/format.rs000064400000000000000000000102731046102023000145700ustar 00000000000000use std::borrow::Cow; use std::fmt::Display; use cargo_metadata::{Dependency, DependencyKind as DK, Target, TargetKind as TK}; pub fn make_diff(old: &str, new: &str, header: Option<(&str, &str)>) -> String { let diff = similar::TextDiff::from_lines(old, new); let mut udiff = diff.unified_diff(); if let Some((a, b)) = header { udiff.header(a, b); } udiff.to_string() } pub fn format_list(items: &[D]) -> String { format!( "[{}]", items .iter() .map(|i| format!("\"{i}\"")) .collect::>() .join(", ") ) } pub fn format_option_string(value: Option<&String>) -> &str { value.map_or("(none)", String::as_str) } pub fn path_from_dep(dep: &Dependency) -> String { let mut path_items: Vec> = Vec::new(); if let Some(target) = &dep.target { path_items.push(format!("target.\"{}\"", target.to_string().replace('"', "\'")).into()); } match dep.kind { DK::Normal => path_items.push("dependencies".into()), DK::Development => path_items.push("dev-dependencies".into()), DK::Build => path_items.push("build-dependencies".into()), _ => {}, } path_items.push(dep.rename.clone().unwrap_or(dep.name.clone()).clone().into()); path_items.join(".") } pub fn value_from_dep(dep: &Dependency) -> String { let mut items = Vec::new(); if dep.rename.is_some() { items.push(format!("package = \"{}\"", dep.name)); } items.push(format!("version = \"{}\"", dep.req)); if let Some(path) = &dep.path { items.push(format!("path = \"{path}\"")); } if dep.optional { items.push(String::from("optional = true")); } if !dep.uses_default_features { items.push(String::from("default-features = false")); } if !dep.features.is_empty() { items.push(format!( "features = [{}]", dep.features .iter() .map(|f| format!("\"{f}\"")) .collect::>() .join(", ") )); } format!("{{ {} }}", items.join(", ")) } const LIBRARY_KINDS: &[TK] = &[TK::Lib, TK::RLib, TK::CDyLib, TK::DyLib, TK::StaticLib, TK::ProcMacro]; pub fn path_from_target(target: &Target) -> String { let kind = if LIBRARY_KINDS.iter().any(|t| target.kind.contains(t)) { "lib" } else if target.kind.contains(&TK::Bench) { "benches" } else if target.kind.contains(&TK::Bin) { "bin" } else if target.kind.contains(&TK::CustomBuild) { "build" } else if target.kind.contains(&TK::Example) { "example" } else if target.kind.contains(&TK::Test) { "test" } else { "" }; format!("{}[\"{}\"]", kind, target.name) } pub fn value_from_target(target: &Target) -> String { let mut items = Vec::new(); items.push(format!("path = \"{}\"", target.src_path)); items.push(format!("edition = \"{}\"", target.edition)); if LIBRARY_KINDS.iter().any(|t| target.kind.contains(t)) { items.push(format!( "crate-type = [{}]", target .crate_types .iter() .map(|t| format!("\"{t}\"")) .collect::>() .join(", ") )); } if !target.required_features.is_empty() { items.push(format!( "required-features = [{}]", target .required_features .iter() .map(|f| format!("\"{f}\"")) .collect::>() .join(", ") )); } if !target.doctest { items.push(String::from("doctest = false")); } if !target.test { items.push(String::from("test = false")); } if !target.doc { items.push(String::from("doc = false")); } format!("{{ {} }}", items.join(", ")) } #[cfg(test)] mod tests { use super::*; #[test] fn test_format_list() { assert_eq!(format_list::<&str>(&[]), "[]"); assert_eq!(format_list(&["hello"]), "[\"hello\"]"); assert_eq!(format_list(&["hello", "world"]), "[\"hello\", \"world\"]"); } } ingredients-0.2.2/src/krate.rs000064400000000000000000000165541046102023000144160ustar 00000000000000use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use reqwest::StatusCode; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::Deserialize; use tempfile::{NamedTempFile, TempDir}; use tracing::{debug, trace}; use crate::error::Error; use crate::metadata::{Metadata, get_crate_metadata}; use crate::utils::{self, DirectoryContents, get_contents}; use crate::vcsinfo::{CargoVcsInfo, vcs_info_from_root}; // these files are expected to be different in published crates const FILTERED_FILE_NAMES: [&str; 4] = ["Cargo.toml", "Cargo.toml.orig", "Cargo.lock", ".cargo_vcs_info.json"]; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION")); pub(crate) static CRATES_IO_CLIENT: LazyLock = LazyLock::new(CratesIoClient::init); #[derive(Deserialize)] struct CratesIoVersions { versions: Vec, } #[derive(Deserialize)] struct CratesIoVersion { #[serde(rename = "num")] version: String, // incomplete } pub struct CratesIoClient { client: reqwest::Client, // incomplete } impl CratesIoClient { pub fn init() -> Self { let mut headers = HeaderMap::new(); headers.insert( HeaderName::from_static("user-agent"), HeaderValue::from_static(USER_AGENT), ); debug!("Initializing HTTP client"); // this is not an error that can reasonably be handled #[expect(clippy::expect_used)] let client = reqwest::ClientBuilder::new() .default_headers(headers) .build() .expect("Cannot initialize HTTP client."); CratesIoClient { client } } pub(crate) async fn versions(&self, name: &str) -> Result, Error> { let url = format!("https://crates.io/api/v1/crates/{name}/versions"); debug!("Querying crates.io API for available versions: {name}"); let response = self.client.get(url).send().await?; if response.status() == StatusCode::NOT_FOUND { // 404 NOT FOUND is returned if the name is unknown return Err(Error::CrateNotFound { name: String::from(name), }); } let contents = response.text().await?; // if this fails, something is seriously wrong - just panic #[expect(clippy::expect_used)] let json: CratesIoVersions = serde_json::from_str(&contents).expect("Failed to deserialize JSON from crates.io API response."); Ok(json.versions.into_iter().map(|v| v.version).collect()) } pub(crate) async fn download(&self, name: &str, version: &str) -> Result { let url = format!("https://crates.io/api/v1/crates/{name}/{version}/download"); let versions = self.versions(name).await?; if !versions.iter().any(|v| v == version) { return Err(Error::VersionNotFound { name: String::from(name), version: String::from(version), }); } let mut temp = NamedTempFile::new()?; debug!("Downloading crate from crates.io: {name}@{version}"); let mut response = self.client.get(url).send().await?; response.error_for_status_ref()?; // streaming download to avoid having the entire file contents in memory trace!( "Writing crate download to temporary file: {}", temp.path().to_string_lossy() ); while let Some(bytes) = response.chunk().await? { temp.write_all(&bytes)?; } Ok(temp) } } /// Crate archive #[derive(Debug)] #[non_exhaustive] pub struct Crate { pub(crate) metadata: Metadata, pub(crate) root: PathBuf, pub(crate) vcs_info: Option, _temp: Option, } impl Crate { /// Load crate sources from local ".crate" file. /// /// ```rust,no_run /// use ingredients::Crate; /// /// let _krate = Crate::local("ingredients", "0.1.0", "./ingredients-0.1.0.crate").unwrap(); /// ``` /// /// # Errors /// /// * The specified path does not exist. /// * The file at the specified path is not a crate archive (.tar.gz format). /// * The crate cannot be unpacked in a temporary location. /// * The crate does not contain valid crate metadata. pub fn local>(name: &str, version: &str, path: P) -> Result { let temp = utils::unpack_tar_gz(path)?; let root = temp.path().join(format!("{name}-{version}")); let metadata = get_crate_metadata(root.clone())?; let vcs_info = vcs_info_from_root(&root)?; Ok(Crate { metadata, root, vcs_info, _temp: Some(temp), }) } /// Download crate sources from crates.io. /// /// In `async` contexts: /// /// ```rust,no_run /// use ingredients::Crate; /// # use tokio::runtime::Runtime; /// /// # let rt = Runtime::new().unwrap(); /// # rt.block_on(async { /// let _krate = Crate::download("ingredients", "0.1.0").await.unwrap(); /// # }) /// ``` /// /// Outside of `async` contexts: /// /// ```rust,no_run /// use ingredients::Crate; /// use tokio::runtime::Runtime; /// /// let rt = Runtime::new().unwrap(); /// let _krate = rt /// .block_on(Crate::download("ingredients", "0.1.0")) /// .unwrap(); /// ``` /// /// # Errors /// /// * The network connection to crates.io fails. /// * The specified crate does not exist. /// * The specified version does not exist. /// * The downloaded file is not a crate archive (.tar.gz format). /// * The crate cannot be unpacked in a temporary location. /// * The crate does not contain valid crate metadata. pub async fn download(name: &str, version: &str) -> Result { let dl = CRATES_IO_CLIENT.download(name, version).await?; let temp = utils::unpack_tar_gz(dl)?; let root = temp.path().join(format!("{name}-{version}")); let metadata = get_crate_metadata(root.clone())?; let vcs_info = vcs_info_from_root(&root)?; Ok(Crate { metadata, root, vcs_info, _temp: Some(temp), }) } /// Name of the crate #[must_use] pub fn name(&self) -> &str { &self.metadata.inner.name } /// Version of the crate #[must_use] pub fn version(&self) -> String { self.metadata.inner.version.to_string() } pub(crate) fn cargo_toml_orig(&self) -> PathBuf { self.root.join("Cargo.toml.orig") } pub(crate) fn file_contents(&self) -> Result { let mut contents = get_contents(&self.root, &self.root)?; contents.files.retain(|f| { f.file_name() .is_some_and(|s| !FILTERED_FILE_NAMES.iter().any(|f| *f == s)) }); Ok(contents) } pub(crate) fn read_entry_to_bytes>(&self, path: P) -> io::Result> { trace!("Reading bytes from file: {}", path.as_ref().to_string_lossy()); utils::read_to_bytes(self.root.join(path)) } pub(crate) fn read_entry_to_string>(&self, path: P) -> io::Result { trace!("Reading text from file: {}", path.as_ref().to_string_lossy()); std::fs::read_to_string(self.root.join(path)) } } ingredients-0.2.2/src/lib.rs000064400000000000000000000141511046102023000140450ustar 00000000000000#![doc = include_str!("../README.md")] mod compare; mod diff; mod error; mod format; mod krate; mod metadata; mod report; mod severity; mod upstream; mod utils; mod vcsinfo; mod workarounds; pub use crate::diff::{Diff, DiffItem}; pub use crate::error::Error; pub use crate::krate::Crate; pub use crate::report::{Report, ReportItem}; pub use crate::severity::Severity; use crate::compare::{CrateComparator, RepoComparator}; use crate::upstream::Repository; use crate::workarounds::sanitize_repo_url; // part of the public API #[doc(hidden)] pub use serde_json::Value; /// Additional options for report generation /// /// ```rust /// use ingredients::ReportOptions; /// /// let mut options = ReportOptions::default(); /// options.repository = Some(String::from("https://example.com/foo/bar")); /// ``` #[non_exhaustive] #[derive(Clone, Default)] pub struct ReportOptions { /// Repository URL /// /// This is useful if published crates do not contain a correct repository URL. pub repository: Option, /// Path in VCS /// /// This is useful for crates that were published with old versions of cargo /// that did not include the `"path_in_vcs"` property in `.cargo_vcs_info.json` /// files. pub path_in_vcs: Option, /// VCS ref /// /// This is useful for crates that were publsihed with old versions of cargo /// that did not include a `.cargo_vcs_info.json` file on upload to crates.io. pub vcs_ref: Option, } impl ReportOptions { /// Initialize new options. #[must_use] pub const fn new() -> Self { ReportOptions { repository: None, path_in_vcs: None, vcs_ref: None, } } /// Check if the options are empty / the defaults. #[must_use] pub const fn is_empty(&self) -> bool { self.repository.is_none() && self.path_in_vcs.is_none() && self.vcs_ref.is_none() } } impl Crate { /// Compare crate against the contents of the project's version control system. /// /// ```rust,no_run /// use ingredients::Crate; /// # use tokio::runtime::Runtime; /// /// # let rt = Runtime::new().unwrap(); /// # rt.block_on(async { /// let krate = Crate::download("ingredients", "0.1.0").await.unwrap(); /// let report = krate.report().await.unwrap(); /// println!("{report}"); /// # }) /// ``` /// /// **Warning**: This attempts to perform a `git clone`, i.e. a git fetch and checkout of the /// referenced repository in a temporary directory. /// /// # Errors /// /// * The repository URL is invalid and fails to parse. /// * Directory contents cannot be read from disk. pub async fn report(&self) -> Result { self.report_with_options(ReportOptions::new()).await } /// Compare crate against the contents of the project's version control system (with custom /// options). /// /// **Warning**: This attempts to perform a `git clone`, i.e. a git fetch and checkout of the /// referenced repository in a temporary directory. /// /// # Errors /// /// * The repository URL is invalid and fails to parse. /// * Directory contents cannot be read from disk. pub async fn report_with_options(&self, options: ReportOptions) -> Result { let mut items = Vec::new(); let mut vcsinfo_path_in_vcs = None; let mut vcsinfo_vcs_ref = None; if let Some(vcs_info) = &self.vcs_info { if vcs_info.git.dirty { items.push(ReportItem::DirtyRepository); } if vcs_info.path_in_vcs.is_none() { items.push(ReportItem::NoPathInVcsInfo); } vcsinfo_path_in_vcs = vcs_info.path_in_vcs.as_deref(); vcsinfo_vcs_ref = Some(vcs_info.git.sha1.as_str()); } else { items.push(ReportItem::MissingVcsInfo); } let Some(vcs_ref) = options.vcs_ref.as_deref().or(vcsinfo_vcs_ref) else { return Ok(Report::from_items(items)); }; let path_in_vcs = options.path_in_vcs.as_deref().or(vcsinfo_path_in_vcs); let Some(url) = options.repository.or(self.metadata.inner.repository.clone()) else { items.push(ReportItem::MissingRepositoryUrl); return Ok(Report::from_items(items)); }; let sanitized_url = sanitize_repo_url(&url)?; let upstream = match Repository::clone(&sanitized_url, vcs_ref, path_in_vcs, &self.metadata.inner.name).await { Ok(repo) => repo, Err(err) => match err { Error::InvalidGitRef { repo, rev } => { items.push(ReportItem::InvalidGitRef { repo, rev }); return Ok(Report::from_items(items)); }, Error::InvalidRepoUrl { repo } => { items.push(ReportItem::InvalidRepoUrl { repo }); return Ok(Report::from_items(items)); }, Error::CrateNotFound { name } => { items.push(ReportItem::NotFoundInRepo { repo: url, name }); return Ok(Report::from_items(items)); }, _ => return Err(err), }, }; let comparator = RepoComparator::new(self, &upstream); items.extend(comparator.compare()?); Ok(Report::from_items(items)) } /// Compare crate against a different version of the same crate. /// /// ```rust,no_run /// use ingredients::Crate; /// # use tokio::runtime::Runtime; /// /// # let rt = Runtime::new().unwrap(); /// # rt.block_on(async { /// let krate1 = Crate::download("syn", "2.0.110").await.unwrap(); /// let krate2 = Crate::download("syn", "2.0.111").await.unwrap(); /// let diff = krate1.diff(&krate2).unwrap(); /// println!("{diff}"); /// # }) /// ``` /// /// # Errors /// /// * Directory contents cannot be read from disk. pub fn diff(&self, other: &Crate) -> Result { let comparator = CrateComparator::new(self, other); Ok(Diff::from_items(comparator.compare()?)) } } ingredients-0.2.2/src/main.rs000064400000000000000000000233461046102023000142310ustar 00000000000000//! ingredients CLI use std::io::Write; use std::path::PathBuf; use clap::{CommandFactory, Parser, ValueHint}; use clap_complete::aot::Shell; use owo_colors::OwoColorize; use ingredients::{Crate, Diff, Report, ReportOptions, Severity}; fn pretty_format_severity(severity: Severity) -> String { match severity { x @ Severity::Fatal => x.to_string().bold().on_bright_cyan().to_string(), x @ Severity::Error => x.to_string().red().bold().to_string(), x @ Severity::Warning => x.to_string().yellow().bold().to_string(), x @ Severity::Debug => x.to_string().green().bold().to_string(), x @ Severity::Info | x => x.bold().to_string(), } } const fn plural_suffix(len: usize) -> &'static str { if len == 1 { "" } else { "s" } } fn pretty_print_report(report: &Report, minimum: Severity, verbose: bool) { let mut out = anstream::stdout(); for item in report.items() { if item.severity() < minimum { continue; } let severity = pretty_format_severity(item.severity()); if verbose { let _ = writeln!(out, "{}: {}", severity, item.message()); if let Some(extra) = item.extra() { for line in extra.lines() { let _ = writeln!(out, " {line}"); } let _ = writeln!(out); } } else { let _ = writeln!(out, "{}: {}", severity, item.message()); } } let fatal = report .items() .iter() .filter(|item| item.severity() == Severity::Fatal) .collect::>() .len(); let error = report .items() .iter() .filter(|item| item.severity() == Severity::Error) .collect::>() .len(); let warning = report .items() .iter() .filter(|item| item.severity() == Severity::Warning) .collect::>() .len(); let _ = writeln!( out, "{}: {} fatal issue{}, {} error{}, {} warning{}", "summary".bold(), fatal, plural_suffix(fatal), error, plural_suffix(error), warning, plural_suffix(warning), ); } fn pretty_print_diff(diff: &Diff, minimum: Severity, verbose: bool) { let mut out = anstream::stdout(); for item in diff.items() { if item.severity() < minimum { continue; } let severity = pretty_format_severity(item.severity()); if verbose { let _ = writeln!(out, "{}: {}", severity, item.message()); if let Some(extra) = item.extra() { for line in extra.lines() { let _ = writeln!(out, " {line}"); } let _ = writeln!(out); } } else { let _ = writeln!(out, "{}: {}", severity, item.message()); } } let fatal = diff .items() .iter() .filter(|item| item.severity() == Severity::Fatal) .collect::>() .len(); let error = diff .items() .iter() .filter(|item| item.severity() == Severity::Error) .collect::>() .len(); let warning = diff .items() .iter() .filter(|item| item.severity() == Severity::Warning) .collect::>() .len(); let _ = writeln!( out, "{}: {} fatal issue{}, {} error{}, {} warning{}", "summary".bold(), fatal, plural_suffix(fatal), error, plural_suffix(error), warning, plural_suffix(warning), ); } #[derive(Clone, Copy, Debug, Default, clap::ValueEnum)] enum Format { #[default] Plain, Verbose, Json, } /// Check ingredients of published Rust crates #[derive(Debug, clap::Parser)] #[command(disable_help_subcommand(true))] struct Args { #[command(subcommand)] command: Command, } /// Compare a crate against the contents of the upstream version control system /// /// By default, the crate archive is downloaded from crates.io. To override this /// behaviour, pass `--file ` to use a locally downloaded file instead. /// /// For crates that don't set `package.repository` in their metadata, a URL can /// be provided with the `--with-repository ` argument. /// /// For crates that have been published with old versions of cargo that did not /// yet include complete `.cargo_vcs_info.json` files, the "path in VCS" (the /// relative path where the crate can be found in the repository) can be /// provided with the `--with-path-in-vcs ` argument, and the VCS ref can /// be provided with the `--with-vcs-ref ` argument. #[derive(Debug, clap::Args)] struct ReportArgs { /// Name of the crate name: String, /// Version of the crate version: String, /// Output format #[clap(long, default_value = "plain")] format: Format, /// Minimum severity #[clap(long, default_value = "info")] severity: Severity, /// Path to local crate archive to use instead of downloading from crates.io #[clap(long, short = 'f', value_hint = ValueHint::FilePath)] file: Option, /// Provide repository URL if package.repository is missing or wrong #[clap(long, short = 'u')] with_repository: Option, /// Provide "path in VCS" if `.cargo_vcs_info.json` is missing or incomplete #[clap(long, short = 'p')] with_path_in_vcs: Option, /// Provide VCS ref if `.cargo_vcs_info.json` is missing or incomplete #[clap(long, short = 'r')] with_vcs_ref: Option, } /// Compare two versions of a crate /// /// By default, the crate archives are downloaded from crates.io. To override /// this behaviour, pass `--old_file ` and `--new-file ` to use /// locally downloaded files instead. #[derive(Debug, clap::Args)] struct DiffArgs { /// Name of the crate name: String, /// Old version of the crate old_version: String, /// New version of the crate new_version: String, /// Output format #[clap(long, default_value = "plain")] format: Format, /// Minimum severity #[clap(long, default_value = "info")] severity: Severity, /// Path to local crate archive for old crate version #[clap(long, value_hint = ValueHint::FilePath)] old_file: Option, /// Path to local crate archive for new crate version #[clap(long, value_hint = ValueHint::FilePath)] new_file: Option, } #[derive(Clone, Copy, Debug, clap::ValueEnum)] enum ShellKind { Bash, Fish, Zsh, Nushell, } #[derive(Debug, clap::Args)] struct CompletionsArgs { shell: ShellKind, } #[derive(Debug, clap::Subcommand)] enum Command { Report(ReportArgs), Diff(DiffArgs), #[clap(hide = true)] Completions(CompletionsArgs), } #[expect(clippy::print_stdout)] async fn report(args: ReportArgs) -> anyhow::Result<()> { let mut options = ReportOptions::default(); options.repository = args.with_repository; options.path_in_vcs = args.with_path_in_vcs; options.vcs_ref = args.with_vcs_ref; let krate = if let Some(path) = args.file { Crate::local(&args.name, &args.version, &path)? } else { Crate::download(&args.name, &args.version).await? }; let report = if options.is_empty() { krate.report().await? } else { krate.report_with_options(options).await? }; match args.format { Format::Plain => pretty_print_report(&report, args.severity, false), Format::Verbose => pretty_print_report(&report, args.severity, true), Format::Json => println!("{}", report.to_json()), } Ok(()) } #[expect(clippy::print_stdout)] async fn diff(args: DiffArgs) -> anyhow::Result<()> { let old_crate = if let Some(old_file) = args.old_file { Crate::local(&args.name, &args.old_version, &old_file)? } else { Crate::download(&args.name, &args.old_version).await? }; let new_crate = if let Some(new_file) = args.new_file { Crate::local(&args.name, &args.new_version, &new_file)? } else { Crate::download(&args.name, &args.new_version).await? }; let diff = old_crate.diff(&new_crate)?; match args.format { Format::Plain => pretty_print_diff(&diff, args.severity, false), Format::Verbose => pretty_print_diff(&diff, args.severity, true), Format::Json => println!("{}", diff.to_json()), } Ok(()) } fn completions(args: &CompletionsArgs) { match args.shell { ShellKind::Bash => { clap_complete::generate( Shell::Bash, &mut Args::command(), env!("CARGO_PKG_NAME"), &mut std::io::stdout(), ); }, ShellKind::Fish => { clap_complete::generate( Shell::Fish, &mut Args::command(), env!("CARGO_PKG_NAME"), &mut std::io::stdout(), ); }, ShellKind::Zsh => { clap_complete::generate( Shell::Zsh, &mut Args::command(), env!("CARGO_PKG_NAME"), &mut std::io::stdout(), ); }, ShellKind::Nushell => { clap_complete::generate( clap_complete_nushell::Nushell, &mut Args::command(), env!("CARGO_PKG_NAME"), &mut std::io::stdout(), ); }, } } #[tokio::main] async fn main() -> anyhow::Result<()> { env_logger::init_from_env("INGREDIENTS_LOG"); let args = Args::parse(); match args.command { Command::Report(args) => report(args).await?, Command::Diff(args) => diff(args).await?, Command::Completions(args) => completions(&args), } Ok(()) } ingredients-0.2.2/src/metadata.rs000064400000000000000000000055371046102023000150670ustar 00000000000000use std::path::Path; use cargo_metadata::camino::Utf8PathBuf; use tracing::debug; use crate::error::Error; fn utf8_path_replace(path: &Utf8PathBuf, left: &str, right: &str) -> Utf8PathBuf { Utf8PathBuf::from(path.to_string().replace(left, right)) } #[derive(Clone, Debug, PartialEq)] pub struct Metadata { pub inner: cargo_metadata::Package, } impl Metadata { pub fn from_cargo_metadata>( mut md: cargo_metadata::Package, root: P, path_in_vcs: Option<&str>, ) -> Self { let root_str = { let mut temp = root.as_ref().to_path_buf(); if let Some(path_in_vcs) = path_in_vcs { temp.push(path_in_vcs); } temp.push(""); temp.to_string_lossy().to_string() }; // manifest_path: replace absolute path md.manifest_path = utf8_path_replace(&md.manifest_path, &root_str, ""); for target in &mut md.targets { // src_path: replace absolute path target.src_path = utf8_path_replace(&target.src_path, &root_str, ""); } Metadata { inner: md } } } pub(crate) fn load_metadata_from_path>(path: P) -> Result { debug!( "Loading cargo metadata from directory: {}", path.as_ref().to_string_lossy() ); cargo_metadata::MetadataCommand::new() .manifest_path(path.as_ref()) .no_deps() .other_options(vec![String::from("--offline")]) .exec() .map_err(|err| Error::Metadata { inner: err.to_string() }) } pub fn get_crate_metadata>(root: P) -> Result { let path = root.as_ref().join("Cargo.toml"); let md = load_metadata_from_path(path)?; let [package]: [cargo_metadata::Package; 1] = md.packages .into_iter() .collect::>() .try_into() .map_err(|_| Error::Metadata { inner: String::from("Failed to load crate metadata (too many workspace members)."), })?; Ok(Metadata::from_cargo_metadata(package, root, None)) } pub fn get_crate_metadata_from_workspace>( root: P, path_in_vcs: &str, name: &str, ) -> Result { let path = root.as_ref().join(path_in_vcs).join("Cargo.toml"); let md = load_metadata_from_path(path)?; let [package]: [cargo_metadata::Package; 1] = md .packages .into_iter() .filter(|p| p.name.as_ref() == name) .collect::>() .try_into() .map_err(|_| Error::Metadata { inner: format!( "Failed to load crate metadata (name does not match any single workspace member): {}", name.to_owned() ), })?; Ok(Metadata::from_cargo_metadata(package, root, Some(path_in_vcs))) } ingredients-0.2.2/src/report.rs000064400000000000000000000325551046102023000146220ustar 00000000000000use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use serde::Serialize; use serde_json::Value; use crate::severity::Severity; /// List of differences between a published crate and the associated state of /// the upstream version control system #[derive(Debug, Default)] pub struct Report { items: Vec, } impl Report { pub(crate) const fn from_items(items: Vec) -> Self { Report { items } } /// List of differences #[must_use] pub fn items(&self) -> &[ReportItem] { &self.items } /// Get list of differences in machine-readable JSON format /// /// # Panics /// /// This function panics if there are internal errors related to serializing /// data in JSON format. #[must_use] pub fn to_json(&self) -> String { let items: Vec = self .items .iter() .map(|i| JsonReportItem { severity: i.severity().to_string(), kind: i.kind(), data: i.data(), }) .collect(); // if this fails, something is seriously wrong - just panic #[expect(clippy::expect_used)] serde_json::to_string_pretty(&items).expect("Failed to serialize report as JSON.") } } impl Display for Report { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for item in self.items() { if f.alternate() { write!(f, "{item:#}")?; } else { write!(f, "{item}")?; } } Ok(()) } } /// A specific difference between a published crate and the associated state of the upstream version /// control system #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] // keep in sync with the Python enum pub enum ReportItem { /// The crate metadata does not contain a repository URL. MissingRepositoryUrl, /// The crate metadata specifies an invalid repository URL. InvalidRepoUrl { /// Repository URL repo: String, }, /// The git ref from `.cargo_vcs_info.json` could not be checked out. InvalidGitRef { /// Repository URL repo: String, /// Repository ref rev: String, }, /// The `.cargo_vcs_info.json` file is missing from the published crate. MissingVcsInfo, /// The `"path_in_vcs"` property is missing from `.cargo_vcs_info.json`. NoPathInVcsInfo, /// The `"path_in_vcs"` property is missing from `.cargo_vcs_info.json` and the crate cannot be /// found inside the repository. NotFoundInRepo { /// Repository URL repo: String, /// Crate name name: String, }, /// The crate was published from a "dirty" repository according to `.cargo_vcs_info.json`. DirtyRepository, /// Crate metadata does not match metadata from VCS contents. MetadataMismatch { /// Field in Cargo.toml that does not match field: Cow<'static, str>, /// Value in the published crate krate: Option, /// Value from VCS contents urepo: Option, }, /// The crate contains a broken symbolic link. BrokenSymlinkInCrate { /// Path of the symbolic link path: String, }, /// The repository contains a broken symbolic link. BrokenSymlinkInRepo { /// Path of the symbolic link path: String, }, /// The crate contains a symbolic link that points outside the source directory. InvalidSymlinkInCrate { /// Path of the symbolic link path: String, }, /// The repository contains a symbolic link that points outside the source directory. InvalidSymlinkInRepo { /// Path of the symbolic link path: String, }, /// The crate contains a file that is not present in the VCS. MissingFile { /// Path of the file path: String, }, /// The crate contains a file that does not match file contents from the VCS. ContentMismatch { /// Path of the file path: String, /// Diff between the file in the published crate and the file in the VCS /// /// If this is `None` then the file is a binary file and / or not valid UTF-8. diff: Option, }, /// The crate contains a file that has different line endings than the file in the VCS. LineEndings { /// Path of the file path: String, }, /// The crate contains a file that has different mode / permissions than the file in the VCS. Permissions { /// Path of the file path: String, /// Mode of the file in the published crate krate: String, /// Mode of the file in the VCS urepo: String, }, } impl ReportItem { pub(crate) fn metadata_mismatch>>( field: F, krate: Option, urepo: Option, ) -> Self { ReportItem::MetadataMismatch { field: field.into(), krate, urepo, } } } #[derive(Serialize)] struct JsonReportItem { severity: String, kind: &'static str, data: Value, } impl ReportItem { /// Severity associated with this report item. #[must_use] pub const fn severity(&self) -> Severity { #[expect(clippy::match_same_arms)] match self { Self::MissingRepositoryUrl => Severity::Fatal, Self::InvalidRepoUrl { .. } => Severity::Fatal, Self::InvalidGitRef { .. } => Severity::Fatal, Self::MissingVcsInfo => Severity::Fatal, Self::NoPathInVcsInfo => Severity::Warning, Self::NotFoundInRepo { .. } => Severity::Fatal, Self::DirtyRepository => Severity::Warning, Self::MetadataMismatch { .. } => Severity::Error, Self::BrokenSymlinkInCrate { .. } => Severity::Warning, Self::BrokenSymlinkInRepo { .. } => Severity::Warning, Self::InvalidSymlinkInCrate { .. } => Severity::Error, Self::InvalidSymlinkInRepo { .. } => Severity::Error, Self::MissingFile { .. } => Severity::Error, Self::ContentMismatch { .. } => Severity::Error, Self::LineEndings { .. } => Severity::Warning, Self::Permissions { .. } => Severity::Warning, } } /// String representation / ID of the report item kind. #[must_use] // keep in sync with the Python enum pub const fn kind(&self) -> &'static str { match self { Self::MissingRepositoryUrl => "MissingRepositoryUrl", Self::InvalidRepoUrl { .. } => "InvalidRepoUrl", Self::InvalidGitRef { .. } => "InvalidGitRef", Self::MissingVcsInfo => "MissingVcsInfo", Self::NoPathInVcsInfo => "NoPathInVcsInfo", Self::NotFoundInRepo { .. } => "NotFoundInRepository", Self::DirtyRepository => "DirtyRepository", Self::MetadataMismatch { .. } => "MetadataMismatch", Self::BrokenSymlinkInCrate { .. } => "BrokenSymlinkInCrate", Self::BrokenSymlinkInRepo { .. } => "BrokenSymlinkInRepo", Self::InvalidSymlinkInCrate { .. } => "InvalidSymlinkInCrate", Self::InvalidSymlinkInRepo { .. } => "InvalidSymlinkInRepo", Self::MissingFile { .. } => "MissingFile", Self::ContentMismatch { .. } => "ContentMismatch", Self::LineEndings { .. } => "LineEndings", Self::Permissions { .. } => "Permissions", } } /// Data associated with this report item. #[must_use] pub fn data(&self) -> Value { let mut data = serde_json::Map::new(); #[expect(clippy::match_same_arms)] match self { Self::MissingRepositoryUrl => {}, Self::InvalidRepoUrl { repo } => { data.insert(String::from("url"), Value::from(repo.clone())); }, Self::InvalidGitRef { repo, rev } => { data.insert(String::from("url"), Value::from(repo.clone())); data.insert(String::from("ref"), Value::from(rev.clone())); }, Self::MissingVcsInfo => {}, Self::NoPathInVcsInfo => {}, Self::NotFoundInRepo { repo, name } => { data.insert(String::from("url"), Value::from(repo.clone())); data.insert(String::from("name"), Value::from(name.clone())); }, Self::DirtyRepository => {}, Self::MetadataMismatch { field, krate, urepo } => { data.insert(String::from("field"), Value::from(String::from(field.as_ref()))); data.insert(String::from("crate"), Value::from(krate.clone())); data.insert(String::from("urepo"), Value::from(urepo.clone())); }, Self::BrokenSymlinkInCrate { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::BrokenSymlinkInRepo { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::InvalidSymlinkInCrate { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::InvalidSymlinkInRepo { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::MissingFile { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::ContentMismatch { path, diff } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("diff"), Value::from(diff.clone())); }, Self::LineEndings { path } => { data.insert(String::from("path"), Value::from(path.clone())); }, Self::Permissions { path, krate, urepo } => { data.insert(String::from("path"), Value::from(path.clone())); data.insert(String::from("mode-in-crate"), Value::from(krate.clone())); data.insert(String::from("mode-in-repo"), Value::from(urepo.clone())); }, } Value::Object(data) } /// Human-readable message associated with this report item. #[must_use] pub fn message(&self) -> Cow<'static, str> { match self { Self::MissingRepositoryUrl => Into::into("missing repository URL in crate metadata"), Self::InvalidRepoUrl { repo } => format!("invalid repository URL: '{repo}'").into(), Self::InvalidGitRef { repo, rev } => format!("invalid git ref '{rev}' for repository at '{repo}'").into(), Self::MissingVcsInfo => Into::into("missing '.cargo_vcs_info.json' in published crate"), Self::NoPathInVcsInfo => Into::into("no path specified in '.cargo_vcs_info.json'"), Self::NotFoundInRepo { repo, name } => { format!("crate '{name}' cannot be found in repository at '{repo}'").into() }, Self::DirtyRepository => Into::into("crate was published from a \"dirty\" repository"), Self::MetadataMismatch { field, krate, urepo } => { let kmd = krate.as_ref().map_or("(none)", String::as_str); let umd = urepo.as_ref().map_or("(none)", String::as_str); format!("metadata mismatch: '{field}' differs between crate ({kmd}) and repository ({umd})").into() }, Self::BrokenSymlinkInCrate { path } => format!("broken symbolic link in crate at path '{path}'").into(), Self::BrokenSymlinkInRepo { path } => format!("broken symbolic link in repository at path '{path}'").into(), Self::InvalidSymlinkInCrate { path } => format!("invalid symbolic link in crate at path '{path}'").into(), Self::InvalidSymlinkInRepo { path } => { format!("invalid symbolic link in repository at path '{path}'").into() }, Self::MissingFile { path } => { format!("file present in crate missing from repository at path '{path}'").into() }, Self::ContentMismatch { path, .. } => { format!("contents of file at path '{path}' differ between crate and repository").into() }, Self::LineEndings { path } => { format!("contents of file at path '{path}' use different line endings (CRLF / LF)").into() }, Self::Permissions { path, krate, urepo } => { format!("file at path '{path}' has different modes in crate ({krate}) and repository ({urepo})").into() }, } } /// Additional message content from this report item. #[must_use] pub fn extra(&self) -> Option { if let Self::ContentMismatch { diff, .. } = self && let Some(diff) = diff { Some(diff.clone()) } else { None } } } impl Display for ReportItem { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if f.alternate() { writeln!(f, "{}: {}", self.severity(), self.message())?; if let Some(extra) = self.extra() { for line in extra.lines() { writeln!(f, " {line}")?; } writeln!(f)?; } Ok(()) } else { writeln!(f, "{}: {}", self.severity(), self.message()) } } } ingredients-0.2.2/src/severity.rs000064400000000000000000000017761046102023000151620ustar 00000000000000use std::fmt::Display; /// Severity associated with report and diff items #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(feature = "cli", derive(clap::ValueEnum))] #[non_exhaustive] // keep in sync with the Python enum pub enum Severity { /// Fatal problems that prevent further processing Fatal = 4, /// Probable errors that require manual investigation Error = 3, /// Potential issues that might or might not be harmless Warning = 2, /// Additional information provided for some situations Info = 1, /// Information only useful for debugging purposes Debug = 0, } impl Display for Severity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Severity::Fatal => write!(f, "fatal"), Severity::Error => write!(f, "error"), Severity::Warning => write!(f, "warning"), Severity::Info => write!(f, "info"), Severity::Debug => write!(f, "debug"), } } } ingredients-0.2.2/src/upstream.rs000064400000000000000000000102021046102023000151300ustar 00000000000000use std::io; use std::path::{Path, PathBuf}; use tempfile::TempDir; use tokio::process::Command; use tracing::{debug, trace}; use crate::error::Error; use crate::metadata::{Metadata, get_crate_metadata_from_workspace}; use crate::utils::{self, DirectoryContents, get_contents}; use crate::workarounds::find_in_vcs; pub(crate) async fn git_clone(url: &str, sha1: &str) -> Result { let out = TempDir::new()?; let path = out.path().to_string_lossy(); let cmd = ["clone", url, path.as_ref(), "--revision", sha1, "--depth", "1"]; debug!("Cloning git repository: {}", url); trace!("Cloning git repository into: {}", path); let result = Command::new("git") .args(cmd.iter()) .env("GIT_TERMINAL_PROMPT", "0") .output() .await?; if !result.status.success() { let stdout = String::from_utf8_lossy(&result.stdout).to_string(); let stderr = String::from_utf8_lossy(&result.stderr).to_string(); if stderr.contains("terminal prompts disabled") { // specified URL points to host that supports git, // but the specified repository does not exist? return Err(Error::InvalidRepoUrl { repo: String::from(url), }); } if stderr.contains("dumb http transport") { // specified URL not actually pointing at a git repository? return Err(Error::InvalidRepoUrl { repo: String::from(url), }); } if stderr.contains(&format!("not our ref {sha1}")) { // specified git ref not found in the remote? return Err(Error::InvalidGitRef { repo: String::from(url), rev: String::from(sha1), }); } // other error return Err(Error::Subprocess { cmd: cmd.join(" "), stdout, stderr, }); } Ok(out) } #[derive(Debug)] #[non_exhaustive] pub struct Repository<'a> { pub metadata: Metadata, pub root: PathBuf, pub id: &'a str, pub path_in_vcs: String, _temp: Option, } impl<'a> Repository<'a> { pub(crate) async fn clone(url: &str, id: &'a str, path_in_vcs: Option<&str>, name: &str) -> Result { let temp = git_clone(url, id).await?; let (path_in_vcs, metadata) = if let Some(path_in_vcs) = path_in_vcs { // path_in_vcs determined from '.cargo_vcs_info.json' let metadata = get_crate_metadata_from_workspace(temp.as_ref(), path_in_vcs, name)?; (path_in_vcs.to_string(), metadata) } else { debug!("Using heuristics to find crate in repository"); let Some((detected_path_in_vcs, metadata)) = find_in_vcs(&temp, name) else { return Err(Error::PathNotDeterminable { repo: url.to_string(), name: name.to_string(), }); }; debug!("Crate found at: '{detected_path_in_vcs}'"); (detected_path_in_vcs, metadata) }; Ok(Repository { metadata, root: temp.path().to_owned(), id, path_in_vcs, _temp: Some(temp), }) } pub(crate) fn cargo_toml(&self) -> PathBuf { self.root.join(&self.path_in_vcs).join("Cargo.toml") } pub(crate) fn file_contents(&self) -> Result { let mut contents = get_contents(&self.root.join(&self.path_in_vcs), &self.root)?; contents .files .retain(|f| f.file_name().is_some_and(|s| s != "Cargo.toml")); Ok(contents) } pub(crate) fn read_entry_to_bytes>(&self, path: P) -> io::Result> { trace!("Reading bytes from file: {}", path.as_ref().to_string_lossy()); utils::read_to_bytes(self.root.join(&self.path_in_vcs).join(path)) } pub(crate) fn read_entry_to_string>(&self, path: P) -> io::Result { trace!("Reading text from file: {}", path.as_ref().to_string_lossy()); std::fs::read_to_string(self.root.join(&self.path_in_vcs).join(path)) } } ingredients-0.2.2/src/utils.rs000064400000000000000000000074271046102023000144470ustar 00000000000000use std::fs::{self, File}; use std::io::{self, Read}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use flate2::read::GzDecoder; use tempfile::TempDir; use tracing::trace; use crate::error::Error; pub(crate) fn read_to_bytes>(path: P) -> io::Result> { let mut buf = Vec::new(); let mut file = File::open(path)?; file.read_to_end(&mut buf)?; Ok(buf) } pub(crate) fn file_mode>(path: P) -> io::Result { trace!("Determining file mode: {}", path.as_ref().to_string_lossy()); let mode = File::open(path)?.metadata()?.permissions().mode() & 0o777; Ok(format!("{mode:03o}")) } pub(crate) fn unpack_tar_gz>(path: P) -> io::Result { trace!("Unpacking tar/gz file: {}", path.as_ref().to_string_lossy()); let file = File::open(path)?; let ungz = GzDecoder::new(file); let mut archive = tar::Archive::new(ungz); let out = TempDir::new()?; trace!("Unpacking tar/gz file into: {}", out.path().to_string_lossy()); archive.unpack(&out)?; Ok(out) } #[derive(Debug, Default)] pub(crate) struct DirectoryContents { // normal files with valid paths pub files: Vec, // pointers to outside the crate pub outside_base: Vec, // otherwise broken symlinks pub broken_links: Vec, } pub(crate) fn get_contents>(path: P, root: P) -> Result { trace!( "Listing contents of directory tree: {}", path.as_ref().to_string_lossy() ); let wd = walkdir::WalkDir::new(&path).follow_links(true).sort_by_file_name(); let mut dc = DirectoryContents::default(); for result in wd { match result { Ok(entry) => { if entry.path_is_symlink() { match fs::canonicalize(entry.path()) { Ok(real_path) => { if real_path.is_dir() { continue; } if real_path.strip_prefix(&root).is_ok() { dc.files.push( entry .path() .strip_prefix(&path) .map_err(|err| Error::Walk { inner: err.to_string() })? .to_owned(), ); } else { // symlinks that point outside the base directory are not considered valid dc.outside_base.push(entry.path().read_link()?); } }, Err(err) => return Err(err.into()), } } else { if entry.file_type().is_dir() { continue; } // there should be no files outside the base directory let file = entry .path() .strip_prefix(&path) .map_err(|err| Error::Walk { inner: err.to_string() })?; dc.files.push(file.to_owned()); } }, Err(err) => { if let Some(file_path) = err.path() { if let Ok(rel_path) = file_path.strip_prefix(&root) { dc.broken_links.push(rel_path.to_owned()); } else { dc.outside_base.push(file_path.to_owned()); } } else { return Err(Error::Walk { inner: err.to_string() }); } }, } } Ok(dc) } ingredients-0.2.2/src/vcsinfo.rs000064400000000000000000000026711046102023000147520ustar 00000000000000use std::fs::File; use std::path::Path; use serde::Deserialize; use tracing::debug; use crate::error::Error; // TODO: support more VCS than just "git"? #[derive(Debug, Deserialize)] #[non_exhaustive] pub(crate) struct CargoVcsInfo { pub git: GitInfo, pub path_in_vcs: Option, } #[derive(Debug, Deserialize)] #[non_exhaustive] pub(crate) struct GitInfo { pub sha1: String, #[serde(default)] pub dirty: bool, } pub(crate) fn vcs_info_from_root>(root: P) -> Result, Error> { let path = root.as_ref().join(".cargo_vcs_info.json"); debug!("Loading '.cargo_vcs_info.json' from path: {}", path.to_string_lossy()); let file = match File::open(path) { Ok(file) => file, Err(err) => { if err.kind() == std::io::ErrorKind::NotFound { return Ok(None); } return Err(Error::VcsInfo { inner: format!("failed to open file ({err})"), }); }, }; let info: CargoVcsInfo = serde_json::from_reader(file).map_err(|err| Error::VcsInfo { inner: format!("failed to deserialize JSON ({err})"), })?; Ok(Some(info)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_deserialize_vcs_info() { let contents = std::fs::read_to_string("tests/cargo_vcs_info.json").unwrap(); let _data: CargoVcsInfo = serde_json::from_str(&contents).unwrap(); } } ingredients-0.2.2/src/workarounds.rs000064400000000000000000000047271046102023000156650ustar 00000000000000use std::borrow::Cow; use std::path::Path; use tracing::debug; use url::Url; use crate::error::Error; use crate::metadata::{Metadata, load_metadata_from_path}; pub(crate) fn find_in_vcs>(root: P, name: &str) -> Option<(String, Metadata)> { let path = root.as_ref().join("Cargo.toml"); let md = load_metadata_from_path(&path).ok()?; let [package]: [cargo_metadata::Package; 1] = md .packages .into_iter() .filter(|p| p.name.as_ref() == name) .collect::>() .try_into() .inspect_err(|_| { debug!("Failed to load crate metadata (name does not match any single workspace member): {name}"); }) .ok()?; let Some(cpath_in_vcs) = package.manifest_path.parent() else { debug!( "Failed to load crate metadata (manifest path invalid): {}", package.manifest_path ); return None; }; let Ok(path_in_vcs) = cpath_in_vcs.as_std_path().strip_prefix(&root) else { debug!( "Failed to load crate metadata (manifest path invalid): {}", package.manifest_path ); return None; }; let path = path_in_vcs.to_string_lossy().to_string(); Some((path.clone(), Metadata::from_cargo_metadata(package, root, Some(&path)))) } pub(crate) fn sanitize_repo_url(url: &str) -> Result, Error> { let mut parsed = Url::parse(url).map_err(|_| Error::InvalidRepoUrl { repo: String::from(url), })?; if parsed.host_str() == Some("github.com") { let Some(segments) = parsed.path_segments() else { return Err(Error::InvalidRepoUrl { repo: String::from(url), }); }; let segments: Vec<_> = segments.collect(); match segments.as_slice() { [_org, _repo] => {}, // OK [] | [_] => { // https://github.com | https://github.com/user return Err(Error::InvalidRepoUrl { repo: String::from(url), }); }, [org, repo, ..] => { // https://github.com/user/project/tree/main/crate/ // FIXME: report this in a better way than a debug log? debug!("Attempting to sanitize GitHub repository URL: {url}"); parsed.set_path(&format!("{org}/{repo}")); }, } return Ok(Cow::Owned(parsed.to_string())); } Ok(Cow::Borrowed(url)) } ingredients-0.2.2/tests/cargo_vcs_info.json000064400000000000000000000001621046102023000171550ustar 00000000000000{ "git": { "sha1": "7e3bbb2932eafd7cc6ab4b142b9f82b8ea5bb06d" }, "path_in_vcs": "crates/newtype-uuid" }