diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 339778536..790d0943d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: name: Lint runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: "clippy, rustfmt" @@ -35,17 +35,10 @@ jobs: name: Test strategy: matrix: - os: [ubuntu-22.04] + os: [ubuntu-22.04, macos-14] runs-on: ${{ matrix.os }} steps: - # We need to disable conversion to CRLF line endings on windows because it's - # dumb and causes advisory tests to fails since the submodule is checked - # out with the broken line endings - - if: matrix.os == 'windows-2022' - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - uses: dtolnay/rust-toolchain@stable @@ -55,22 +48,6 @@ jobs: run: cargo build --tests - run: cargo test - # Verifies we can build aarch64-apple-darwin binaries until GHA actually has - # runners for them that we can actually run tests on - build-aarch64-apple-darwin: - name: Build aarch64-apple-darwin - runs-on: macos-11 - # Only run this PRs - if: false #github.ref != 'refs/heads/main' - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin - - uses: Swatinem/rust-cache@v2 - - run: cargo fetch --target aarch64-apple-darwin - - run: cargo build --release --target aarch64-apple-darwin - self: name: Check Users if: false # disabled for now @@ -83,7 +60,7 @@ jobs: env: TARGET: x86_64-unknown-linux-musl steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} @@ -111,7 +88,7 @@ jobs: name: Build the book runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - env: version: "0.4.32" run: | @@ -128,7 +105,7 @@ jobs: name: Publish Check runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo fetch @@ -160,7 +137,7 @@ jobs: bin: cargo-deny runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@master with: @@ -215,7 +192,7 @@ jobs: runs-on: ubuntu-22.04 if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download book uses: actions/download-artifact@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 3a3871301..d9a1259ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,9 +42,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" [[package]] name = "byteorder" @@ -261,7 +261,7 @@ dependencies = [ "tame-index", "tempfile", "time", - "toml 0.8.8", + "toml-span", "twox-hash", "url", "walkdir", @@ -275,15 +275,15 @@ checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" dependencies = [ "semver", "serde", - "toml 0.7.8", + "toml", "url", ] [[package]] name = "cargo-platform" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" dependencies = [ "serde", ] @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.6" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" dependencies = [ "smallvec", "target-lexicon", @@ -330,9 +330,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" dependencies = [ "clap_builder", "clap_derive", @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" dependencies = [ "anstream", "anstyle", @@ -352,21 +352,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "clru" @@ -381,6 +381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3362992a0d9f1dd7c3d0e89e0ab2bb540b7a95fea8cd798090e758fda2899b5e" dependencies = [ "codespan-reporting", + "serde", ] [[package]] @@ -389,6 +390,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -438,9 +440,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -527,6 +529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -547,9 +550,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encode_unicode" @@ -852,9 +855,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ffc7db3fb50b7dae6ecd937a3527cb725f444614df2ad8988d81806f13f09" +checksum = "c82b5e9494e61983e61049bbd15fe0fa6b70672dd236362bdb5b2b50fc428f10" dependencies = [ "bstr", "gix-path", @@ -1091,9 +1094,9 @@ dependencies = [ [[package]] name = "gix-lock" -version = "13.0.0" +version = "13.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "651e46174dc5e7d18b7b809d31937b6de3681b1debd78618c99162cc30fcf3e1" +checksum = "886490a07b1d6433aa91262a99d430a91cc8b9a1f758ac1282e132f1098a9342" dependencies = [ "gix-tempfile", "gix-utils", @@ -1108,7 +1111,7 @@ checksum = "d75e7ab728059f595f6ddc1ad8771b8d6a231971ae493d9d5948ecad366ee8bb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -1212,9 +1215,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a6282621aed1becc3f83d64099a564b3b9063f22783d9a87ea502a3e9f2e40" +checksum = "97e9ad649bf5e109562d6acba657ca428661ec08e77eaf3a755d8fa55485be9c" dependencies = [ "bstr", "gix-trace", @@ -1376,9 +1379,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "13.0.0" +version = "13.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30" +checksum = "439f4da9657aec7434cde3c2510dcdb0a35f2031233ff67ae986269c788966d7" dependencies = [ "gix-fs", "libc", @@ -1559,9 +1562,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" [[package]] name = "home" @@ -1656,9 +1659,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown", @@ -1702,18 +1705,18 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -1747,9 +1750,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linked-hash-map" @@ -1781,13 +1784,13 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "maybe-async" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" +checksum = "afc95a651c82daf7004c824405aa1019723644950d488571bd718e3ed84646ed" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -1798,9 +1801,9 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45fd3a57831bf88bc63f8cebc0cf956116276e97fef3966103e96416209f7c92" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -1813,9 +1816,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -1840,11 +1843,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1861,9 +1870,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -1948,9 +1957,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plain" @@ -2046,9 +2055,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2063,9 +2072,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "async-compression", "base64", @@ -2091,6 +2100,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-rustls", @@ -2148,9 +2158,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2204,9 +2214,9 @@ dependencies = [ [[package]] name = "rustsec" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c843c389d3d5175ab32d894b1b5c08570fdd319ee78ef29e27c70bfc1be79464" +checksum = "229441b3fb74fa677ffd9f3fd4488ab4dcc980552cdf4d66d96ce25d745c294e" dependencies = [ "cargo-lock", "cvss", @@ -2215,7 +2225,7 @@ dependencies = [ "semver", "serde", "thiserror", - "toml 0.7.8", + "toml", "url", ] @@ -2272,7 +2282,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -2319,29 +2329,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -2474,37 +2484,37 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" dependencies = [ "proc-macro2", "quote", @@ -2512,15 +2522,10 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.48" +name = "sync_wrapper" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" @@ -2545,9 +2550,9 @@ dependencies = [ [[package]] name = "tame-index" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c3e0e3420ed82a172b5be8120d7d051b6578b42e1efa9f51f9e9ff675012dc" +checksum = "2d8d8b231dac2161abc5ac5abc07115fa29886f1defa6eae2bfcca40d6d61560" dependencies = [ "bytes", "camino", @@ -2566,7 +2571,7 @@ dependencies = [ "smol_str", "thiserror", "tokio", - "toml 0.8.8", + "toml-span", "twox-hash", ] @@ -2584,13 +2589,12 @@ checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] @@ -2606,33 +2610,34 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -2648,10 +2653,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -2672,9 +2678,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -2719,19 +2725,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] -name = "toml" -version = "0.8.8" +name = "toml-span" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "32c5896fa509a428e0ffcb1e37c7954f183a507784cd2999cb76c3b7544bf52d" dependencies = [ + "codespan-reporting", "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.21.0", + "smallvec", ] [[package]] @@ -2756,19 +2761,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" version = "0.3.2" @@ -2906,9 +2898,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2916,24 +2908,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -2943,9 +2935,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2953,28 +2945,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -2982,9 +2974,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "winapi" @@ -3151,9 +3143,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index c08da516b..8613f697f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,8 +55,8 @@ camino = "1.1" # Argument parsing, kept aligned with cargo clap = { version = "4.3", features = ["derive", "env"] } # Used for diagnostic reporting -codespan = "0.11" -codespan-reporting = "0.11" +codespan = { version = "0.11", features = ["serialization"] } +codespan-reporting = { version = "0.11", features = ["serialization"] } # Brrrrr crossbeam = "0.8" # Logging utilities @@ -88,7 +88,7 @@ reqwest = { version = "0.11", default-features = false } # sha-256 hash calculation, already a dependency via rustls/etc ring = "0.17" # Used for interacting with advisory databases -rustsec = { version = "0.28", default-features = false } +rustsec = { version = "0.29", default-features = false } # Parsing and checking of versions/version requirements semver = "1.0" # Gee what could it be @@ -99,7 +99,7 @@ smallvec = "1.9" # Used for parsing and checking SPDX license expressions spdx = "0.10" # Lazy -strum = { version = "0.25", features = ["derive"] } +strum = { version = "0.26", features = ["derive"] } # Index retrieval and querying tame-index = { version = "0.9", default-features = false, features = [ "git", @@ -111,7 +111,7 @@ time = { version = "0.3", default-features = false, features = [ "macros", ] } # Deserialization of configuration files and crate manifests -toml = "0.8" +toml-span = { version = "0.1.0", features = ["reporting"] } # Small fast hash crate twox-hash = { version = "1.5", default-features = false } # Url parsing/manipulation @@ -137,6 +137,8 @@ fs_extra = "1.3" # Snapshot testing insta = { version = "1.21", features = ["json"] } tame-index = { version = "0.9", features = ["local-builder"] } +time = { version = "0.3", features = ["serde"] } +toml-span = { version = "0.1.0", features = ["serde"] } # We use this for creating fake crate directories for crawling license files on disk tempfile = "3.1.0" diff --git a/deny.toml b/deny.toml index d80720f8f..6cf4e0b20 100644 --- a/deny.toml +++ b/deny.toml @@ -1,12 +1,14 @@ +[graph] # cargo-deny is really only ever intended to run on the "normal" tier-1 targets targets = [ - { triple = "x86_64-unknown-linux-gnu" }, - { triple = "aarch64-unknown-linux-gnu" }, - { triple = "x86_64-unknown-linux-musl" }, - { triple = "aarch64-apple-darwin" }, - { triple = "x86_64-apple-darwin" }, - { triple = "x86_64-pc-windows-msvc" }, + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", ] +all-features = true [advisories] vulnerability = "deny" @@ -23,37 +25,25 @@ ignore = [ multiple-versions = "deny" wildcards = 'deny' deny = [ - # We use gix - { name = "git2" }, - # NEVER AGAIN - { name = "openssl" }, - { name = "openssl-sys" }, - { name = "libssh2-sys" }, - # There is literally never a reason to use this - { name = "cmake" }, - # Ditto - { name = "windows" }, + { crate = "git2", use-instead = "gix" }, + { crate = "openssl", use-instead = "rustls" }, + { crate = "openssl-sys", use-instead = "rustls" }, + "libssh2-sys", + { crate = "cmake", use-instead = "cc" }, + { crate = "windows", reason = "bloated and unnecessary", use-instead = "ideally inline bindings, practically, windows-sys" }, ] skip = [ - # strum_macros + maybe-async - { name = "syn", version = "=1.0.109" }, - # security-framework uses an ooooold version - { name = "bitflags", version = "=1.3.2" }, + # https://github.com/seanmonstar/reqwest/pull/2130 should be in the next reqwest release + { crate = "bitflags@1.3.2", reason = "reqwest -> system-configuration uses this old version" }, ] skip-tree = [ - # Sigh - { name = "windows-sys", version = "<=0.48" }, - # cargo-lock uses an old version - { name = "toml", version = "=0.7.8" }, + { crate = "windows-sys:<=0.48", reason = "a foundational crate for many that bumps far too frequently to ever have a shared version" }, ] [sources] unknown-registry = "deny" unknown-git = "deny" -# [sources.allow-org] -# github = ["EmbarkStudios"] - [licenses] unlicensed = "deny" allow-osi-fsf-free = "neither" @@ -71,18 +61,18 @@ allow = [ exceptions = [ { allow = [ "Zlib", - ], name = "tinyvec" }, + ], crate = "tinyvec" }, { allow = [ "Unicode-DFS-2016", - ], name = "unicode-ident" }, + ], crate = "unicode-ident" }, { allow = [ "OpenSSL", - ], name = "ring" }, + ], crate = "ring" }, ] # Sigh [[licenses.clarify]] -name = "ring" +crate = "ring" # SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses # https://spdx.org/licenses/OpenSSL.html # ISC - Both BoringSSL and ring use this for their new files @@ -94,12 +84,12 @@ expression = "ISC AND MIT AND OpenSSL" license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] [[licenses.clarify]] -name = "webpki" +crate = "webpki" expression = "ISC" license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] # Actually "ISC-style" [[licenses.clarify]] -name = "rustls-webpki" +crate = "rustls-webpki" expression = "ISC" license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] diff --git a/src/advisories.rs b/src/advisories.rs index 55301c77b..a2582d19f 100644 --- a/src/advisories.rs +++ b/src/advisories.rs @@ -71,6 +71,7 @@ pub fn check( use bitvec::prelude::*; let mut ignore_hits: BitVec = BitVec::repeat(false, ctx.cfg.ignore.len()); + let mut ignore_yanked_hits: BitVec = BitVec::repeat(false, ctx.cfg.ignore_yanked.len()); // Emit diagnostics for any advisories found that matched crates in the graph for (krate, krate_index, advisory) in &report.advisories { @@ -98,7 +99,22 @@ pub fn check( sink.push(ctx.diag_for_index_failure(krate, ind, e)); } } else { - sink.push(ctx.diag_for_yanked(krate, ind)); + // Check to see if the user has added an ignore for the yanked + // crate, eg. see https://github.com/EmbarkStudios/cargo-deny/issues/579 + // this should be extremely rare and very temporary as in most cases + // a new semver compatible version of the yanked version is published + // around the same time as a yank occurs + if let Some(i) = ctx + .cfg + .ignore_yanked + .iter() + .position(|iy| crate::match_krate(krate, &iy.spec)) + { + sink.push(ctx.diag_for_yanked_ignore(krate, i)); + ignore_yanked_hits.as_mut_bitslice().set(i, true); + } else { + sink.push(ctx.diag_for_yanked(krate, ind)); + } } } @@ -122,6 +138,14 @@ pub fn check( sink.push(ctx.diag_for_advisory_not_encountered(ignore)); } + for ignore in ignore_yanked_hits + .into_iter() + .zip(ctx.cfg.ignore_yanked.iter()) + .filter_map(|(hit, ignore)| if !hit { Some(ignore) } else { None }) + { + sink.push(ctx.diag_for_ignored_yanked_not_encountered(ignore)); + } + if let Some(mut reporter) = audit_compatible_reporter { for ser_report in report.serialized_reports { reporter.report(ser_report); diff --git a/src/advisories/cfg.rs b/src/advisories/cfg.rs index 959fa9065..f7ff43907 100644 --- a/src/advisories/cfg.rs +++ b/src/advisories/cfg.rs @@ -1,47 +1,32 @@ use crate::{ + cfg::{PackageSpecOrExtended, Reason, ValidationContext}, diag::{Diagnostic, FileId, Label}, LintLevel, PathBuf, Spanned, }; use rustsec::advisory; -use serde::Deserialize; +use time::Duration; +use toml_span::{de_helpers::*, value::ValueInner}; use url::Url; -#[allow(clippy::reversed_empty_ranges)] -const fn yanked() -> Spanned { - Spanned::new(LintLevel::Warn, 0..0) -} - -#[allow(clippy::reversed_empty_ranges)] -fn ninety_days() -> Spanned { - Spanned::new("P90D".to_owned(), 0..0) -} - -#[derive(Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct Config { /// Path to the root directory where advisory databases are stored (default: $CARGO_HOME/advisory-dbs) pub db_path: Option, /// List of urls to git repositories of different advisory databases. - #[serde(default)] - pub db_urls: Vec>, + pub db_urls: Vec>, /// How to handle crates that have a security vulnerability - #[serde(default = "crate::lint_deny")] pub vulnerability: LintLevel, /// How to handle crates that have been marked as unmaintained in an advisory database - #[serde(default = "crate::lint_warn")] pub unmaintained: LintLevel, /// How to handle crates that have been marked as unsound in an advisory database - #[serde(default = "crate::lint_warn")] pub unsound: LintLevel, /// How to handle crates that have been yanked from eg crates.io - #[serde(default = "yanked")] pub yanked: Spanned, /// How to handle crates that have been marked with a notice in the advisory database - #[serde(default = "crate::lint_warn")] pub notice: LintLevel, /// Ignore advisories for the given IDs - #[serde(default)] - pub ignore: Vec>, + pub ignore: Vec, + /// Ignore yanked crates + pub ignore_yanked: Vec>>, /// CVSS Qualitative Severity Rating Scale threshold to alert at. /// /// Vulnerabilities with explicit CVSS info which have a severity below @@ -50,7 +35,6 @@ pub struct Config { /// Use the git executable to fetch advisory database rather than gitoxide pub git_fetch_with_cli: Option, /// If set to true, the local crates indices are not checked for yanked crates - #[serde(default)] pub disable_yank_checking: bool, /// The maximum duration, in RFC3339 format, that an advisory database is /// allowed to not have been updated. This only applies when fetching advisory @@ -59,8 +43,7 @@ pub struct Config { /// Note that if fractional units are used in the format string they must /// use the '.' separator instead of ',' which is used by some locales and /// supported in the RFC3339 format, but not by this implementation - #[serde(default = "ninety_days")] - pub maximum_db_staleness: Spanned, + pub maximum_db_staleness: Spanned, } impl Default for Config { @@ -69,96 +52,194 @@ impl Default for Config { db_path: None, db_urls: Vec::new(), ignore: Vec::new(), + ignore_yanked: Vec::new(), vulnerability: LintLevel::Deny, unmaintained: LintLevel::Warn, unsound: LintLevel::Warn, - yanked: yanked(), + yanked: Spanned::new(LintLevel::Warn), notice: LintLevel::Warn, severity_threshold: None, git_fetch_with_cli: None, disable_yank_checking: false, - maximum_db_staleness: ninety_days(), + maximum_db_staleness: Spanned::new(Duration::seconds_f64(NINETY_DAYS)), } } } -impl crate::cfg::UnvalidatedConfig for Config { - type ValidCfg = ValidConfig; +const NINETY_DAYS: f64 = 90. * 24. * 60. * 60. * 60.; + +impl<'de> toml_span::Deserialize<'de> for Config { + fn deserialize( + value: &mut toml_span::value::Value<'de>, + ) -> Result { + let mut th = toml_span::de_helpers::TableHelper::new(value)?; + + let db_path = th.optional::("db-path").map(PathBuf::from); + let db_urls = if let Some((_, mut urls)) = th.take("db-urls") { + let mut u = Vec::new(); + + match urls.take() { + ValueInner::Array(urla) => { + for mut v in urla { + match parse(&mut v) { + Ok(url) => u.push(Spanned::with_span(url, v.span)), + Err(err) => th.errors.push(err), + } + } + } + other => { + th.errors.push(expected("an array", other, urls.span)); + } + } - fn validate( - self, - cfg_file: FileId, - _files: &mut crate::diag::Files, - diags: &mut Vec, - ) -> Self::ValidCfg { - let mut ignored: Vec<_> = self.ignore.into_iter().map(AdvisoryId::from).collect(); - ignored.sort(); - - let mut db_urls: Vec<_> = self - .db_urls - .into_iter() - .filter_map(|dburl| match crate::cfg::parse_url(cfg_file, dburl) { - Ok(u) => Some(u), - Err(diag) => { - diags.push(diag); - None + u.sort(); + u + } else { + Vec::new() + }; + let vulnerability = th.optional("vulnerability").unwrap_or(LintLevel::Deny); + let unmaintained = th.optional("unmaintained").unwrap_or(LintLevel::Warn); + let unsound = th.optional("unsound").unwrap_or(LintLevel::Warn); + let yanked = th + .optional_s("yanked") + .unwrap_or(Spanned::new(LintLevel::Warn)); + let notice = th.optional("notice").unwrap_or(LintLevel::Warn); + let (ignore, ignore_yanked) = if let Some((_, mut ignore)) = th.take("ignore") { + let mut u = Vec::new(); + let mut y = Vec::new(); + + match ignore.take() { + ValueInner::Array(ida) => { + for mut v in ida { + let inner = v.take(); + if let ValueInner::String(s) = &inner { + // Attempt to parse an advisory id first, note we can't + // just immediately use parse as the from_str implementation + // for id will just blindly accept any string + if advisory::IdKind::detect(s.as_ref()) != advisory::IdKind::Other { + if let Ok(id) = s.parse::() { + u.push(Spanned::with_span(id, v.span)); + continue; + } + } + } + + let found = inner.type_str(); + v.set(inner); + + match PackageSpecOrExtended::deserialize(&mut v) { + Ok(pse) => y.push(Spanned::with_span(pse, v.span)), + Err(_err) => { + th.errors.push(toml_span::Error { + kind: toml_span::ErrorKind::Wanted { + expected: "an advisory id or package spec", + found, + }, + span: v.span, + line_info: None, + }); + } + } + } } - }) - .collect(); - - db_urls.sort(); - - // Warn about duplicates before removing them so the user can cleanup their config - if db_urls.len() > 1 { - for window in db_urls.windows(2) { - if window[0] == window[1] { - diags.push( - Diagnostic::warning() - .with_message("duplicate advisory database url detected") - .with_labels(vec![ - Label::secondary(cfg_file, window[0].span.clone()), - Label::secondary(cfg_file, window[1].span.clone()), - ]), - ); + other => { + th.errors.push(expected("an array", other, ignore.span)); } } - } - db_urls.dedup(); + u.sort(); + (u, y) + } else { + (Vec::new(), Vec::new()) + }; + let severity_threshold = th.parse_opt("severity-threshold"); + let git_fetch_with_cli = th.optional("git-fetch-with-cli"); + let disable_yank_checking = th.optional("disable-yank-checking").unwrap_or_default(); + let maximum_db_staleness = if let Some((_, mut val)) = th.take("maximum-db-staleness") { + match val.take_string(Some("an RFC3339 time duration")) { + Ok(mds) => match parse_rfc3339_duration(&mds) { + Ok(mds) => Some(Spanned::with_span(mds, val.span)), + Err(err) => { + th.errors.push( + ( + toml_span::ErrorKind::Custom(err.to_string().into()), + val.span, + ) + .into(), + ); + None + } + }, + Err(err) => { + th.errors.push(err); + None + } + } + } else { + None + }; + + th.finalize(None)?; + + // Use the 90 days default as a fallback + let maximum_db_staleness = maximum_db_staleness + .unwrap_or_else(|| Spanned::new(Duration::seconds_f64(NINETY_DAYS))); + + Ok(Self { + db_path, + db_urls, + vulnerability, + unmaintained, + unsound, + yanked, + notice, + ignore, + ignore_yanked, + severity_threshold, + git_fetch_with_cli, + disable_yank_checking, + maximum_db_staleness, + }) + } +} + +impl crate::cfg::UnvalidatedConfig for Config { + type ValidCfg = ValidConfig; + + fn validate(self, mut ctx: ValidationContext<'_>) -> Self::ValidCfg { + let mut ignore = self.ignore; + let mut ignore_yanked = self.ignore_yanked; + let mut db_urls = self.db_urls; + + ctx.dedup(&mut ignore); + ctx.dedup(&mut ignore_yanked); + ctx.dedup(&mut db_urls); // Require that each url has a valid domain name for when we splat it to a local path for url in &db_urls { if url.value.domain().is_none() { - diags.push( + ctx.push( Diagnostic::error() .with_message("advisory database url doesn't have a domain name") - .with_labels(vec![Label::secondary(cfg_file, url.span.clone())]), + .with_labels(vec![Label::secondary(ctx.cfg_id, url.span)]), ); } } - let maximum_db_staleness = match parse_rfc3339_duration(&self.maximum_db_staleness.value) { - Ok(mds) => mds, - Err(err) => { - diags.push( - Diagnostic::error() - .with_message("failed to parse RFC3339 duration") - .with_labels(vec![Label::secondary( - cfg_file, - self.maximum_db_staleness.span.clone(), - )]) - .with_notes(vec![err.to_string()]), - ); - // Use the 90 days default as a fallback - time::Duration::seconds_f64(90. * 24. * 60. * 60. * 60.) - } - }; - ValidConfig { - file_id: cfg_file, + file_id: ctx.cfg_id, db_path: self.db_path, db_urls, - ignore: ignored, + ignore, + ignore_yanked: ignore_yanked + .into_iter() + .map(|s| crate::bans::SpecAndReason { + spec: s.value.spec, + reason: s.value.inner, + use_instead: None, + file_id: ctx.cfg_id, + }) + .collect(), vulnerability: self.vulnerability, unmaintained: self.unmaintained, unsound: self.unsound, @@ -167,18 +248,20 @@ impl crate::cfg::UnvalidatedConfig for Config { severity_threshold: self.severity_threshold, git_fetch_with_cli: self.git_fetch_with_cli.unwrap_or_default(), disable_yank_checking: self.disable_yank_checking, - maximum_db_staleness, + maximum_db_staleness: self.maximum_db_staleness, } } } pub(crate) type AdvisoryId = Spanned; +#[cfg_attr(test, derive(serde::Serialize))] pub struct ValidConfig { pub file_id: FileId, pub db_path: Option, pub db_urls: Vec>, pub(crate) ignore: Vec, + pub(crate) ignore_yanked: Vec, pub vulnerability: LintLevel, pub unmaintained: LintLevel, pub unsound: LintLevel, @@ -187,7 +270,7 @@ pub struct ValidConfig { pub severity_threshold: Option, pub git_fetch_with_cli: bool, pub disable_yank_checking: bool, - pub maximum_db_staleness: time::Duration, + pub maximum_db_staleness: Spanned, } /// We need to implement this ourselves since time doesn't support it @@ -206,7 +289,7 @@ pub struct ValidConfig { /// /// duration = "P" (dur-date / dur-time / dur-week) /// ``` -fn parse_rfc3339_duration(value: &str) -> anyhow::Result { +fn parse_rfc3339_duration(value: &str) -> anyhow::Result { use anyhow::Context as _; let mut value = value @@ -277,7 +360,7 @@ fn parse_rfc3339_duration(value: &str) -> anyhow::Result { } } - let mut duration = time::Duration::new(0, 0); + let mut duration = Duration::new(0, 0); // The format requires that the units are in a specific order, but each // unit is optional @@ -347,54 +430,55 @@ fn parse_rfc3339_duration(value: &str) -> anyhow::Result { #[cfg(test)] mod test { use super::{parse_rfc3339_duration as dur_parse, *}; - use crate::cfg::{test::*, Fake, UnvalidatedConfig}; + use crate::test_utils::{write_diagnostics, ConfigData}; + + struct Advisories { + advisories: Config, + } + + impl<'de> toml_span::Deserialize<'de> for Advisories { + fn deserialize( + value: &mut toml_span::value::Value<'de>, + ) -> Result { + let mut th = toml_span::de_helpers::TableHelper::new(value)?; + let advisories = th.required("advisories").unwrap(); + th.finalize(None)?; + Ok(Self { advisories }) + } + } #[test] fn deserializes_advisories_cfg() { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct Advisories { - advisories: Config, - } + let cd = ConfigData::::load("tests/cfg/advisories.toml"); + let validated = cd.validate(|a| a.advisories); - let mut cd: ConfigData = load("tests/cfg/advisories.toml"); - let mut diags = Vec::new(); - let validated = cd - .config - .advisories - .validate(cd.id, &mut cd.files, &mut diags); - assert!( - !diags - .iter() - .any(|d| d.severity >= crate::diag::Severity::Error), - "{diags:#?}" - ); + insta::assert_json_snapshot!(validated); + } - assert_eq!(validated.file_id, cd.id); - assert!(validated - .db_path - .iter() - .map(|dp| dp.as_str()) - .eq(vec!["~/.cargo/advisory-dbs"])); - assert!(validated.db_urls.iter().eq(vec![&Url::parse( - "https://github.com/RustSec/advisory-db" - ) - .unwrap() - .fake()])); - assert_eq!(validated.vulnerability, LintLevel::Deny); - assert_eq!(validated.unmaintained, LintLevel::Warn); - assert_eq!(validated.unsound, LintLevel::Warn); - assert_eq!(validated.yanked, LintLevel::Warn); - assert_eq!(validated.notice, LintLevel::Warn); - assert_eq!( - validated.ignore, - vec!["RUSTSEC-0000-0000" - .parse::() - .unwrap()] - ); - assert_eq!( - validated.severity_threshold, - Some(rustsec::advisory::Severity::Medium) + #[test] + fn warns_on_duplicates() { + let dupes = r#" +[advisories] +db-urls = [ + "https://github.com/rust-lang/crates.io-index", + "https://one.reg", + "https://one.reg", +] +ignore = [ + "RUSTSEC-0000-0001", + { crate = "boop" }, + "RUSTSEC-0000-0001", + "boop", +] +"#; + + let cd = ConfigData::::load_str("duplicates", dupes); + let _validated = cd.validate_with_diags( + |a| a.advisories, + |files, diags| { + let diags = write_diagnostics(files, diags.into_iter()); + insta::assert_snapshot!(diags); + }, ); } @@ -414,7 +498,7 @@ mod test { let failures: String = FAILURES.iter().fold(String::new(), |mut acc, bad| { use std::fmt::Write; - writeln!(&mut acc, "{:?}", dur_parse(bad)).unwrap(); + writeln!(&mut acc, "{:#?}", dur_parse(bad).unwrap_err()).unwrap(); acc }); @@ -462,7 +546,7 @@ mod test { Ok(parsed) => { assert_eq!( parsed, - time::Duration::seconds_f64(*secs), + Duration::seconds_f64(*secs), "unexpected duration for '{dur}'" ); } diff --git a/src/advisories/diags.rs b/src/advisories/diags.rs index 57559dc18..7fe23c6ce 100644 --- a/src/advisories/diags.rs +++ b/src/advisories/diags.rs @@ -22,9 +22,11 @@ pub enum Code { Unmaintained, Unsound, Yanked, + YankedIgnored, IndexFailure, IndexCacheLoadFailure, AdvisoryNotDetected, + YankedNotDetected, UnknownAdvisory, } @@ -187,6 +189,18 @@ impl<'a> crate::CheckCtx<'a, super::cfg::ValidConfig> { pack } + pub(crate) fn diag_for_yanked_ignore(&self, krate: &crate::Krate, ignore: usize) -> Pack { + let mut pack = Pack::with_kid(Check::Advisories, krate.id.clone()); + pack.push( + Diagnostic::note() + .with_message(format!("yanked crate '{krate}' detected, but ignored",)) + .with_code(Code::YankedIgnored) + .with_labels(self.cfg.ignore_yanked[ignore].to_labels(Some("yanked ignore"))), + ); + + pack + } + pub(crate) fn diag_for_index_failure( &self, krate: &crate::Krate, @@ -202,7 +216,7 @@ impl<'a> crate::CheckCtx<'a, super::cfg::ValidConfig> { // to the beginning and confuses users if !self.cfg.yanked.span.is_empty() { labels.push( - Label::primary(self.cfg.file_id, self.cfg.yanked.span.clone()) + Label::primary(self.cfg.file_id, self.cfg.yanked.span) .with_message("lint level defined here"), ); } @@ -238,20 +252,36 @@ impl<'a> crate::CheckCtx<'a, super::cfg::ValidConfig> { Diagnostic::new(Severity::Warning) .with_message("advisory was not encountered") .with_code(Code::AdvisoryNotDetected) - .with_labels(vec![Label::primary(self.cfg.file_id, not_hit.span.clone()) + .with_labels(vec![Label::primary(self.cfg.file_id, not_hit.span) .with_message("no crate matched advisory criteria")]), ) .into() } + #[allow(clippy::unused_self)] + pub(crate) fn diag_for_ignored_yanked_not_encountered( + &self, + not_hit: &crate::bans::SpecAndReason, + ) -> Pack { + ( + Check::Advisories, + Diagnostic::new(Severity::Warning) + .with_message("yanked crate was not encountered") + .with_code(Code::YankedNotDetected) + .with_labels(not_hit.to_labels(Some("yanked crate not detected"))), + ) + .into() + } + pub(crate) fn diag_for_unknown_advisory(&self, unknown: &crate::cfg::Spanned) -> Pack { ( Check::Advisories, Diagnostic::new(Severity::Warning) .with_message("advisory not found in any advisory database") .with_code(Code::UnknownAdvisory) - .with_labels(vec![Label::primary(self.cfg.file_id, unknown.span.clone()) - .with_message("unknown advisory")]), + .with_labels(vec![ + Label::primary(self.cfg.file_id, unknown.span).with_message("unknown advisory") + ]), ) .into() } diff --git a/src/advisories/helpers/index.rs b/src/advisories/helpers/index.rs index 732ad8e39..bccaeb023 100644 --- a/src/advisories/helpers/index.rs +++ b/src/advisories/helpers/index.rs @@ -39,6 +39,7 @@ impl<'k> Indices<'k> { indices.push((source, index)); } + #[allow(clippy::blocks_in_conditions)] let cargo_package_lock = match tame_index::utils::flock::LockOptions::cargo_package_lock(Some(cargo_home)) .expect("unreachable") diff --git a/src/advisories/snapshots/cargo_deny__advisories__cfg__test__deserializes_advisories_cfg.snap b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__deserializes_advisories_cfg.snap new file mode 100644 index 000000000..6039f3894 --- /dev/null +++ b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__deserializes_advisories_cfg.snap @@ -0,0 +1,44 @@ +--- +source: src/advisories/cfg.rs +expression: validated +--- +{ + "file_id": 1, + "db_path": "~/.cargo/advisory-dbs", + "db_urls": [ + "https://github.com/RustSec/advisory-db" + ], + "ignore": [ + "RUSTSEC-0000-0000" + ], + "ignore_yanked": [ + { + "spec": { + "name": "crate", + "version-req": "=0.1" + }, + "reason": null, + "use-instead": null + }, + { + "spec": { + "name": "yanked", + "version-req": null + }, + "reason": "a new version has not been released", + "use-instead": null + } + ], + "vulnerability": "deny", + "unmaintained": "warn", + "unsound": "warn", + "yanked": "warn", + "notice": "warn", + "severity_threshold": "medium", + "git_fetch_with_cli": false, + "disable_yank_checking": false, + "maximum_db_staleness": [ + 466560000, + 0 + ] +} diff --git a/src/advisories/snapshots/cargo_deny__advisories__cfg__test__rejects_invalid_durations.snap b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__rejects_invalid_durations.snap index 10433f6bd..a92cfafea 100644 --- a/src/advisories/snapshots/cargo_deny__advisories__cfg__test__rejects_invalid_durations.snap +++ b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__rejects_invalid_durations.snap @@ -2,17 +2,17 @@ source: src/advisories/cfg.rs expression: failures --- -Err(duration requires 'P' prefix) -Err(must supply at least one time unit) -Err(must supply at least one time unit) -Err('H' must be preceded with 'T') -Err(unit not specified for value '2') -Err(value not specified for 'M') -Err(unit 'H' cannot follow 'M') -Err(unit 'Y' cannot follow 'M') -Err(unit 'Y' cannot follow 'W') -Err(unit 'H' cannot follow 'W') -Err('H' must be preceded with 'T') -Err('S' must be preceded with 'T') -Err(',' is valid in the RFC-3339 duration format but not supported by this implementation, use '.' instead) +duration requires 'P' prefix +"must supply at least one time unit" +"must supply at least one time unit" +"'H' must be preceded with 'T'" +"unit not specified for value '2'" +"value not specified for 'M'" +"unit 'H' cannot follow 'M'" +"unit 'Y' cannot follow 'M'" +"unit 'Y' cannot follow 'W'" +"unit 'H' cannot follow 'W'" +"'H' must be preceded with 'T'" +"'S' must be preceded with 'T'" +"',' is valid in the RFC-3339 duration format but not supported by this implementation, use '.' instead" diff --git a/src/advisories/snapshots/cargo_deny__advisories__cfg__test__warns_on_duplicates.snap b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__warns_on_duplicates.snap new file mode 100644 index 000000000..25e65f960 --- /dev/null +++ b/src/advisories/snapshots/cargo_deny__advisories__cfg__test__warns_on_duplicates.snap @@ -0,0 +1,31 @@ +--- +source: src/advisories/cfg.rs +expression: diags +--- +warning: duplicate items detected + ┌─ duplicates:9:6 + │ + 9 │ "RUSTSEC-0000-0001", + │ ----------------- +10 │ { crate = "boop" }, +11 │ "RUSTSEC-0000-0001", + │ ----------------- + +warning: duplicate items detected + ┌─ duplicates:10:5 + │ +10 │ { crate = "boop" }, + │ ------------------ +11 │ "RUSTSEC-0000-0001", +12 │ "boop", + │ ---- + +warning: duplicate items detected + ┌─ duplicates:5:6 + │ +5 │ "https://one.reg", + │ --------------- +6 │ "https://one.reg", + │ --------------- + + diff --git a/src/bans.rs b/src/bans.rs index d4858b746..95ed690dc 100644 --- a/src/bans.rs +++ b/src/bans.rs @@ -2,8 +2,9 @@ pub mod cfg; mod diags; mod graph; -use self::cfg::{TreeSkip, ValidBuildConfig, ValidConfig}; +use self::cfg::{ValidBuildConfig, ValidConfig, ValidTreeSkip}; use crate::{ + cfg::{PackageSpec, Reason, Span, Spanned}, diag::{self, CfgCoord, FileId, KrateCoord}, Kid, Krate, Krates, LintLevel, }; @@ -13,50 +14,58 @@ use krates::cm::DependencyKind; use semver::VersionReq; use std::fmt; -#[derive(PartialEq, Eq, Clone)] -#[cfg_attr(test, derive(Debug))] -pub struct KrateId { - pub(crate) name: String, - pub(crate) version: Option, +struct ReqMatch<'vr> { + specr: &'vr SpecAndReason, + index: usize, } -impl fmt::Display for KrateId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} = {:?}", self.name, self.version) - } +pub(crate) struct SpecAndReason { + pub(crate) spec: PackageSpec, + pub(crate) reason: Option, + pub(crate) use_instead: Option>, + pub(crate) file_id: FileId, } -struct ReqMatch<'vr> { - id: &'vr cfg::Skrate, - index: usize, +#[cfg(test)] +impl serde::Serialize for SpecAndReason { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("spec", &self.spec)?; + map.serialize_entry("reason", &self.reason)?; + map.serialize_entry("use-instead", &self.use_instead)?; + map.end() + } } -/// Returns the version requirements that matched the version, if any -#[inline] -fn matches<'v>(arr: &'v [cfg::Skrate], details: &Krate) -> Option>> { - let matches: Vec<_> = arr - .iter() - .enumerate() - .filter_map(|(index, req)| { - if req.value.name == details.name - && crate::match_req(&details.version, req.value.version.as_ref()) - { - Some(ReqMatch { id: req, index }) - } else { - None - } - }) - .collect(); +struct SpecsAndReasons(Vec); - if matches.is_empty() { - None - } else { - Some(matches) +impl SpecsAndReasons { + /// Returns the specs that match the specified crate + #[inline] + fn matches<'s>(&'s self, details: &Krate) -> Option>> { + let matches: Vec<_> = self + .0 + .iter() + .enumerate() + .filter_map(|(index, req)| { + crate::match_krate(details, &req.spec).then_some(ReqMatch { specr: req, index }) + }) + .collect(); + + if matches.is_empty() { + None + } else { + Some(matches) + } } } struct SkipRoot { - span: std::ops::Range, + specr: SpecAndReason, skip_crates: Vec, skip_hits: BitVec, } @@ -67,15 +76,10 @@ use bitvec::prelude::*; // each dependency as a skipped crate at the specific version struct TreeSkipper { roots: Vec, - cfg_file_id: FileId, } impl TreeSkipper { - fn build( - skip_roots: Vec>, - krates: &Krates, - cfg_file_id: FileId, - ) -> (Self, Pack) { + fn build(skip_roots: Vec, krates: &Krates, cfg_file_id: FileId) -> (Self, Pack) { let mut roots = Vec::with_capacity(skip_roots.len()); let mut pack = Pack::new(Check::Bans); @@ -83,11 +87,11 @@ impl TreeSkipper { for ts in skip_roots { let num_roots = roots.len(); - for nid in krates.krates_by_name(&ts.value.id.name).filter_map(|km| { - crate::match_req(&km.krate.version, ts.value.id.version.as_ref()) + for nid in krates.krates_by_name(&ts.spec.name.value).filter_map(|km| { + crate::match_req(&km.krate.version, ts.spec.version_req.as_ref()) .then_some(km.node_id) }) { - roots.push(Self::build_skip_root(ts.clone(), nid, krates)); + roots.push(Self::build_skip_root(ts.clone(), cfg_file_id, nid, krates)); } // If no roots were added, add a diagnostic that the user's configuration @@ -96,24 +100,25 @@ impl TreeSkipper { pack.push(diags::UnmatchedSkipRoot { skip_root_cfg: CfgCoord { file: cfg_file_id, - span: ts.span, + span: ts.spec.name.span, }, }); } } - (Self { roots, cfg_file_id }, pack) + (Self { roots }, pack) } fn build_skip_root( - ts: crate::Spanned, + ts: ValidTreeSkip, + file_id: FileId, krate_id: krates::NodeId, krates: &Krates, ) -> SkipRoot { - let span = ts.span; - let ts = ts.value; + let (max_depth, reason) = ts.inner.map_or((std::usize::MAX, None), |inn| { + (inn.depth.unwrap_or(std::usize::MAX), inn.reason) + }); - let max_depth = ts.depth.unwrap_or(std::usize::MAX); let mut skip_crates = Vec::with_capacity(10); let graph = krates.graph(); @@ -139,7 +144,12 @@ impl TreeSkipper { let skip_hits = BitVec::repeat(false, skip_crates.len()); SkipRoot { - span, + specr: SpecAndReason { + spec: ts.spec, + reason, + use_instead: None, + file_id, + }, skip_crates, skip_hits, } @@ -152,10 +162,7 @@ impl TreeSkipper { if let Ok(i) = root.skip_crates.binary_search(&krate.id) { pack.push(diags::SkippedByRoot { krate, - skip_root_cfg: CfgCoord { - file: self.cfg_file_id, - span: root.span.clone(), - }, + skip_root_cfg: &root.specr, }); root.skip_hits.as_mut_bitslice().set(i, true); @@ -214,15 +221,15 @@ pub fn check( sink.push(build_diags); } + use std::collections::BTreeMap; + struct BanWrappers { - map: std::collections::BTreeMap>)>, + map: BTreeMap>)>, hits: BitVec, } impl BanWrappers { - fn new( - mut map: std::collections::BTreeMap>)>, - ) -> Self { + fn new(mut map: BTreeMap>)>) -> Self { let hits = BitVec::repeat( false, map.values_mut().fold(0, |sum, v| { @@ -240,11 +247,11 @@ pub fn check( } #[inline] - fn check(&mut self, i: usize, name: &str) -> Option { + fn check(&mut self, i: usize, name: &str) -> Option { let (offset, wrappers) = &self.map[&i]; if let Some(pos) = wrappers.iter().position(|wrapper| wrapper.value == name) { self.hits.set(*offset + pos, true); - Some(wrappers[pos].span.clone()) + Some(wrappers[pos].span) } else { None } @@ -252,26 +259,53 @@ pub fn check( } let (denied_ids, mut ban_wrappers) = { - let mut bw = std::collections::BTreeMap::new(); + let mut bw = BTreeMap::new(); ( - denied - .into_iter() - .enumerate() - .map(|(i, kb)| { - if let Some(wrappers) = kb.wrappers.filter(|v| !v.is_empty()) { - bw.insert(i, (0, wrappers)); - } + SpecsAndReasons( + denied + .into_iter() + .enumerate() + .map(|(i, kb)| { + let (reason, use_instead) = if let Some(ext) = kb.inner { + if let Some(wrappers) = ext.wrappers.filter(|w| !w.is_empty()) { + bw.insert(i, (0, wrappers)); + } - kb.id - }) - .collect::>(), + (ext.reason, ext.use_instead) + } else { + (None, None) + }; + + SpecAndReason { + spec: kb.spec, + reason, + use_instead, + file_id, + } + }) + .collect(), + ), BanWrappers::new(bw), ) }; - let (feature_ids, features): (Vec<_>, Vec<_>) = - features.into_iter().map(|cf| (cf.id, cf.features)).unzip(); + let (feature_ids, features): (Vec<_>, Vec<_>) = features + .into_iter() + .map(|cf| { + ( + SpecAndReason { + spec: cf.spec, + reason: cf.reason, + use_instead: None, + file_id, + }, + cf.features, + ) + }) + .unzip(); + + let feature_ids = SpecsAndReasons(feature_ids); // Keep track of all the crates we skip, and emit a warning if // we encounter a skip that didn't actually match any crate version @@ -306,6 +340,42 @@ pub fn check( } }; + let dmv = SpecsAndReasons( + denied_multiple_versions + .into_iter() + .map(|spec| SpecAndReason { + spec, + reason: None, + use_instead: None, + file_id, + }) + .collect(), + ); + + let allowed = SpecsAndReasons( + allowed + .into_iter() + .map(|all| SpecAndReason { + spec: all.spec, + reason: all.inner, + use_instead: None, + file_id, + }) + .collect(), + ); + + let skipped = SpecsAndReasons( + skipped + .into_iter() + .map(|skip| SpecAndReason { + spec: skip.spec, + reason: skip.inner, + use_instead: None, + file_id, + }) + .collect(), + ); + let report_duplicates = |multi_detector: &MultiDetector<'_>, sink: &mut diag::ErrorSink| { if multi_detector.dupes.len() <= 1 { return; @@ -313,7 +383,7 @@ pub fn check( let lint_level = if multi_detector.dupes.iter().any(|kindex| { let krate = &ctx.krates[*kindex]; - matches(&denied_multiple_versions, krate).is_some() + dmv.matches(krate).is_some() }) { LintLevel::Deny } else { @@ -371,7 +441,7 @@ pub fn check( num_dupes: kids.len(), krates_coord: KrateCoord { file: krate_spans.file_id, - span: all_start..all_end, + span: (all_start..all_end).into(), }, severity, } @@ -448,11 +518,11 @@ pub fn check( let mut pack = Pack::with_kid(Check::Bans, krate.id.clone()); // Check if the crate has been explicitly banned - if let Some(matches) = matches(&denied_ids, krate) { + if let Some(matches) = denied_ids.matches(krate) { for rm in matches { let ban_cfg = CfgCoord { file: file_id, - span: rm.id.span.clone(), + span: rm.specr.spec.name.span, }; // The crate is banned, but it might be allowed if it's @@ -483,7 +553,7 @@ pub fn check( ), None => ( diags::BannedUnmatchedWrapper { - ban_cfg: ban_cfg.clone(), + ban_cfg: rm.specr, banned_krate: krate, parent_krate: src.krate, } @@ -502,23 +572,23 @@ pub fn check( }; if !is_allowed_by_wrapper { - pack.push(diags::ExplicitlyBanned { krate, ban_cfg }); + pack.push(diags::ExplicitlyBanned { + krate, + ban_cfg: rm.specr, + }); } } } - if !allowed.is_empty() { + if !allowed.0.is_empty() { // Since only allowing specific crates is pretty draconian, // also emit which allow filters actually passed each crate - match matches(&allowed, krate) { + match allowed.matches(krate) { Some(matches) => { for rm in matches { pack.push(diags::ExplicitlyAllowed { krate, - allow_cfg: CfgCoord { - file: file_id, - span: rm.id.span.clone(), - }, + allow_cfg: rm.specr, }); } } @@ -557,7 +627,7 @@ pub fn check( } // Check if the crate has had features denied/allowed or are required to be exact - if let Some(matches) = matches(&feature_ids, krate) { + if let Some(matches) = feature_ids.matches(krate) { for rm in matches { let feature_bans = &features[rm.index]; @@ -592,7 +662,7 @@ pub fn check( if !enabled_features.contains(&af.value) { Some(CfgCoord { file: file_id, - span: af.span.clone(), + span: af.span, }) } else { None @@ -608,7 +678,7 @@ pub fn check( not_allowed: ¬_explicitly_allowed, exact_coord: CfgCoord { file: file_id, - span: feature_bans.exact.span.clone(), + span: feature_bans.exact.span, }, krate, }); @@ -665,7 +735,7 @@ pub fn check( feature, allowed: CfgCoord { file: file_id, - span: feature_bans.allow.span.clone(), + span: feature_bans.allow.span, }, }); } @@ -744,14 +814,11 @@ pub fn check( } if should_add_dupe(&krate.id) { - if let Some(matches) = matches(&skipped, krate) { + if let Some(matches) = skipped.matches(krate) { for rm in matches { pack.push(diags::Skipped { krate, - skip_cfg: CfgCoord { - file: file_id, - span: rm.id.span.clone(), - }, + skip_cfg: rm.specr, }); // Mark each skip filter that is hit so that we can report unused @@ -903,16 +970,10 @@ pub fn check( for skip in skip_hit .into_iter() - .zip(skipped.into_iter()) + .zip(skipped.0.into_iter()) .filter_map(|(hit, skip)| (!hit).then_some(skip)) { - pack.push(diags::UnmatchedSkip { - skip_cfg: CfgCoord { - file: file_id, - span: skip.span, - }, - skipped_krate: &skip.value, - }); + pack.push(diags::UnmatchedSkip { skip_cfg: &skip }); } for wrapper in ban_wrappers @@ -940,24 +1001,21 @@ pub fn check_build( krates: &Krates, pack: &mut Pack, ) -> Option { - if let Some(allow_build_scripts) = &config.allow_build_scripts { + let build_script_allowed = if let Some(allow_build_scripts) = &config.allow_build_scripts { let has_build_script = krate .targets .iter() .any(|t| t.kind.iter().any(|k| *k == "custom-build")); - if has_build_script { - let allowed_build_script = allow_build_scripts.value.iter().any(|id| { - krate.name == id.name && crate::match_req(&krate.version, id.version.as_ref()) - }); - - if !allowed_build_script { - pack.push(diags::BuildScriptNotAllowed { krate }); - } - } - } + !has_build_script + || allow_build_scripts + .iter() + .any(|id| crate::match_krate(krate, id)) + } else { + true + }; - if config.executables == LintLevel::Allow { + if build_script_allowed && config.executables == LintLevel::Allow { return None; } @@ -1005,10 +1063,7 @@ pub fn check_build( .bypass .iter() .enumerate() - .find_map(|(i, ae)| { - (ae.name.value == krate.name && crate::match_req(&krate.version, ae.version.as_ref())) - .then_some((i, ae)) - }) + .find_map(|(i, ae)| crate::match_krate(krate, &ae.spec).then_some((i, ae))) .unzip(); // If the build script hashes to the same value and required features are not actually @@ -1074,6 +1129,11 @@ pub fn check_build( } } + if !build_script_allowed { + pack.push(diags::BuildScriptNotAllowed { krate }); + return kc_index; + } + let root = krate.manifest_path.parent().unwrap(); let (tx, rx) = crossbeam::channel::unbounded(); diff --git a/src/bans/cfg.rs b/src/bans/cfg.rs index 8b7855865..b389ab93e 100644 --- a/src/bans/cfg.rs +++ b/src/bans/cfg.rs @@ -1,68 +1,83 @@ -use super::KrateId; use crate::{ + cfg::{PackageSpec, PackageSpecOrExtended, Reason, ValidationContext}, diag::{Diagnostic, FileId, Label}, LintLevel, Spanned, }; -use semver::VersionReq; -use serde::Deserialize; +use toml_span::{de_helpers::TableHelper, value::Value, DeserError, Deserialize}; -#[derive(Deserialize, Clone)] #[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(deny_unknown_fields)] -pub struct CrateId { - // The name of the crate - pub name: String, - /// The version constraints of the crate - pub version: Option, -} - -#[derive(Deserialize, Clone)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct CrateBan { - pub name: Spanned, - pub version: Option, +pub struct CrateBanExtended { /// One or more crates that will allow this crate to be used if it is a /// direct dependency pub wrappers: Option>>>, - /// Setting this to true will only emit an error if multiple - // versions of the crate are found + /// Setting this to true will only emit an error if multiple versions of the + /// crate are found pub deny_multiple_versions: Option>, + /// The reason for banning the crate + pub reason: Option, + /// The crate to use instead of the banned crate, could be just the crate name + /// or a URL + pub use_instead: Option>, } -#[derive(Deserialize, Clone)] +impl<'de> Deserialize<'de> for CrateBanExtended { + fn deserialize(value: &mut Value<'de>) -> Result { + let mut th = TableHelper::new(value)?; + + let wrappers = th.optional("wrappers"); + let deny_multiple_versions = th.optional("deny-multiple-versions"); + let reason = th.optional_s("reason"); + let use_instead = th.optional("use-instead"); + th.finalize(None)?; + + Ok(Self { + wrappers, + deny_multiple_versions, + reason: reason.map(Reason::from), + use_instead, + }) + } +} + +//#[derive(Clone)] #[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(deny_unknown_fields)] pub struct CrateFeatures { - pub name: Spanned, - pub version: Option, + pub spec: PackageSpec, /// All features that are allowed to be used. - #[serde(default)] pub allow: Spanned>>, /// All features that are denied. - #[serde(default)] pub deny: Vec>, /// The actual feature set has to exactly match the `allow` set. - #[serde(default)] pub exact: Spanned, + /// The reason for specifying the crate features + pub reason: Option, } -#[derive(Deserialize, Clone)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct TreeSkip { - #[serde(flatten)] - pub id: CrateId, - pub depth: Option, -} +impl<'de> Deserialize<'de> for CrateFeatures { + fn deserialize(value: &mut Value<'de>) -> Result { + let spec = PackageSpec::deserialize(value)?; + + let mut th = TableHelper::new(value)?; + + let allow = th.optional("allow").unwrap_or_default(); + let deny = th.optional("deny").unwrap_or_default(); + let exact = th.optional("exact").unwrap_or_default(); + let reason = th.optional_s("reason"); + th.finalize(None)?; -const fn highlight() -> GraphHighlight { - GraphHighlight::All + Ok(Self { + spec, + allow, + deny, + exact, + reason: reason.map(Reason::from), + }) + } } -#[derive(Deserialize, PartialEq, Eq, Copy, Clone)] -#[cfg_attr(test, derive(Debug))] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(PartialEq, Eq, Copy, Clone, Default, strum::VariantArray, strum::VariantNames)] +#[strum(serialize_all = "kebab-case")] pub enum GraphHighlight { /// Highlights the path to a duplicate dependency with the fewest number /// of total edges, which tends to make it the best candidate for removing @@ -70,9 +85,12 @@ pub enum GraphHighlight { /// Highlights the path to the duplicate dependency with the lowest version LowestVersion, /// Highlights with all of the other configs + #[default] All, } +crate::enum_deser!(GraphHighlight); + impl GraphHighlight { #[inline] pub(crate) fn simplest(self) -> bool { @@ -132,44 +150,43 @@ impl std::str::FromStr for Checksum { } impl<'de> Deserialize<'de> for Checksum { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - struct HexStrVisitor; + fn deserialize(value: &mut Value<'de>) -> Result { + let val = value.take_string(Some("a sha-256 hex encoded string"))?; - impl<'de> serde::de::Visitor<'de> for HexStrVisitor { - type Value = Checksum; + val.parse().map_err(|err| { + let err = match err { + ChecksumParseError::InvalidLength(len) => { + toml_span::Error::from((toml_span::ErrorKind::Custom(format!("a sha-256 hex encoded string of length 64 but got a string of length '{len}'").into()), value.span)) + } + ChecksumParseError::InvalidValue(c) => toml_span::Error::from((toml_span::ErrorKind::Unexpected(c), value.span)), + }; + err.into() + }) + } +} - fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "a sha-256 hex encoded string") - } +#[cfg(test)] +impl serde::Serialize for Checksum { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut hexs = [0; 64]; - fn visit_str(self, data: &str) -> Result { - data.parse().map_err(|err| match err { - ChecksumParseError::InvalidLength(len) => { - serde::de::Error::invalid_length(len, &"a string with 64 characters") - } - ChecksumParseError::InvalidValue(c) => serde::de::Error::invalid_value( - serde::de::Unexpected::Char(c), - &"a hexadecimal character", - ), - }) - } + const CHARS: &[u8] = b"0123456789abcdef"; - fn visit_borrowed_str(self, data: &'de str) -> Result { - self.visit_str(data) - } + for (i, &byte) in self.0.iter().enumerate() { + let i = i * 2; + hexs[i] = CHARS[(byte >> 4) as usize]; + hexs[i + 1] = CHARS[(byte & 0xf) as usize]; } - deserializer.deserialize_str(HexStrVisitor) + serializer.serialize_str(std::str::from_utf8(&hexs).unwrap()) } } -#[derive(Deserialize, Clone)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))] pub struct BypassPath { /// The crate-relative path to the executable pub path: Spanned, @@ -177,16 +194,28 @@ pub struct BypassPath { pub checksum: Option>, } -#[derive(Deserialize, Clone)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +impl<'de> Deserialize<'de> for BypassPath { + fn deserialize(value: &mut Value<'de>) -> Result { + let mut th = TableHelper::new(value)?; + + let path: Spanned = th.required_s("path")?; + let checksum = th.optional("checksum"); + th.finalize(None)?; + + Ok(Self { + path: path.map(), + checksum, + }) + } +} + +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Bypass { - pub name: Spanned, - pub version: Option, + pub spec: PackageSpec, pub build_script: Option>, /// List of features that, if matched, means the build script/proc macro is /// not actually executed/run - #[serde(default)] pub required_features: Vec>, /// List of glob patterns that are allowed. This is much more loose than /// `allow`, but can be useful in scenarios where things like test suites or @@ -196,22 +225,37 @@ pub struct Bypass { pub allow_globs: Option>>, /// One or more executables that are allowed. If not set all executables are /// allowed. - #[serde(default)] pub allow: Vec, } -#[derive(Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +impl<'de> Deserialize<'de> for Bypass { + fn deserialize(value: &mut Value<'de>) -> Result { + let spec = PackageSpec::deserialize(value)?; + let mut th = TableHelper::new(value)?; + let build_script = th.optional("build-script"); + let required_features = th.optional("required-features").unwrap_or_default(); + let allow_globs = th.optional("allow-globs"); + let allow = th.optional("allow").unwrap_or_default(); + th.finalize(None)?; + + Ok(Self { + spec, + build_script, + required_features, + allow_globs, + allow, + }) + } +} + pub struct BuildConfig { /// List of crates that are allowed to have build scripts. If this is set, /// any crates with a build script that aren't listed here will be banned - pub allow_build_scripts: Option>>, + pub allow_build_scripts: Option>, /// Lint level for when executables are detected within crates with build /// scripts or are proc macros, or are a dependency of either of them - #[serde(default = "crate::lint_deny")] pub executables: LintLevel, /// The lint level for interpreted scripts - #[serde(default = "crate::lint_allow")] pub interpreted: LintLevel, /// List of script extensions that are considered to be executable. These /// are always in addition to the builtin ones. @@ -219,57 +263,96 @@ pub struct BuildConfig { /// The list of allowed executables, by crate pub bypass: Option>, /// If true, enables the built-in glob patterns - #[serde(default)] pub enable_builtin_globs: bool, /// If true, all dependencies of proc macro crates or crates with build /// scripts are also checked for executables/glob patterns - #[serde(default)] pub include_dependencies: bool, /// If true, workspace crates are included - #[serde(default)] pub include_workspace: bool, /// If true, archive files are counted as native executables - #[serde(default)] pub include_archives: bool, } -#[derive(Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +impl<'de> Deserialize<'de> for BuildConfig { + fn deserialize(value: &mut Value<'de>) -> Result { + let mut th = TableHelper::new(value)?; + let allow_build_scripts = th.optional("allow-build-scripts"); + let executables = th.optional("executables").unwrap_or(LintLevel::Deny); + let interpreted = th.optional("interpreted").unwrap_or(LintLevel::Allow); + let script_extensions = th.optional("script-extensions"); + let bypass = th.optional("bypass"); + let enable_builtin_globs = th.optional("enable-builtin-globs").unwrap_or_default(); + let include_dependencies = th.optional("include-dependencies").unwrap_or_default(); + let include_workspace = th.optional("include-workspace").unwrap_or_default(); + let include_archives = th.optional("include-archives").unwrap_or_default(); + th.finalize(None)?; + + Ok(Self { + allow_build_scripts, + executables, + interpreted, + script_extensions, + bypass, + enable_builtin_globs, + include_dependencies, + include_workspace, + include_archives, + }) + } +} + +#[derive(Clone)] +#[cfg_attr(test, derive(Debug, PartialEq, Eq, serde::Serialize))] +pub struct TreeSkipExtended { + pub depth: Option, + /// Reason the tree is being skipped + pub reason: Option, +} + +impl<'de> Deserialize<'de> for TreeSkipExtended { + fn deserialize(value: &mut Value<'de>) -> Result { + let reason = if value.has_key("reason") { + Some(Reason::deserialize(value)?) + } else { + None + }; + + let mut th = TableHelper::new(value)?; + let depth = th.optional("depth"); + th.finalize(None)?; + Ok(Self { depth, reason }) + } +} + +pub type CrateBan = PackageSpecOrExtended; +pub type CrateAllow = PackageSpecOrExtended; +pub type CrateSkip = PackageSpecOrExtended; +pub type TreeSkip = PackageSpecOrExtended; + pub struct Config { /// How to handle multiple versions of the same crate - #[serde(default = "crate::lint_warn")] pub multiple_versions: LintLevel, - #[serde(default)] pub multiple_versions_include_dev: bool, /// How the duplicate graphs are highlighted - #[serde(default = "highlight")] pub highlight: GraphHighlight, /// The crates that will cause us to emit failures - #[serde(default)] pub deny: Vec, /// If specified, means only the listed crates are allowed - #[serde(default)] - pub allow: Vec>, + pub allow: Vec, /// Allows specifying features that are or are not allowed on crates - #[serde(default)] pub features: Vec, /// The default lint level for default features for external, non-workspace /// crates, can be overriden in `features` on a crate by crate basis - #[serde(default)] pub external_default_features: Option>, /// The default lint level for default features for workspace crates, can be /// overriden in `features` on a crate by crate basis - #[serde(default)] pub workspace_default_features: Option>, /// If specified, disregards the crate completely - #[serde(default)] - pub skip: Vec>, + pub skip: Vec, /// If specified, disregards the crate's transitive dependencies /// down to a certain depth - #[serde(default)] - pub skip_tree: Vec>, + pub skip_tree: Vec, /// How to handle wildcard dependencies - #[serde(default = "crate::lint_allow")] pub wildcards: LintLevel, /// Wildcard dependencies defined using path attributes will be treated as /// if they were [`LintLevel::Allow`] for private crates, but other wildcard @@ -277,11 +360,10 @@ pub struct Config { /// /// crates.io does not allow packages to be published with path dependencies, /// thus this rule will not effect public packages. - #[serde(default)] pub allow_wildcard_paths: bool, /// Deprecated and moved into `build.allow_build_scripts`, will eventually /// be removed - pub allow_build_scripts: Option>>, + pub allow_build_scripts: Option>>, /// Options for crates that run at build time pub build: Option, } @@ -307,122 +389,151 @@ impl Default for Config { } } +impl<'de> Deserialize<'de> for Config { + fn deserialize(value: &mut Value<'de>) -> Result { + let mut th = TableHelper::new(value)?; + + let multiple_versions = th.optional("multiple-versions").unwrap_or(LintLevel::Warn); + let multiple_versions_include_dev = th + .optional("multiple-versions-include-dev") + .unwrap_or_default(); + let highlight = th.optional("highlight").unwrap_or_default(); + let deny = th.optional("deny").unwrap_or_default(); + let allow = th.optional("allow").unwrap_or_default(); + let features = th.optional("features").unwrap_or_default(); + let external_default_features = th.optional("external-default-features"); + let workspace_default_features = th.optional("workspace-default-features"); + let skip = th.optional("skip").unwrap_or_default(); + let skip_tree = th.optional("skip-tree").unwrap_or_default(); + let wildcards = th.optional("wildcards").unwrap_or(LintLevel::Allow); + let allow_wildcard_paths = th.optional("allow-wildcard-paths").unwrap_or_default(); + let allow_build_scripts = th.optional("allow-build-scripts"); + let build = th.optional("build"); + + th.finalize(None)?; + + Ok(Self { + multiple_versions, + multiple_versions_include_dev, + highlight, + deny, + allow, + features, + external_default_features, + workspace_default_features, + skip, + skip_tree, + wildcards, + allow_wildcard_paths, + allow_build_scripts, + build, + }) + } +} + impl crate::cfg::UnvalidatedConfig for Config { type ValidCfg = ValidConfig; - fn validate( - self, - cfg_file: FileId, - files: &mut crate::diag::Files, - diags: &mut Vec, - ) -> Self::ValidCfg { - let from = |s: Spanned| { - Skrate::new( - KrateId { - name: s.value.name, - version: s.value.version, - }, - s.span, - ) - }; + fn validate(self, mut ctx: ValidationContext<'_>) -> Self::ValidCfg { + let cfg_id = ctx.cfg_id; - let (deny_multiple_versions, deny): (Vec<_>, Vec<_>) = - self.deny.into_iter().partition(|kb| { - kb.deny_multiple_versions - .as_ref() - .map_or(false, |spanned| spanned.value) - }); + let (denied_multiple_versions, denied) = { + let mut dmulti = Vec::new(); + let mut denied = Vec::new(); + for deny_spec in self.deny { + let spec = deny_spec.spec; - let denied: Vec<_> = deny - .into_iter() - .map(|cb| KrateBan { - id: Skrate::new( - KrateId { - name: cb.name.value, - version: cb.version, - }, - cb.name.span, - ), - wrappers: cb.wrappers.map(|spanned| spanned.value), - }) - .collect(); + let inner = if let Some(extended) = deny_spec.inner { + let dmv = extended.deny_multiple_versions; + let wrappers = extended.wrappers; - let denied_multiple_versions: Vec<_> = deny_multiple_versions - .into_iter() - .map(|cb| { - let wrappers = cb.wrappers.filter(|spanned| !spanned.value.is_empty()); - if let Some(wrappers) = wrappers { - // cb.multiple_versions is guaranteed to be Some(_) by the - // earlier call to `partition` - let multiple_versions = cb.deny_multiple_versions.unwrap(); - diags.push( - Diagnostic::error() - .with_message( - "a crate ban was specified with both `wrappers` and `multiple-versions`", - ) - .with_labels(vec![ - Label::secondary(cfg_file, wrappers.span) - .with_message("has one or more `wrappers`"), - Label::secondary(cfg_file, multiple_versions.span) - .with_message("has `multiple-versions` set to true"), - ]), - ); - } + if let Some((dmv, wrappers)) = dmv.as_ref().zip(wrappers.as_ref()) { + if dmv.value && !wrappers.value.is_empty() { + ctx.push( + Diagnostic::error() + .with_message( + "a crate ban was specified with both `wrappers` and `deny-multiple-versions` = true", + ) + .with_labels(vec![ + Label::secondary(cfg_id, wrappers.span) + .with_message(format!("has {} `wrappers`", wrappers.value.len())), + Label::secondary(cfg_id, dmv.span) + .with_message("has `deny-multiple-versions` set to true"), + ]), + ); + continue; + } + } - Skrate::new( - KrateId { - name: cb.name.value, - version: cb.version, - }, - cb.name.span, - ) - }) - .collect(); + if dmv.map_or(false, |d| d.value) { + dmulti.push(spec); + continue; + } + + Some(KrateBan { + wrappers: wrappers.map(|sv| sv.value), + reason: extended.reason, + use_instead: extended.use_instead, + }) + } else { + None + }; + + denied.push(ValidKrateBan { spec, inner }); + } + + (dmulti, denied) + }; - let allowed: Vec<_> = self.allow.into_iter().map(from).collect(); - let skipped: Vec<_> = self.skip.into_iter().map(from).collect(); + let allowed = self.allow; + let skipped = self.skip; - let dupe_crate_diag = |first: (&Skrate, &str), second: (&Skrate, &str)| -> Diagnostic { - Diagnostic::error() + let dupe_crate_diag = |ctx: &mut ValidationContext<'_>, + first: (&PackageSpec, &str), + second: (&PackageSpec, &str)| { + let diag = Diagnostic::error() .with_message(format!( "a crate was specified in both `{}` and `{}`", second.1, first.1 )) .with_labels(vec![ - Label::secondary(cfg_file, first.0.span.clone()) + Label::secondary(cfg_id, first.0.name.span) .with_message(format!("marked as `{}`", first.1)), - Label::secondary(cfg_file, second.0.span.clone()) + Label::secondary(cfg_id, second.0.name.span) .with_message(format!("marked as `{}`", second.1)), - ]) + ]); + + ctx.push(diag); }; - let dupe_feature_diag = |krate: &Skrate, + let dupe_feature_diag = |ctx: &mut ValidationContext<'_>, + krate: &PackageSpec, allow: &Spanned, - deny: &Spanned| - -> Diagnostic { - Diagnostic::error() + deny: &Spanned| { + let diag = Diagnostic::error() .with_message("a crate feature was specified as both allowed and denied") .with_labels(vec![ - Label::primary(cfg_file, krate.span.clone()).with_message("crate ban entry"), - Label::secondary(cfg_file, allow.span.clone()) - .with_message("marked as `allow`"), - Label::secondary(cfg_file, deny.span.clone()).with_message("marked as `deny`"), - ]) + Label::primary(cfg_id, krate.name.span).with_message("crate ban entry"), + Label::secondary(cfg_id, allow.span).with_message("marked as `allow`"), + Label::secondary(cfg_id, deny.span).with_message("marked as `deny`"), + ]); + + ctx.push(diag); }; for d in &denied { - if let Some(dupe) = exact_match(&allowed, &d.id.value) { - diags.push(dupe_crate_diag((&d.id, "deny"), (dupe, "allow"))); + if let Some(dupe) = exact_match(&allowed, &d.spec) { + dupe_crate_diag(&mut ctx, (&d.spec, "deny"), (dupe, "allow")); } - if let Some(dupe) = exact_match(&skipped, &d.id.value) { - diags.push(dupe_crate_diag((&d.id, "deny"), (dupe, "skip"))); + if let Some(dupe) = exact_match(&skipped, &d.spec) { + dupe_crate_diag(&mut ctx, (&d.spec, "deny"), (dupe, "skip")); } } for all in &allowed { - if let Some(dupe) = exact_match(&skipped, &all.value) { - diags.push(dupe_crate_diag((all, "allow"), (dupe, "skip"))); + if let Some(dupe) = exact_match(&skipped, &all.spec) { + dupe_crate_diag(&mut ctx, (&all.spec, "allow"), (dupe, "skip")); } } @@ -431,27 +542,21 @@ impl crate::cfg::UnvalidatedConfig for Config { .features .into_iter() .map(|cf| { - let id = Skrate::new( - KrateId { - name: cf.name.value, - version: cf.version, - }, - cf.name.span, - ); - + let spec = cf.spec; for allowed in &cf.allow.value { if let Some(denied) = cf.deny.iter().find(|df| df.value == allowed.value) { - diags.push(dupe_feature_diag(&id, allowed, denied)); + dupe_feature_diag(&mut ctx, &spec, allowed, denied); } } - KrateFeatures { - id, + ValidKrateFeatures { + spec, features: Features { allow: cf.allow, deny: cf.deny, exact: cf.exact, }, + reason: cf.reason.map(Reason::from), } }) .collect(); @@ -463,24 +568,23 @@ impl crate::cfg::UnvalidatedConfig for Config { for ext in extensions { // This top level config should only be extensions, not glob patterns if !ext.value.is_ascii() { - diags.push( + ctx.diagnostics.push( Diagnostic::error() .with_message("non-ascii file extension provided") - .with_labels(vec![Label::primary(cfg_file, ext.span.clone()) + .with_labels(vec![Label::primary(ctx.cfg_id, ext.span) .with_message("invalid extension")]), ); continue; } if let Some(i) = ext.value.chars().position(|c| !c.is_ascii_alphanumeric()) { - diags.push( + ctx.diagnostics.push( Diagnostic::error() .with_message("invalid file extension provided") .with_labels(vec![ - Label::primary(cfg_file, ext.span.clone()) - .with_message("extension"), + Label::primary(ctx.cfg_id, ext.span).with_message("extension"), Label::secondary( - cfg_file, + ctx.cfg_id, ext.span.start + i..ext.span.start + i + 1, ) .with_message("invalid character"), @@ -494,10 +598,10 @@ impl crate::cfg::UnvalidatedConfig for Config { gsb.add(glob, GlobPattern::User(ext)); } Err(err) => { - diags.push( + ctx.diagnostics.push( Diagnostic::error() .with_message(format!("invalid glob pattern: {err}")) - .with_labels(vec![Label::primary(cfg_file, ext.span.clone()) + .with_labels(vec![Label::primary(ctx.cfg_id, ext.span) .with_message("extension")]), ); } @@ -506,11 +610,11 @@ impl crate::cfg::UnvalidatedConfig for Config { } if bc.enable_builtin_globs { - load_builtin_globs(files, &mut gsb); + load_builtin_globs(ctx.files, &mut gsb); } let script_extensions = gsb.build().unwrap_or_else(|err| { - diags + ctx.diagnostics .push(Diagnostic::error().with_message(format!( "failed to build script extensions glob set: {err}" ))); @@ -521,6 +625,8 @@ impl crate::cfg::UnvalidatedConfig for Config { let mut aex = Vec::new(); for aexe in aexes { + let spec = aexe.spec; + let allow_globs = if let Some(allow_globs) = aexe.allow_globs { let mut gsb = GlobsetBuilder::new(); @@ -530,13 +636,10 @@ impl crate::cfg::UnvalidatedConfig for Config { gsb.add(glob, GlobPattern::User(ag)); } Err(err) => { - diags.push( + ctx.diagnostics.push( Diagnostic::error() .with_message(format!("invalid glob pattern: {err}")) - .with_labels(vec![Label::primary( - cfg_file, - ag.span.clone(), - )]), + .with_labels(vec![Label::primary(ctx.cfg_id, ag.span)]), ); } } @@ -545,9 +648,10 @@ impl crate::cfg::UnvalidatedConfig for Config { match gsb.build() { Ok(set) => Some(set), Err(err) => { - diags.push(Diagnostic::error().with_message(format!( - "failed to build script extensions glob set: {err}" - ))); + ctx.diagnostics + .push(Diagnostic::error().with_message(format!( + "failed to build script extensions glob set: {err}" + ))); None } } @@ -559,13 +663,10 @@ impl crate::cfg::UnvalidatedConfig for Config { allow.retain(|ae| { let keep = ae.path.value.is_relative(); if !keep { - diags.push( + ctx.diagnostics.push( Diagnostic::error() .with_message("absolute paths are not allowed") - .with_labels(vec![Label::primary( - cfg_file, - ae.path.span.clone(), - )]), + .with_labels(vec![Label::primary(ctx.cfg_id, ae.path.span)]), ); } @@ -574,8 +675,7 @@ impl crate::cfg::UnvalidatedConfig for Config { allow.sort_by(|a, b| a.path.value.cmp(&b.path.value)); aex.push(ValidBypass { - name: aexe.name, - version: aexe.version, + spec, build_script: aexe.build_script, required_features: aexe.required_features, allow, @@ -598,14 +698,14 @@ impl crate::cfg::UnvalidatedConfig for Config { include_archives: bc.include_archives, interpreted: bc.interpreted, }) - } else if let Some(allow_build_scripts) = self.allow_build_scripts { - diags.push(Diagnostic::warning() + } else if let Some(abs) = self.allow_build_scripts { + ctx.push(Diagnostic::warning() .with_message("[bans.allow-build-scripts] has been deprecated in favor of [bans.build.allow-build-scripts], this will become an error in the future") .with_labels(vec![ - Label::primary(cfg_file, allow_build_scripts.span.clone()) + Label::primary(ctx.cfg_id, abs.span) ])); Some(ValidBuildConfig { - allow_build_scripts: Some(allow_build_scripts), + allow_build_scripts: Some(abs.value), executables: LintLevel::Allow, script_extensions: ValidGlobSet::default(), bypass: Vec::new(), @@ -619,7 +719,7 @@ impl crate::cfg::UnvalidatedConfig for Config { }; ValidConfig { - file_id: cfg_file, + file_id: ctx.cfg_id, multiple_versions: self.multiple_versions, multiple_versions_include_dev: self.multiple_versions_include_dev, highlight: self.highlight, @@ -632,11 +732,7 @@ impl crate::cfg::UnvalidatedConfig for Config { skipped, wildcards: self.wildcards, allow_wildcard_paths: self.allow_wildcard_paths, - tree_skipped: self - .skip_tree - .into_iter() - .map(crate::Spanned::from) - .collect(), + tree_skipped: self.skip_tree, build, } } @@ -645,15 +741,14 @@ impl crate::cfg::UnvalidatedConfig for Config { fn load_builtin_globs(files: &mut crate::diag::Files, gsb: &mut GlobsetBuilder) { const BUILTIN_GLOBS: &str = include_str!("builtin_globs.toml"); - #[derive(Deserialize)] - struct Builtin { - globs: Vec>, - } + let mut biv = toml_span::parse(BUILTIN_GLOBS).expect("failed to parse builtin_globs.toml"); + let mut th = TableHelper::new(&mut biv).expect("builtin_globs.toml does not have a root table"); + + let globs: Vec> = th.required("globs").expect("failed to find 'globs' array"); - let bi: Builtin = toml::from_str(BUILTIN_GLOBS).expect("failed to parse builtin_globs.toml"); let file_id = files.add("builtin_globs.toml", BUILTIN_GLOBS.to_owned()); - for glob in bi.globs { + for glob in globs { gsb.add( globset::Glob::new(&glob.value).expect("failed to parse builtin glob"), GlobPattern::Builtin((glob, file_id)), @@ -662,31 +757,38 @@ fn load_builtin_globs(files: &mut crate::diag::Files, gsb: &mut GlobsetBuilder) } #[inline] -pub(crate) fn exact_match<'v>(arr: &'v [Skrate], id: &'_ KrateId) -> Option<&'v Skrate> { - arr.iter().find(|sid| *sid == id) +pub(crate) fn exact_match<'v, T>( + arr: &'v [PackageSpecOrExtended], + id: &'_ PackageSpec, +) -> Option<&'v PackageSpec> { + arr.iter() + .find_map(|sid| (&sid.spec == id).then_some(&sid.spec)) } -pub(crate) type Skrate = Spanned; - -#[cfg_attr(test, derive(Debug))] +#[cfg_attr(test, derive(serde::Serialize))] pub(crate) struct KrateBan { - pub id: Skrate, pub wrappers: Option>>, + pub reason: Option, + pub use_instead: Option>, } -#[cfg_attr(test, derive(Debug))] +pub(crate) type ValidKrateBan = PackageSpecOrExtended; + +#[cfg_attr(test, derive(serde::Serialize))] pub struct Features { pub allow: Spanned>>, pub deny: Vec>, pub exact: Spanned, } -#[cfg_attr(test, derive(Debug))] -pub(crate) struct KrateFeatures { - pub id: Skrate, +#[cfg_attr(test, derive(serde::Serialize))] +pub(crate) struct ValidKrateFeatures { + pub spec: PackageSpec, pub features: Features, + pub reason: Option, } +#[cfg_attr(test, derive(serde::Serialize))] pub enum GlobPattern { Builtin((Spanned, FileId)), User(Spanned), @@ -727,6 +829,21 @@ pub struct ValidGlobSet { pub(crate) patterns: Vec, } +#[cfg(test)] +impl serde::Serialize for ValidGlobSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_seq(self.patterns.iter().filter_map(|gp| { + let GlobPattern::User(gp) = gp else { + return None; + }; + Some(gp) + })) + } +} + impl Default for ValidGlobSet { fn default() -> Self { Self { @@ -748,17 +865,18 @@ impl ValidGlobSet { } } +#[cfg_attr(test, derive(serde::Serialize))] pub struct ValidBypass { - pub name: Spanned, - pub version: Option, + pub spec: PackageSpec, pub build_script: Option>, pub required_features: Vec>, pub allow_globs: Option, pub allow: Vec, } +#[cfg_attr(test, derive(serde::Serialize))] pub struct ValidBuildConfig { - pub allow_build_scripts: Option>>, + pub allow_build_scripts: Option>, pub executables: LintLevel, pub script_extensions: ValidGlobSet, pub bypass: Vec, @@ -768,19 +886,23 @@ pub struct ValidBuildConfig { pub interpreted: LintLevel, } +pub type ValidTreeSkip = PackageSpecOrExtended; +pub type SpecAndReason = PackageSpecOrExtended; + +#[cfg_attr(test, derive(serde::Serialize))] pub struct ValidConfig { pub file_id: FileId, pub multiple_versions: LintLevel, pub multiple_versions_include_dev: bool, pub highlight: GraphHighlight, - pub(crate) denied: Vec, - pub(crate) denied_multiple_versions: Vec, - pub(crate) allowed: Vec, - pub(crate) features: Vec, + pub(crate) denied: Vec, + pub(crate) denied_multiple_versions: Vec, + pub(crate) allowed: Vec, + pub(crate) features: Vec, pub external_default_features: Option>, pub workspace_default_features: Option>, - pub(crate) skipped: Vec, - pub(crate) tree_skipped: Vec>, + pub(crate) skipped: Vec, + pub(crate) tree_skipped: Vec, pub wildcards: LintLevel, pub allow_wildcard_paths: bool, pub build: Option, @@ -789,140 +911,28 @@ pub struct ValidConfig { #[cfg(test)] mod test { use super::*; - use crate::cfg::{test::*, *}; - - macro_rules! kid { - ($name:expr) => { - KrateId { - name: String::from($name), - version: None, - } - }; - - ($name:expr, $vs:expr) => { - KrateId { - name: String::from($name), - version: Some($vs.parse::().unwrap().into()), - } - }; - } - - impl PartialEq for KrateBan { - fn eq(&self, o: &KrateId) -> bool { - &self.id.value == o - } - } + use crate::test_utils::ConfigData; #[test] fn deserializes_ban_cfg() { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] struct Bans { bans: Config, } - let mut cd: ConfigData = load("tests/cfg/bans.toml"); - - let mut diags = Vec::new(); - let validated = cd.config.bans.validate(cd.id, &mut cd.files, &mut diags); - assert!(diags.is_empty()); - - assert_eq!(validated.file_id, cd.id); - assert_eq!(validated.multiple_versions, LintLevel::Deny); - - assert_eq!(validated.wildcards, LintLevel::Deny); - assert!(validated.allow_wildcard_paths); - - assert_eq!(validated.highlight, GraphHighlight::SimplestPath); - assert_eq!( - validated.external_default_features.unwrap().value, - LintLevel::Deny - ); - assert_eq!( - validated.workspace_default_features.unwrap().value, - LintLevel::Warn - ); - - assert_eq!( - validated.allowed, - vec![kid!("all-versionsa"), kid!("specific-versiona", "<0.1.1")] - ); - - assert_eq!( - validated.denied, - vec![kid!("all-versionsd"), kid!("specific-versiond", "=0.1.9")] - ); - - assert_eq!(validated.skipped, vec![kid!("rand", "=0.6.5")]); - - assert_eq!( - validated.tree_skipped, - vec![TreeSkip { - id: CrateId { - name: "blah".to_owned(), - version: None, - }, - depth: Some(20), - }] - ); - - let kf = &validated.features[0]; - - assert_eq!(kf.id, kid!("featured-krate", "1.0")); - assert_eq!(kf.features.deny[0].value, "bad-feature"); - assert_eq!(kf.features.allow.value[0].value, "good-feature"); - assert!(kf.features.exact.value); + impl<'de> toml_span::Deserialize<'de> for Bans { + fn deserialize( + value: &mut toml_span::value::Value<'de>, + ) -> Result { + let mut th = toml_span::de_helpers::TableHelper::new(value)?; + let bans = th.required("bans").unwrap(); + th.finalize(None)?; + Ok(Self { bans }) + } + } - let mut bc = validated.build.expect("expected build config"); - assert_eq!( - bc.allow_build_scripts.unwrap().value.pop().unwrap().name, - "all-versionsa" - ); - assert_eq!(bc.executables, LintLevel::Warn); - assert_eq!(bc.interpreted, LintLevel::Deny); + let cd = ConfigData::::load("tests/cfg/bans.toml"); + let validated = cd.validate(|b| b.bans); - assert!(bc.script_extensions.patterns.iter().any(|gp| { - let GlobPattern::User(gp) = gp else { - return false; - }; - gp.value == "cs" - })); - assert!(bc.script_extensions.patterns.iter().any(|gp| { - let GlobPattern::Builtin(gp) = gp else { - return false; - }; - gp.0.value == "*.py" - })); - assert!(bc.include_dependencies); - assert!(bc.include_workspace); - assert!(bc.include_archives); - - let mut bypass = bc.bypass.pop().unwrap(); - assert_eq!(bypass.name.value, "allversionsa"); - assert!(bypass.version.is_none()); - assert_eq!( - bypass.build_script.unwrap().value, - "5392f0e58ad06e089462d93304dfe82337acbbefb87a0749a7dc2ed32af04af7" - .parse() - .unwrap() - ); - assert_eq!( - bypass.required_features.pop().unwrap().value, - "feature-used-at-build-time" - ); - assert!(bypass.allow_globs.unwrap().patterns.iter().any(|gp| { - let GlobPattern::User(gp) = gp else { - return false; - }; - gp.value == "scripts/*.cs" - })); - let ba = bypass.allow.pop().unwrap(); - assert_eq!(ba.path.value, "bin/x86_64-linux"); - assert_eq!( - ba.checksum.unwrap().value, - "5392f0e58ad06e089462d93304dfe82337acbbefb87a0749a7dc2ed32af04af7" - .parse() - .unwrap() - ); + insta::assert_json_snapshot!(validated); } } diff --git a/src/bans/diags.rs b/src/bans/diags.rs index 72e17ea71..65bc26cc5 100644 --- a/src/bans/diags.rs +++ b/src/bans/diags.rs @@ -1,7 +1,7 @@ use std::fmt; use crate::{ - bans::{cfg, KrateId}, + bans::{cfg, SpecAndReason}, diag::{ CfgCoord, Check, Diag, Diagnostic, FileId, GraphNode, KrateCoord, Label, Pack, Severity, }, @@ -59,9 +59,34 @@ impl From for String { } } +impl SpecAndReason { + pub(crate) fn to_labels(&self, spec_msg: Option<&str>) -> Vec