├── .github └── workflows │ ├── audit.yml │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── bin │ ├── cargo-deadlinks.rs │ ├── deadlinks.rs │ └── shared.rs ├── check.rs ├── lib.rs └── parse.rs └── tests ├── broken_links.rs ├── broken_links ├── Cargo.toml ├── hardcoded-target │ └── index.html └── src │ └── lib.rs ├── cli_args ├── Cargo.toml └── src │ └── lib.rs ├── html ├── anchors.html ├── index.html ├── missing_index │ └── .gitkeep └── range.html ├── non_existent_http_link.rs ├── non_existent_http_link ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── renamed_package ├── Cargo.toml └── src │ └── main.rs ├── simple_project.rs ├── simple_project ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── working_http_check.rs ├── working_http_check ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs └── workspace ├── Cargo.toml ├── a ├── Cargo.toml └── src │ └── lib.rs └── b ├── Cargo.toml └── src └── lib.rs /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | push: 5 | pull_request: 6 | paths: 7 | - "**/Cargo.toml" 8 | - "**/Cargo.lock" 9 | schedule: 10 | - cron: "0 0 * * *" 11 | 12 | jobs: 13 | security_audit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/audit-check@v1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | RUST_BACKTRACE: 1 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - id: install 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: install 23 | args: cargo-sweep 24 | 25 | - name: Cache directories 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.cargo/registry 30 | ~/.cargo/bin 31 | ~/.cargo/git 32 | key: cargo-test-dirs-${{ hashFiles('**/Cargo.lock') }} 33 | restore-keys: cargo-test-dirs- 34 | 35 | - name: Cache build 36 | uses: actions/cache@v2 37 | with: 38 | path: target 39 | key: cargo-test-build-${{ steps.install.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }} 40 | restore-keys: | 41 | cargo-test-build-${{ steps.install.outputs.rustc_hash }}- 42 | cargo-test-build- 43 | 44 | - name: Register artifacts 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: sweep 48 | args: --stamp 49 | 50 | - name: Build 51 | run: cargo build 52 | 53 | - name: Test 54 | run: cargo test 55 | 56 | - name: Clean unused artifacts 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: sweep 60 | args: --file 61 | 62 | fmt: 63 | name: Rustfmt 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - uses: actions/checkout@master 68 | - id: install 69 | uses: actions-rs/toolchain@v1 70 | with: 71 | toolchain: stable 72 | override: true 73 | components: rustfmt 74 | 75 | - uses: actions-rs/cargo@v1 76 | with: 77 | command: fmt 78 | args: -- --check 79 | 80 | clippy: 81 | name: Clippy 82 | runs-on: ubuntu-latest 83 | 84 | steps: 85 | - uses: actions/checkout@master 86 | - id: install 87 | uses: actions-rs/toolchain@v1 88 | with: 89 | toolchain: stable 90 | override: true 91 | components: clippy 92 | - uses: actions-rs/cargo@v1 93 | with: 94 | command: install 95 | args: cargo-sweep 96 | 97 | - name: Cache directories 98 | uses: actions/cache@v2 99 | with: 100 | path: | 101 | ~/.cargo/registry 102 | ~/.cargo/bin 103 | ~/.cargo/git 104 | key: cargo-clippy-dirs-${{ hashFiles('**/Cargo.lock') }} 105 | restore-keys: cargo-clippy-dirs- 106 | - name: Cache build 107 | uses: actions/cache@v2 108 | with: 109 | path: target 110 | key: cargo-clippy-${{ steps.install.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }} 111 | restore-keys: | 112 | cargo-clippy-${{ steps.install.outputs.rustc_hash }}- 113 | cargo-clippy- 114 | 115 | - name: Register artifacts 116 | uses: actions-rs/cargo@v1 117 | with: 118 | command: sweep 119 | args: --stamp 120 | 121 | - uses: actions-rs/cargo@v1 122 | with: 123 | command: clippy 124 | args: -- -D warnings 125 | 126 | - name: Clean unused artifacts 127 | uses: actions-rs/cargo@v1 128 | with: 129 | command: sweep 130 | args: --file 131 | 132 | msrv: 133 | name: Check MSRV 134 | runs-on: ubuntu-latest 135 | steps: 136 | - uses: actions/checkout@master 137 | - id: install 138 | uses: actions-rs/toolchain@v1 139 | with: 140 | toolchain: 1.46.0 141 | override: true 142 | - uses: actions-rs/cargo@v1 143 | with: 144 | toolchain: stable 145 | command: install 146 | args: cargo-sweep 147 | 148 | - name: Cache directories 149 | uses: actions/cache@v2 150 | with: 151 | path: | 152 | ~/.cargo/registry 153 | ~/.cargo/bin 154 | ~/.cargo/git 155 | key: cargo-test-dirs-${{ hashFiles('**/Cargo.lock') }} 156 | restore-keys: cargo-test-dirs- 157 | 158 | - name: Cache build 159 | uses: actions/cache@v2 160 | with: 161 | path: target 162 | key: cargo-test-build-${{ steps.install.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }} 163 | restore-keys: | 164 | cargo-test-build-${{ steps.install.outputs.rustc_hash }}- 165 | cargo-test-build- 166 | 167 | - name: Register artifacts 168 | uses: actions-rs/cargo@v1 169 | with: 170 | command: sweep 171 | args: --stamp 172 | 173 | - name: Check build succeeds 174 | run: cargo check 175 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish deadlinks for ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | name: [linux, windows, macos] 15 | 16 | include: 17 | - name: linux 18 | os: ubuntu-latest 19 | suffix: "" 20 | asset_suffix: -linux 21 | cargo_args: --target x86_64-unknown-linux-musl 22 | - name: windows 23 | os: windows-latest 24 | suffix: .exe 25 | asset_suffix: -windows 26 | - name: macos 27 | os: macos-latest 28 | suffix: "" 29 | asset_suffix: -macos 30 | 31 | steps: 32 | - uses: actions/checkout@v1 33 | 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | profile: minimal 37 | toolchain: stable 38 | 39 | - name: Install MUSL target 40 | if: ${{ matrix.name == 'linux' }} 41 | run: rustup target add x86_64-unknown-linux-musl && sudo apt update && sudo apt install musl-tools 42 | 43 | - name: Build 44 | run: cargo build --release ${{ matrix.cargo_args }} 45 | 46 | - name: Make build directories consistent 47 | if: ${{ matrix.name == 'linux' }} 48 | run: mkdir -p target/release && mv target/x86_64-unknown-linux-musl/release/{cargo-,}deadlinks target/release 49 | 50 | - name: Upload `deadlinks` binaries 51 | uses: svenstaro/upload-release-action@v1-release 52 | with: 53 | repo_token: ${{ secrets.GITHUB_TOKEN }} 54 | file: target/release/deadlinks${{ matrix.suffix }} 55 | asset_name: deadlinks${{ matrix.asset_suffix }} 56 | tag: ${{ github.ref }} 57 | 58 | - name: Upload `cargo-deadlinks` binaries 59 | uses: svenstaro/upload-release-action@v1-release 60 | with: 61 | repo_token: ${{ secrets.GITHUB_TOKEN }} 62 | file: target/release/cargo-deadlinks${{ matrix.suffix }} 63 | asset_name: cargo-deadlinks${{ matrix.asset_suffix }} 64 | tag: ${{ github.ref }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/working_http_check/target2/ 2 | tests/simple_project/target2/ 3 | target 4 | Cargo.lock 5 | !/Cargo.lock 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## NEXT (UNRELEASED) 4 | 5 | 6 | ## 0.8.1 (2021-10-12) 7 | 8 | #### Changed 9 | 10 | * Updated many dependencies. Deadlinks no longer has any dependencies that fail `cargo audit`. [PR#153] 11 | 12 | #### Fixed 13 | 14 | * Tests now pass even if the project directory is not named "cargo-deadlinks". [PR#149] 15 | 16 | [PR#153]: https://github.com/deadlinks/cargo-deadlinks/pull/153 17 | [PR#149]: https://github.com/deadlinks/cargo-deadlinks/pull/149 18 | 19 | 20 | ## 0.8.0 (2020-01-17) 21 | 22 | #### Added 23 | 24 | * `cargo deadlinks` and `deadlinks` now take a `--forbid-http` argument which gives an error if any HTTP links are present. 25 | This can be useful for ensuring all documentation is viewable offline. [PR#138] 26 | 27 | #### Changed 28 | 29 | * `CheckError` now has an `HttpForbidden` variant. [PR#138] 30 | * The `check_http` field of `CheckContext` is now an enum instead of a boolean [PR#138] 31 | * ureq has been upgraded to 2.0. This affects the public `CheckError` API, but should otherwise have no user-facing impact. [PR#134] 32 | 33 | [PR#134]: https://github.com/deadlinks/cargo-deadlinks/pull/134 34 | [PR#138]: https://github.com/deadlinks/cargo-deadlinks/pull/138 35 | 36 | 37 | ## 0.7.2 (2020-01-09) 38 | 39 | #### Fixed 40 | 41 | * When a website gives 405 Method Not Supported for HEAD requests, fall back to GET. In particular, 42 | this no longer marks all links to play.rust-lang.org as broken. [PR#136] 43 | * URL-encoded fragments, like `#%E2%80%A0`, are now decoded. [PR#141] 44 | 45 | [PR#136]: https://github.com/deadlinks/cargo-deadlinks/pull/136 46 | [PR#141]: https://github.com/deadlinks/cargo-deadlinks/pull/141 47 | 48 | #### Changed 49 | 50 | * Give a warning when HTTP links are present but `--check-http` wasn't passed. Previously this was only a DEBUG message. 51 | Note that this still requires opting-in to warnings with `RUST_LOG=warn`. [PR#137] 52 | 53 | [PR#137]: https://github.com/deadlinks/cargo-deadlinks/pull/137 54 | 55 | 56 | ## 0.7.1 (2020-12-18) 57 | 58 | #### Fixed 59 | 60 | * HTML `` redirects are now followed. 61 | 62 | 63 | ## 0.7.0 (2020-12-06) 64 | 65 | #### Added 66 | 67 | * `cargo deadlinks` now takes a `--cargo-dir` argument, allowing you to check projects other than the current directory. 68 | This is most useful for developing deadlinks itself, but might be helpful for other use cases. [PR#119] 69 | * `cargo deadlinks` can now check for broken [intra-doc links] based on heuristics. 70 | This feature is still experimental and may have bugs; in particular, only 71 | links with backticks (i.e. generated as ``) are currently found. 72 | You can opt in with `--check-intra-doc-links`. 73 | `deadlinks` has not been changed. [PR#126] [PR#128] 74 | 75 | [intra-doc links]: https://doc.rust-lang.org/rustdoc/linking-to-items-by-name.html 76 | [PR#128]: https://github.com/deadlinks/cargo-deadlinks/pull/128 77 | [PR#126]: https://github.com/deadlinks/cargo-deadlinks/pull/126 78 | [PR#119]: https://github.com/deadlinks/cargo-deadlinks/pull/119 79 | 80 | #### Changed 81 | 82 | * `walk_dir` now takes `&CheckContext`, not `CheckContext`. [PR#118] 83 | * `CheckError` now has a new `IntraDocLink` variant. [PR#126] 84 | * `parse_html_file` has been removed. Instead, use `parse_a_hrefs` or `broken_intra_doc_links` (or both). [PR#126] 85 | * `Link::File` now stores a `PathBuf`, not a `String`. [PR#127] 86 | * `print_shortened` has been removed; using `Display` directly is recommended instead. [PR#127] 87 | In particular, it's no longer possible to shorten files without going 88 | through `unavailable_urls`. If you were using this API, please let me know 89 | so I can help design an API that fits your use case; the previous one was a 90 | maintenance burden. 91 | 92 | #### Fixed 93 | 94 | * Fragment errors are now shortened to use the directory being checked as the base, the same as normal 'file not found errors'. [PR#127] 95 | * 307 and 308 redirects are now followed. Previously, they would erroneously be reported as an error. [PR#129] 96 | 97 | [PR#118]: https://github.com/deadlinks/cargo-deadlinks/pull/118 98 | [PR#127]: https://github.com/deadlinks/cargo-deadlinks/pull/127 99 | [PR#129]: https://github.com/deadlinks/cargo-deadlinks/pull/129 100 | 101 | 102 | ## 0.6.2 (2020-11-27) 103 | 104 | #### Added 105 | 106 | * `cargo-deadlinks` now allows passing arguments to `cargo doc`, using `cargo deadlinks -- `. [PR#116] 107 | * `deadlinks` now allows specifying multiple directories to check. [PR#116] 108 | 109 | #### Fixed 110 | 111 | * Warnings from cargo are no longer silenced when documenting. [PR#114] 112 | * `cargo deadlinks` no longer ignores all directories on Windows. [PR#121] 113 | 114 | #### Changes 115 | 116 | * Argument parsing now uses `pico-args`, not `docopt`. [PR#116] 117 | * Running `cargo-deadlinks` (not `cargo deadlinks`) now gives a better error message. [PR#116] 118 | * Both binaries now print the name of the binary when passed `--version`. [PR#116] 119 | 120 | [PR#114]: https://github.com/deadlinks/cargo-deadlinks/pull/114 121 | [PR#116]: https://github.com/deadlinks/cargo-deadlinks/pull/116 122 | [PR#121]: https://github.com/deadlinks/cargo-deadlinks/pull/121 123 | 124 | 125 | ## 0.6.1 (2020-11-23) 126 | 127 | #### Added 128 | 129 | * `--ignore-fragments` CLI parameter to disable URL fagment checking. [PR#108] 130 | 131 | #### Fixed 132 | 133 | * Empty fragments are no longer treated as broken links. This allows using `deadlinks` with unsafe functions, which have a generated fragment URL from rustdoc. [PR#109] 134 | 135 | [PR#108]: https://github.com/deadlinks/cargo-deadlinks/pull/108 136 | [PR#109]: https://github.com/deadlinks/cargo-deadlinks/pull/109 137 | 138 | 139 | ## 0.6.0 (2020-11-19) 140 | 141 | #### Added 142 | 143 | * `RUST_LOG` is now read, and controls logging. [PR#100] 144 | * There is now a separate `deadlinks` binary which doesn't depend on cargo in any way. [PR#87] 145 | * `CheckContext` now implements `Default`. [PR#101] 146 | * `cargo deadlinks` will now run `cargo doc` automatically. You can opt-out of this behavior with `--no-build`. [PR#102] 147 | 148 | #### Changes 149 | 150 | * Errors are now printed to stdout, not stderr. [PR#100] 151 | * Logging now follows the standard `env_logger` format. [PR#100] 152 | * `--debug` and `--verbose` are deprecated in favor of `RUST_LOG`. [PR#100] 153 | * Published Linux binaries are now built against musl libc, not glibc. This allows running deadlinks in an alpine docker container. [PR#103] 154 | 155 | #### Fixes 156 | 157 | * `doc = false` is now taken into account when running `cargo deadlinks`. It will still be ignored when running with `--no-build`. [PR#102] 158 | * `CARGO_BUILD_TARGET` and other cargo configuration is now taken into account when running `cargo deadlinks`. It will still be ignored when running with `--no-build`. [PR#102] 159 | 160 | [PR#87]: https://github.com/deadlinks/cargo-deadlinks/pull/87 161 | [PR#100]: https://github.com/deadlinks/cargo-deadlinks/pull/100 162 | [PR#101]: https://github.com/deadlinks/cargo-deadlinks/pull/101 163 | [PR#102]: https://github.com/deadlinks/cargo-deadlinks/pull/102 164 | [PR#103]: https://github.com/deadlinks/cargo-deadlinks/pull/103 165 | 166 | 167 | ## 0.5.0 (2020-11-13) 168 | 169 | #### Added 170 | 171 | * If a URL points to a directory, check if index.html exists in that directory. [PR#90] 172 | * Treat absolute paths as absolute with respect to the `base_url`, not with respect to the file system. [PR#91] 173 | * Check link fragments, with special handling for Rustdoc ranged fragments to highlight source code lines [PR#94] 174 | 175 | [PR#90]: https://github.com/deadlinks/cargo-deadlinks/pull/90 176 | [PR#91]: https://github.com/deadlinks/cargo-deadlinks/pull/91 177 | [PR#94]: https://github.com/deadlinks/cargo-deadlinks/pull/94 178 | 179 | #### Fixes 180 | 181 | * No longer try to document examples that are dynamic libraries 182 | 183 | This was a regression introduced by [PR#68]. That looked at all targets to 184 | see which should be documented, but the logic for determining whether a target 185 | had docs was incorrect - it counted tests and examples if they were marked as a 186 | library. deadlinks will now ignore tests and examples even if they are not 187 | binaries. 188 | 189 | * No longer download dependencies from crates.io when calculating targets 190 | 191 | Previously, `cargo metadata` would download all dependencies even though they weren't used. 192 | 193 | #### Changes 194 | 195 | * Switch from `reqwest` to `ureq` for HTTP-checking, cutting down the number of dependencies by almost a third. [PR#95] 196 | * Switch from `html5ever` to `lol_html`, making the code much easier to modify. [PR#86] 197 | 198 | [PR#86]: https://github.com/deadlinks/cargo-deadlinks/pull/86 199 | [PR#95]: https://github.com/deadlinks/cargo-deadlinks/pull/95 200 | 201 | 202 | ## 0.4.2 (2020-10-12) 203 | 204 | #### Added 205 | 206 | * Add support for cargo workspaces. Check all crates and targets in the workspaces, excluding tests, benches, and examples. [PR#68], [PR#73] 207 | * Add automatic binary releases. [PR#64] You can find the releases at [/releases] on the GitHub page. 208 | 209 | [PR#64]: https://github.com/deadlinks/cargo-deadlinks/pull/64 210 | [/releases]: https://github.com/deadlinks/cargo-deadlinks/releases 211 | 212 | #### Fixes 213 | 214 | * Take `CARGO_TARGET_DIR` into account when looking for the target directory. [PR#66] 215 | * Give a better error message if Cargo.toml is not present. [PR#67] 216 | * Follow target renames. [PR#68] 217 | * Always output all errors instead of stopping after the first error. [PR#74] 218 | 219 | Previously, deadlinks would stop after the first error, but leave other threads running in parallel. This would lead to non-deterministic and incomplete output if there were broken links in many different files. 220 | Deadlinks will now output all errors before exiting. 221 | 222 | [PR#66]: https://github.com/deadlinks/cargo-deadlinks/pull/66 223 | [PR#67]: https://github.com/deadlinks/cargo-deadlinks/pull/67 224 | [PR#73]: https://github.com/deadlinks/cargo-deadlinks/pull/73 225 | [PR#74]: https://github.com/deadlinks/cargo-deadlinks/pull/74 226 | 227 | #### Changes 228 | 229 | * Update dependencies. [PR#51], [PR#76], [22fa61df] Thanks to [@Marwes][user_marwes]! 230 | * Use HEAD instead of GET for HTTP requests. This should decrease the time for HTTP checks slightly. [PR#63] Thanks to [@zummenix]! 231 | * Check all targets, not just targets with the same name as the package. In particular, this now checks both binaries and libraries. [PR#68] 232 | * Shorten path names when `--debug` is not passed. [PR#20] 233 | 234 | [@zummenix]: https://github.com/zummenix 235 | [PR#20]: https://github.com/deadlinks/cargo-deadlinks/pull/20 236 | [PR#51]: https://github.com/deadlinks/cargo-deadlinks/pull/51 237 | [PR#63]: https://github.com/deadlinks/cargo-deadlinks/pull/63 238 | [PR#68]: https://github.com/deadlinks/cargo-deadlinks/pull/68 239 | [PR#76]: https://github.com/deadlinks/cargo-deadlinks/pull/76 240 | [22fa61df]: https://github.com/deadlinks/cargo-deadlinks/commit/22fa61df44820d7f05415e026fa8396ee0e82954 241 | 242 | 243 | ## 0.4.1 (2019-03-26) 244 | 245 | #### Features 246 | 247 | * Provide a crate in addition to the binary. [PR#48][pr_48] Thanks to [@Marwes][user_marwes]! 248 | 249 | 250 | ## 0.4.0 (2019-03-17) 251 | 252 | #### Features 253 | 254 | * Add checking of HTTP links via `reqwest` (Thanks to [@gsquire][user_gsquire]!) 255 | * Can be used with `cargo deadlinks --check-http` 256 | * Improved error message on missing docs directory. [PR#33][pr_33] 257 | 258 | 259 | 260 | ## 0.3.0 (2017-11-16) 261 | 262 | ??? 263 | 264 | 265 | ## 0.2.1 (2017-10-12) 266 | 267 | ??? 268 | 269 | 270 | ## 0.2.0 (2017-10-06) 271 | 272 | ??? 273 | 274 | 275 | ## 0.1.0 (2016-03-25) 276 | 277 | ??? 278 | 279 | 280 | [user_gsquire]: https://github.com/gsquire 281 | [user_marwes]: https://github.com/Marwes 282 | 283 | [pr_33]: https://github.com/deadlinks/cargo-deadlinks/pull/33 284 | [pr_48]: https://github.com/deadlinks/cargo-deadlinks/pull/48 285 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.4.7" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "0.7.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "assert-json-diff" 22 | version = "2.0.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" 25 | dependencies = [ 26 | "serde", 27 | "serde_json", 28 | ] 29 | 30 | [[package]] 31 | name = "assert_cmd" 32 | version = "2.0.2" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" 35 | dependencies = [ 36 | "bstr", 37 | "doc-comment", 38 | "predicates", 39 | "predicates-core", 40 | "predicates-tree", 41 | "wait-timeout", 42 | ] 43 | 44 | [[package]] 45 | name = "atty" 46 | version = "0.2.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 49 | dependencies = [ 50 | "hermit-abi", 51 | "libc", 52 | "winapi", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "0.1.7" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.0.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 66 | 67 | [[package]] 68 | name = "base64" 69 | version = "0.13.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "1.3.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 78 | 79 | [[package]] 80 | name = "bstr" 81 | version = "0.2.17" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 84 | dependencies = [ 85 | "lazy_static", 86 | "memchr", 87 | "regex-automata", 88 | ] 89 | 90 | [[package]] 91 | name = "bumpalo" 92 | version = "3.7.1" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" 95 | 96 | [[package]] 97 | name = "byteorder" 98 | version = "1.4.3" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 101 | 102 | [[package]] 103 | name = "cached" 104 | version = "0.25.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "b99e696f7b2696ed5eae0d462a9eeafaea111d99e39b2c8ceb418afe1013bcfc" 107 | dependencies = [ 108 | "hashbrown", 109 | "once_cell", 110 | ] 111 | 112 | [[package]] 113 | name = "camino" 114 | version = "1.0.5" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" 117 | dependencies = [ 118 | "serde", 119 | ] 120 | 121 | [[package]] 122 | name = "cargo-deadlinks" 123 | version = "0.8.1" 124 | dependencies = [ 125 | "assert_cmd", 126 | "cached", 127 | "cargo_metadata", 128 | "env_logger", 129 | "log", 130 | "lol_html", 131 | "mockito", 132 | "num_cpus", 133 | "once_cell", 134 | "percent-encoding", 135 | "pico-args", 136 | "predicates", 137 | "rayon", 138 | "regex", 139 | "serde", 140 | "serde_derive", 141 | "serde_json", 142 | "ureq", 143 | "url", 144 | "walkdir", 145 | ] 146 | 147 | [[package]] 148 | name = "cargo-platform" 149 | version = "0.1.2" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" 152 | dependencies = [ 153 | "serde", 154 | ] 155 | 156 | [[package]] 157 | name = "cargo_metadata" 158 | version = "0.14.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "c297bd3135f558552f99a0daa180876984ea2c4ffa7470314540dff8c654109a" 161 | dependencies = [ 162 | "camino", 163 | "cargo-platform", 164 | "semver", 165 | "serde", 166 | "serde_json", 167 | ] 168 | 169 | [[package]] 170 | name = "cc" 171 | version = "1.0.71" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" 174 | 175 | [[package]] 176 | name = "cfg-if" 177 | version = "0.1.10" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 180 | 181 | [[package]] 182 | name = "cfg-if" 183 | version = "1.0.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 186 | 187 | [[package]] 188 | name = "chunked_transfer" 189 | version = "1.4.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" 192 | 193 | [[package]] 194 | name = "cloudabi" 195 | version = "0.0.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 198 | dependencies = [ 199 | "bitflags", 200 | ] 201 | 202 | [[package]] 203 | name = "colored" 204 | version = "2.0.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 207 | dependencies = [ 208 | "atty", 209 | "lazy_static", 210 | "winapi", 211 | ] 212 | 213 | [[package]] 214 | name = "crossbeam-channel" 215 | version = "0.5.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 218 | dependencies = [ 219 | "cfg-if 1.0.0", 220 | "crossbeam-utils", 221 | ] 222 | 223 | [[package]] 224 | name = "crossbeam-deque" 225 | version = "0.8.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 228 | dependencies = [ 229 | "cfg-if 1.0.0", 230 | "crossbeam-epoch", 231 | "crossbeam-utils", 232 | ] 233 | 234 | [[package]] 235 | name = "crossbeam-epoch" 236 | version = "0.9.5" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" 239 | dependencies = [ 240 | "cfg-if 1.0.0", 241 | "crossbeam-utils", 242 | "lazy_static", 243 | "memoffset", 244 | "scopeguard", 245 | ] 246 | 247 | [[package]] 248 | name = "crossbeam-utils" 249 | version = "0.8.14" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" 252 | dependencies = [ 253 | "cfg-if 1.0.0", 254 | ] 255 | 256 | [[package]] 257 | name = "cssparser" 258 | version = "0.25.9" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "fbe18ca4efb9ba3716c6da66cc3d7e673bf59fa576353011f48c4cfddbdd740e" 261 | dependencies = [ 262 | "autocfg 0.1.7", 263 | "cssparser-macros", 264 | "dtoa-short", 265 | "itoa", 266 | "matches", 267 | "phf", 268 | "proc-macro2", 269 | "procedural-masquerade", 270 | "quote", 271 | "smallvec", 272 | "syn", 273 | ] 274 | 275 | [[package]] 276 | name = "cssparser-macros" 277 | version = "0.3.6" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "5bb1c84e87c717666564ec056105052331431803d606bd45529b28547b611eef" 280 | dependencies = [ 281 | "phf_codegen", 282 | "proc-macro2", 283 | "procedural-masquerade", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "difflib" 290 | version = "0.4.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 293 | 294 | [[package]] 295 | name = "doc-comment" 296 | version = "0.3.3" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 299 | 300 | [[package]] 301 | name = "dtoa" 302 | version = "0.4.8" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 305 | 306 | [[package]] 307 | name = "dtoa-short" 308 | version = "0.3.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" 311 | dependencies = [ 312 | "dtoa", 313 | ] 314 | 315 | [[package]] 316 | name = "either" 317 | version = "1.6.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 320 | 321 | [[package]] 322 | name = "encoding_rs" 323 | version = "0.8.28" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" 326 | dependencies = [ 327 | "cfg-if 1.0.0", 328 | ] 329 | 330 | [[package]] 331 | name = "env_logger" 332 | version = "0.9.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 335 | dependencies = [ 336 | "atty", 337 | "humantime", 338 | "log", 339 | "regex", 340 | "termcolor", 341 | ] 342 | 343 | [[package]] 344 | name = "float-cmp" 345 | version = "0.9.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 348 | dependencies = [ 349 | "num-traits", 350 | ] 351 | 352 | [[package]] 353 | name = "form_urlencoded" 354 | version = "1.0.1" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 357 | dependencies = [ 358 | "matches", 359 | "percent-encoding", 360 | ] 361 | 362 | [[package]] 363 | name = "fuchsia-cprng" 364 | version = "0.1.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 367 | 368 | [[package]] 369 | name = "fxhash" 370 | version = "0.2.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 373 | dependencies = [ 374 | "byteorder", 375 | ] 376 | 377 | [[package]] 378 | name = "getrandom" 379 | version = "0.2.3" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 382 | dependencies = [ 383 | "cfg-if 1.0.0", 384 | "libc", 385 | "wasi", 386 | ] 387 | 388 | [[package]] 389 | name = "hashbrown" 390 | version = "0.9.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 393 | dependencies = [ 394 | "ahash", 395 | ] 396 | 397 | [[package]] 398 | name = "hermit-abi" 399 | version = "0.1.19" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 402 | dependencies = [ 403 | "libc", 404 | ] 405 | 406 | [[package]] 407 | name = "httparse" 408 | version = "1.5.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" 411 | 412 | [[package]] 413 | name = "humantime" 414 | version = "2.1.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 417 | 418 | [[package]] 419 | name = "idna" 420 | version = "0.2.3" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 423 | dependencies = [ 424 | "matches", 425 | "unicode-bidi", 426 | "unicode-normalization", 427 | ] 428 | 429 | [[package]] 430 | name = "itertools" 431 | version = "0.10.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" 434 | dependencies = [ 435 | "either", 436 | ] 437 | 438 | [[package]] 439 | name = "itoa" 440 | version = "0.4.8" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 443 | 444 | [[package]] 445 | name = "js-sys" 446 | version = "0.3.55" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 449 | dependencies = [ 450 | "wasm-bindgen", 451 | ] 452 | 453 | [[package]] 454 | name = "lazy_static" 455 | version = "1.4.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 458 | 459 | [[package]] 460 | name = "lazycell" 461 | version = "1.3.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 464 | 465 | [[package]] 466 | name = "libc" 467 | version = "0.2.103" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" 470 | 471 | [[package]] 472 | name = "log" 473 | version = "0.4.14" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 476 | dependencies = [ 477 | "cfg-if 1.0.0", 478 | ] 479 | 480 | [[package]] 481 | name = "lol_html" 482 | version = "0.3.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "b59f94556144354f6abfb3fe175e8f2da290329f254ab4019e5096875f6056e3" 485 | dependencies = [ 486 | "bitflags", 487 | "cfg-if 0.1.10", 488 | "cssparser", 489 | "encoding_rs", 490 | "hashbrown", 491 | "lazy_static", 492 | "lazycell", 493 | "memchr", 494 | "safemem", 495 | "selectors", 496 | "thiserror", 497 | ] 498 | 499 | [[package]] 500 | name = "matches" 501 | version = "0.1.9" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 504 | 505 | [[package]] 506 | name = "maybe-uninit" 507 | version = "2.0.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 510 | 511 | [[package]] 512 | name = "memchr" 513 | version = "2.4.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 516 | 517 | [[package]] 518 | name = "memoffset" 519 | version = "0.6.4" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" 522 | dependencies = [ 523 | "autocfg 1.0.1", 524 | ] 525 | 526 | [[package]] 527 | name = "mockito" 528 | version = "0.31.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "401edc088069634afaa5f4a29617b36dba683c0c16fe4435a86debad23fa2f1a" 531 | dependencies = [ 532 | "assert-json-diff", 533 | "colored", 534 | "httparse", 535 | "lazy_static", 536 | "log", 537 | "rand 0.8.4", 538 | "regex", 539 | "serde_json", 540 | "serde_urlencoded", 541 | "similar", 542 | ] 543 | 544 | [[package]] 545 | name = "nodrop" 546 | version = "0.1.14" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 549 | 550 | [[package]] 551 | name = "normalize-line-endings" 552 | version = "0.3.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 555 | 556 | [[package]] 557 | name = "num-traits" 558 | version = "0.2.14" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 561 | dependencies = [ 562 | "autocfg 1.0.1", 563 | ] 564 | 565 | [[package]] 566 | name = "num_cpus" 567 | version = "1.13.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 570 | dependencies = [ 571 | "hermit-abi", 572 | "libc", 573 | ] 574 | 575 | [[package]] 576 | name = "once_cell" 577 | version = "1.8.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 580 | 581 | [[package]] 582 | name = "percent-encoding" 583 | version = "2.1.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 586 | 587 | [[package]] 588 | name = "phf" 589 | version = "0.7.24" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" 592 | dependencies = [ 593 | "phf_shared", 594 | ] 595 | 596 | [[package]] 597 | name = "phf_codegen" 598 | version = "0.7.24" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" 601 | dependencies = [ 602 | "phf_generator", 603 | "phf_shared", 604 | ] 605 | 606 | [[package]] 607 | name = "phf_generator" 608 | version = "0.7.24" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" 611 | dependencies = [ 612 | "phf_shared", 613 | "rand 0.6.5", 614 | ] 615 | 616 | [[package]] 617 | name = "phf_shared" 618 | version = "0.7.24" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" 621 | dependencies = [ 622 | "siphasher", 623 | ] 624 | 625 | [[package]] 626 | name = "pico-args" 627 | version = "0.3.4" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "28b9b4df73455c861d7cbf8be42f01d3b373ed7f02e378d55fa84eafc6f638b1" 630 | 631 | [[package]] 632 | name = "ppv-lite86" 633 | version = "0.2.10" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 636 | 637 | [[package]] 638 | name = "precomputed-hash" 639 | version = "0.1.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 642 | 643 | [[package]] 644 | name = "predicates" 645 | version = "2.0.3" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" 648 | dependencies = [ 649 | "difflib", 650 | "float-cmp", 651 | "itertools", 652 | "normalize-line-endings", 653 | "predicates-core", 654 | "regex", 655 | ] 656 | 657 | [[package]] 658 | name = "predicates-core" 659 | version = "1.0.2" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" 662 | 663 | [[package]] 664 | name = "predicates-tree" 665 | version = "1.0.4" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" 668 | dependencies = [ 669 | "predicates-core", 670 | "termtree", 671 | ] 672 | 673 | [[package]] 674 | name = "proc-macro2" 675 | version = "1.0.30" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" 678 | dependencies = [ 679 | "unicode-xid", 680 | ] 681 | 682 | [[package]] 683 | name = "procedural-masquerade" 684 | version = "0.1.7" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "8f1383dff4092fe903ac180e391a8d4121cc48f08ccf850614b0290c6673b69d" 687 | 688 | [[package]] 689 | name = "quote" 690 | version = "1.0.10" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 693 | dependencies = [ 694 | "proc-macro2", 695 | ] 696 | 697 | [[package]] 698 | name = "rand" 699 | version = "0.6.5" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 702 | dependencies = [ 703 | "autocfg 0.1.7", 704 | "libc", 705 | "rand_chacha 0.1.1", 706 | "rand_core 0.4.2", 707 | "rand_hc 0.1.0", 708 | "rand_isaac", 709 | "rand_jitter", 710 | "rand_os", 711 | "rand_pcg", 712 | "rand_xorshift", 713 | "winapi", 714 | ] 715 | 716 | [[package]] 717 | name = "rand" 718 | version = "0.8.4" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 721 | dependencies = [ 722 | "libc", 723 | "rand_chacha 0.3.1", 724 | "rand_core 0.6.3", 725 | "rand_hc 0.3.1", 726 | ] 727 | 728 | [[package]] 729 | name = "rand_chacha" 730 | version = "0.1.1" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 733 | dependencies = [ 734 | "autocfg 0.1.7", 735 | "rand_core 0.3.1", 736 | ] 737 | 738 | [[package]] 739 | name = "rand_chacha" 740 | version = "0.3.1" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 743 | dependencies = [ 744 | "ppv-lite86", 745 | "rand_core 0.6.3", 746 | ] 747 | 748 | [[package]] 749 | name = "rand_core" 750 | version = "0.3.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 753 | dependencies = [ 754 | "rand_core 0.4.2", 755 | ] 756 | 757 | [[package]] 758 | name = "rand_core" 759 | version = "0.4.2" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 762 | 763 | [[package]] 764 | name = "rand_core" 765 | version = "0.6.3" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 768 | dependencies = [ 769 | "getrandom", 770 | ] 771 | 772 | [[package]] 773 | name = "rand_hc" 774 | version = "0.1.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 777 | dependencies = [ 778 | "rand_core 0.3.1", 779 | ] 780 | 781 | [[package]] 782 | name = "rand_hc" 783 | version = "0.3.1" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 786 | dependencies = [ 787 | "rand_core 0.6.3", 788 | ] 789 | 790 | [[package]] 791 | name = "rand_isaac" 792 | version = "0.1.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 795 | dependencies = [ 796 | "rand_core 0.3.1", 797 | ] 798 | 799 | [[package]] 800 | name = "rand_jitter" 801 | version = "0.1.4" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 804 | dependencies = [ 805 | "libc", 806 | "rand_core 0.4.2", 807 | "winapi", 808 | ] 809 | 810 | [[package]] 811 | name = "rand_os" 812 | version = "0.1.3" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 815 | dependencies = [ 816 | "cloudabi", 817 | "fuchsia-cprng", 818 | "libc", 819 | "rand_core 0.4.2", 820 | "rdrand", 821 | "winapi", 822 | ] 823 | 824 | [[package]] 825 | name = "rand_pcg" 826 | version = "0.1.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 829 | dependencies = [ 830 | "autocfg 0.1.7", 831 | "rand_core 0.4.2", 832 | ] 833 | 834 | [[package]] 835 | name = "rand_xorshift" 836 | version = "0.1.1" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 839 | dependencies = [ 840 | "rand_core 0.3.1", 841 | ] 842 | 843 | [[package]] 844 | name = "rayon" 845 | version = "1.5.1" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" 848 | dependencies = [ 849 | "autocfg 1.0.1", 850 | "crossbeam-deque", 851 | "either", 852 | "rayon-core", 853 | ] 854 | 855 | [[package]] 856 | name = "rayon-core" 857 | version = "1.9.1" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" 860 | dependencies = [ 861 | "crossbeam-channel", 862 | "crossbeam-deque", 863 | "crossbeam-utils", 864 | "lazy_static", 865 | "num_cpus", 866 | ] 867 | 868 | [[package]] 869 | name = "rdrand" 870 | version = "0.4.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 873 | dependencies = [ 874 | "rand_core 0.3.1", 875 | ] 876 | 877 | [[package]] 878 | name = "regex" 879 | version = "1.7.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 882 | dependencies = [ 883 | "aho-corasick", 884 | "memchr", 885 | "regex-syntax", 886 | ] 887 | 888 | [[package]] 889 | name = "regex-automata" 890 | version = "0.1.10" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 893 | 894 | [[package]] 895 | name = "regex-syntax" 896 | version = "0.6.28" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 899 | 900 | [[package]] 901 | name = "ring" 902 | version = "0.16.20" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 905 | dependencies = [ 906 | "cc", 907 | "libc", 908 | "once_cell", 909 | "spin", 910 | "untrusted", 911 | "web-sys", 912 | "winapi", 913 | ] 914 | 915 | [[package]] 916 | name = "rustls" 917 | version = "0.19.1" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" 920 | dependencies = [ 921 | "base64", 922 | "log", 923 | "ring", 924 | "sct", 925 | "webpki", 926 | ] 927 | 928 | [[package]] 929 | name = "ryu" 930 | version = "1.0.5" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 933 | 934 | [[package]] 935 | name = "safemem" 936 | version = "0.3.3" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 939 | 940 | [[package]] 941 | name = "same-file" 942 | version = "1.0.6" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 945 | dependencies = [ 946 | "winapi-util", 947 | ] 948 | 949 | [[package]] 950 | name = "scopeguard" 951 | version = "1.1.0" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 954 | 955 | [[package]] 956 | name = "sct" 957 | version = "0.6.1" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" 960 | dependencies = [ 961 | "ring", 962 | "untrusted", 963 | ] 964 | 965 | [[package]] 966 | name = "selectors" 967 | version = "0.21.0" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "1b86b100bede4f651059740afc3b6cb83458d7401cb7c1ad96d8a11e91742c86" 970 | dependencies = [ 971 | "bitflags", 972 | "cssparser", 973 | "fxhash", 974 | "log", 975 | "matches", 976 | "phf", 977 | "phf_codegen", 978 | "precomputed-hash", 979 | "servo_arc", 980 | "smallvec", 981 | "thin-slice", 982 | ] 983 | 984 | [[package]] 985 | name = "semver" 986 | version = "1.0.4" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" 989 | dependencies = [ 990 | "serde", 991 | ] 992 | 993 | [[package]] 994 | name = "serde" 995 | version = "1.0.130" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" 998 | dependencies = [ 999 | "serde_derive", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "serde_derive" 1004 | version = "1.0.130" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" 1007 | dependencies = [ 1008 | "proc-macro2", 1009 | "quote", 1010 | "syn", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "serde_json" 1015 | version = "1.0.68" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" 1018 | dependencies = [ 1019 | "itoa", 1020 | "ryu", 1021 | "serde", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "serde_urlencoded" 1026 | version = "0.7.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1029 | dependencies = [ 1030 | "form_urlencoded", 1031 | "itoa", 1032 | "ryu", 1033 | "serde", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "servo_arc" 1038 | version = "0.1.1" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" 1041 | dependencies = [ 1042 | "nodrop", 1043 | "stable_deref_trait", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "similar" 1048 | version = "2.2.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" 1051 | 1052 | [[package]] 1053 | name = "siphasher" 1054 | version = "0.2.3" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" 1057 | 1058 | [[package]] 1059 | name = "smallvec" 1060 | version = "0.6.14" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 1063 | dependencies = [ 1064 | "maybe-uninit", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "spin" 1069 | version = "0.5.2" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1072 | 1073 | [[package]] 1074 | name = "stable_deref_trait" 1075 | version = "1.2.0" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1078 | 1079 | [[package]] 1080 | name = "syn" 1081 | version = "1.0.80" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" 1084 | dependencies = [ 1085 | "proc-macro2", 1086 | "quote", 1087 | "unicode-xid", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "termcolor" 1092 | version = "1.1.2" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1095 | dependencies = [ 1096 | "winapi-util", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "termtree" 1101 | version = "0.2.1" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378" 1104 | 1105 | [[package]] 1106 | name = "thin-slice" 1107 | version = "0.1.1" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" 1110 | 1111 | [[package]] 1112 | name = "thiserror" 1113 | version = "1.0.30" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 1116 | dependencies = [ 1117 | "thiserror-impl", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "thiserror-impl" 1122 | version = "1.0.30" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 1125 | dependencies = [ 1126 | "proc-macro2", 1127 | "quote", 1128 | "syn", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "tinyvec" 1133 | version = "1.5.0" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" 1136 | dependencies = [ 1137 | "tinyvec_macros", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "tinyvec_macros" 1142 | version = "0.1.0" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1145 | 1146 | [[package]] 1147 | name = "unicode-bidi" 1148 | version = "0.3.7" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" 1151 | 1152 | [[package]] 1153 | name = "unicode-normalization" 1154 | version = "0.1.19" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1157 | dependencies = [ 1158 | "tinyvec", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "unicode-xid" 1163 | version = "0.2.2" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1166 | 1167 | [[package]] 1168 | name = "untrusted" 1169 | version = "0.7.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1172 | 1173 | [[package]] 1174 | name = "ureq" 1175 | version = "2.2.0" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "3131cd6cb18488da91da1d10ed31e966f453c06b65bf010d35638456976a3fd7" 1178 | dependencies = [ 1179 | "base64", 1180 | "chunked_transfer", 1181 | "log", 1182 | "once_cell", 1183 | "rustls", 1184 | "url", 1185 | "webpki", 1186 | "webpki-roots", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "url" 1191 | version = "2.2.2" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1194 | dependencies = [ 1195 | "form_urlencoded", 1196 | "idna", 1197 | "matches", 1198 | "percent-encoding", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "wait-timeout" 1203 | version = "0.2.0" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1206 | dependencies = [ 1207 | "libc", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "walkdir" 1212 | version = "2.3.2" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1215 | dependencies = [ 1216 | "same-file", 1217 | "winapi", 1218 | "winapi-util", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "wasi" 1223 | version = "0.10.2+wasi-snapshot-preview1" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1226 | 1227 | [[package]] 1228 | name = "wasm-bindgen" 1229 | version = "0.2.78" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 1232 | dependencies = [ 1233 | "cfg-if 1.0.0", 1234 | "wasm-bindgen-macro", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "wasm-bindgen-backend" 1239 | version = "0.2.78" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 1242 | dependencies = [ 1243 | "bumpalo", 1244 | "lazy_static", 1245 | "log", 1246 | "proc-macro2", 1247 | "quote", 1248 | "syn", 1249 | "wasm-bindgen-shared", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "wasm-bindgen-macro" 1254 | version = "0.2.78" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 1257 | dependencies = [ 1258 | "quote", 1259 | "wasm-bindgen-macro-support", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "wasm-bindgen-macro-support" 1264 | version = "0.2.78" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 1267 | dependencies = [ 1268 | "proc-macro2", 1269 | "quote", 1270 | "syn", 1271 | "wasm-bindgen-backend", 1272 | "wasm-bindgen-shared", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "wasm-bindgen-shared" 1277 | version = "0.2.78" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 1280 | 1281 | [[package]] 1282 | name = "web-sys" 1283 | version = "0.3.55" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" 1286 | dependencies = [ 1287 | "js-sys", 1288 | "wasm-bindgen", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "webpki" 1293 | version = "0.21.4" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" 1296 | dependencies = [ 1297 | "ring", 1298 | "untrusted", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "webpki-roots" 1303 | version = "0.21.1" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" 1306 | dependencies = [ 1307 | "webpki", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "winapi" 1312 | version = "0.3.9" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1315 | dependencies = [ 1316 | "winapi-i686-pc-windows-gnu", 1317 | "winapi-x86_64-pc-windows-gnu", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "winapi-i686-pc-windows-gnu" 1322 | version = "0.4.0" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1325 | 1326 | [[package]] 1327 | name = "winapi-util" 1328 | version = "0.1.5" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1331 | dependencies = [ 1332 | "winapi", 1333 | ] 1334 | 1335 | [[package]] 1336 | name = "winapi-x86_64-pc-windows-gnu" 1337 | version = "0.4.0" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1340 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-deadlinks" 3 | description = "Cargo subcommand for checking your documentation for broken links" 4 | version = "0.8.1" 5 | authors = ["Maximilian Goisser ", "Joshua Nelson , 38 | arg_cargo_directory: Option, 39 | flag_verbose: bool, 40 | flag_debug: bool, 41 | flag_check_http: bool, 42 | flag_forbid_http: bool, 43 | flag_check_intra_doc_links: bool, 44 | flag_no_build: bool, 45 | flag_ignore_fragments: bool, 46 | cargo_args: Vec, 47 | } 48 | 49 | impl From<&MainArgs> for CheckContext { 50 | fn from(args: &MainArgs) -> CheckContext { 51 | let check_http = if args.flag_check_http { 52 | HttpCheck::Enabled 53 | } else if args.flag_forbid_http { 54 | HttpCheck::Forbidden 55 | } else { 56 | HttpCheck::Ignored 57 | }; 58 | CheckContext { 59 | check_http, 60 | verbose: args.flag_debug, 61 | check_fragments: !args.flag_ignore_fragments, 62 | check_intra_doc_links: args.flag_check_intra_doc_links, 63 | } 64 | } 65 | } 66 | 67 | fn parse_args() -> Result { 68 | use pico_args::*; 69 | 70 | let mut args: Vec<_> = std::env::args_os().collect(); 71 | args.remove(0); 72 | if args.get(0).map_or(true, |arg| arg != "deadlinks") { 73 | return Err(Error::ArgumentParsingFailed { 74 | cause: "cargo-deadlinks should be run as `cargo deadlinks`".into(), 75 | } 76 | .into()); 77 | } 78 | args.remove(0); 79 | 80 | let cargo_args = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") { 81 | let c = args.drain(dash_dash + 1..).collect(); 82 | args.pop(); 83 | c 84 | } else { 85 | Vec::new() 86 | }; 87 | 88 | let mut args = Arguments::from_vec(args); 89 | if args.contains(["-V", "--version"]) { 90 | println!(concat!("cargo-deadlinks ", env!("CARGO_PKG_VERSION"))); 91 | std::process::exit(0); 92 | } else if args.contains(["-h", "--help"]) { 93 | println!("{}", MAIN_USAGE); 94 | std::process::exit(0); 95 | } 96 | let main_args = MainArgs { 97 | arg_directory: args.opt_value_from_str("--dir")?, 98 | arg_cargo_directory: args 99 | .opt_value_from_os_str("--cargo-dir", |s| Result::<_, Error>::Ok(s.to_owned()))?, 100 | flag_verbose: args.contains(["-v", "--verbose"]), 101 | flag_debug: args.contains("--debug"), 102 | flag_no_build: args.contains("--no-build"), 103 | flag_ignore_fragments: args.contains("--ignore-fragments"), 104 | flag_check_intra_doc_links: args.contains("--check-intra-doc-links"), 105 | flag_check_http: args.contains("--check-http"), 106 | flag_forbid_http: args.contains("--forbid-http"), 107 | cargo_args, 108 | }; 109 | args.finish()?; 110 | if main_args.flag_forbid_http && main_args.flag_check_http { 111 | Err(pico_args::Error::ArgumentParsingFailed { 112 | cause: "--check-http and --forbid-http are mutually incompatible".into(), 113 | } 114 | .into()) 115 | } else { 116 | Ok(main_args) 117 | } 118 | } 119 | 120 | fn main() { 121 | let args: MainArgs = match parse_args() { 122 | Ok(args) => args, 123 | Err(err) => { 124 | eprintln!("error: {}", err); 125 | std::process::exit(1) 126 | } 127 | }; 128 | 129 | shared::init_logger(args.flag_debug, args.flag_verbose, "cargo_deadlinks"); 130 | 131 | let dirs = args.arg_directory.as_ref().map_or_else( 132 | || { 133 | let dir = args.arg_cargo_directory.as_deref(); 134 | determine_dir(args.flag_no_build, &args.cargo_args, dir) 135 | }, 136 | |dir| vec![dir.into()], 137 | ); 138 | 139 | let ctx = CheckContext::from(&args); 140 | let mut errors = false; 141 | for dir in &dirs { 142 | let dir = match dir.canonicalize() { 143 | Ok(dir) => dir, 144 | Err(_) => { 145 | eprintln!("error: could not find directory {:?}.", dir); 146 | if args.arg_directory.is_none() { 147 | assert!( 148 | args.flag_no_build, 149 | "cargo said it built a directory it didn't build" 150 | ); 151 | eprintln!( 152 | "help: consider removing `--no-build`, or running `cargo doc` yourself." 153 | ); 154 | } 155 | process::exit(1); 156 | } 157 | }; 158 | log::info!("checking directory {:?}", dir); 159 | if walk_dir(&dir, &ctx) { 160 | errors = true; 161 | } 162 | } 163 | if errors { 164 | process::exit(1); 165 | } else if dirs.is_empty() { 166 | assert!(args.arg_directory.is_none()); 167 | eprintln!("warning: no directories were detected"); 168 | } 169 | } 170 | 171 | /// Returns the directories to use as root of the documentation. 172 | /// 173 | /// If an directory has been provided as CLI argument that one is used. 174 | /// Otherwise, if `no_build` is passed, we try to find the `Cargo.toml` and 175 | /// construct the documentation path from the package name found there. 176 | /// Otherwise, build the documentation and have cargo itself tell us where it is. 177 | /// 178 | /// All *.html files under the root directory will be checked. 179 | fn determine_dir( 180 | no_build: bool, 181 | cargo_args: &[OsString], 182 | cargo_dir: Option<&OsStr>, 183 | ) -> Vec { 184 | if no_build { 185 | eprintln!("warning: --no-build ignores `doc = false` and may have other bugs"); 186 | let manifest = MetadataCommand::new() 187 | .no_deps() 188 | .exec() 189 | .unwrap_or_else(|err| { 190 | println!("error: {}", err); 191 | println!("help: if this is not a cargo directory, use `--dir`"); 192 | process::exit(1); 193 | }); 194 | let doc = manifest.target_directory.join("doc"); 195 | 196 | // originally written with this impressively bad jq query: 197 | // `.packages[] |select(.source == null) | .targets[] | select(.kind[] | contains("test") | not) | .name` 198 | let iter = manifest 199 | .packages 200 | .into_iter() 201 | .filter(|package| package.source.is_none()) 202 | .flat_map(|package| package.targets) 203 | .filter(has_docs) 204 | .map(move |target| doc.join(target.name.replace('-', "_"))); 205 | return iter.collect(); 206 | } 207 | 208 | // Build the documentation, collecting info about the build at the same time. 209 | log::info!("building documentation using cargo"); 210 | let cargo = env::var("CARGO").unwrap_or_else(|_| { 211 | println!("error: `cargo-deadlinks` must be run as either `cargo deadlinks` or with the `--dir` flag"); 212 | process::exit(1); 213 | }); 214 | // Stolen from https://docs.rs/cargo_metadata/0.12.0/cargo_metadata/#examples 215 | let mut cargo_process = Command::new(cargo); 216 | #[allow(clippy::needless_borrow)] // MSRV is 1.46 217 | cargo_process 218 | .args(&[ 219 | "doc", 220 | "--no-deps", 221 | "--message-format", 222 | "json-render-diagnostics", 223 | ]) 224 | .args(cargo_args) 225 | .stdout(process::Stdio::piped()); 226 | if let Some(dir) = cargo_dir { 227 | cargo_process.current_dir(dir); 228 | } 229 | // spawn instead of output() allows running deadlinks and cargo in parallel; 230 | // this is helpful when you have many dependencies that take a while to document 231 | let mut cargo_process = cargo_process.spawn().unwrap(); 232 | let reader = BufReader::new(cargo_process.stdout.take().unwrap()); 233 | // Originally written with jq: 234 | // `select(.reason == "compiler-artifact") | .filenames[] | select(endswith("/index.html")) | rtrimstr("/index.html")` 235 | let directories = Message::parse_stream(reader) 236 | .filter_map(|message| match message { 237 | Ok(Message::CompilerArtifact(artifact)) => Some(artifact.filenames), 238 | _ => None, 239 | }) 240 | .flatten() 241 | .filter(|path| path.file_name() == Some("index.html")) 242 | .map(|mut path| { 243 | path.pop(); 244 | path 245 | }) 246 | // TODO: run this in parallel, which should speed up builds a fair bit. 247 | // This will be hard because either cargo's progress bar will overlap with our output, 248 | // or we'll have to recreate the progress bar somehow. 249 | // See https://discord.com/channels/273534239310479360/335502067432947748/778636447154044948 for discussion. 250 | .collect(); 251 | let status = cargo_process.wait().unwrap(); 252 | if !status.success() { 253 | eprintln!("help: if this is not a cargo directory, use `--dir`"); 254 | process::exit(status.code().unwrap_or(2)); 255 | } 256 | directories 257 | } 258 | 259 | fn has_docs(target: &cargo_metadata::Target) -> bool { 260 | // Ignore tests, examples, and benchmarks, but still document binaries 261 | 262 | // See https://doc.rust-lang.org/cargo/reference/external-tools.html#compiler-messages 263 | // and https://github.com/rust-lang/docs.rs/issues/503#issuecomment-562797599 264 | // for the difference between `kind` and `crate_type` 265 | 266 | let mut kinds = target.kind.iter(); 267 | // By default, ignore binaries 268 | if target.crate_types.contains(&"bin".into()) { 269 | // But allow them if this is a literal bin, and not a test or example 270 | kinds.all(|kind| kind == "bin") 271 | } else { 272 | // We also have to consider examples and tests that are libraries 273 | // (e.g. because of `cdylib`). 274 | kinds.all(|kind| !["example", "test", "bench"].contains(&kind.as_str())) 275 | } 276 | } 277 | 278 | #[cfg(test)] 279 | mod test { 280 | use super::has_docs; 281 | use cargo_metadata::Target; 282 | 283 | fn target(crate_types: &str, kind: &str) -> Target { 284 | serde_json::from_str(&format!( 285 | r#"{{ 286 | "crate_types": ["{}"], 287 | "kind": ["{}"], 288 | "name": "simple", 289 | "src_path": "", 290 | "edition": "2018", 291 | "doctest": false, 292 | "test": false 293 | }}"#, 294 | crate_types, kind 295 | )) 296 | .unwrap() 297 | } 298 | 299 | #[test] 300 | fn finds_right_docs() { 301 | assert!(!has_docs(&target("cdylib", "example"))); 302 | assert!(!has_docs(&target("bin", "example"))); 303 | assert!(!has_docs(&target("bin", "test"))); 304 | assert!(!has_docs(&target("bin", "bench"))); 305 | assert!(!has_docs(&target("bin", "custom-build"))); 306 | 307 | assert!(has_docs(&target("bin", "bin"))); 308 | assert!(has_docs(&target("dylib", "dylib"))); 309 | assert!(has_docs(&target("rlib", "rlib"))); 310 | assert!(has_docs(&target("lib", "lib"))); 311 | assert!(has_docs(&target("proc-macro", "proc-macro"))); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/bin/deadlinks.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::process; 3 | 4 | use cargo_deadlinks::{walk_dir, CheckContext, HttpCheck}; 5 | use serde_derive::Deserialize; 6 | 7 | mod shared; 8 | 9 | const MAIN_USAGE: &str = " 10 | Check your package's documentation for dead links. 11 | 12 | Usage: 13 | deadlinks [options] ... 14 | 15 | Options: 16 | -h --help Print this message 17 | --check-http Check 'http' and 'https' scheme links 18 | --forbid-http Give an error if HTTP links are found. This is incompatible with --check-http. 19 | --ignore-fragments Don't check URL fragments. 20 | --debug Use debug output 21 | -v --verbose Use verbose output 22 | -V --version Print version info and exit. 23 | "; 24 | 25 | #[derive(Debug, Deserialize)] 26 | struct MainArgs { 27 | arg_directory: Vec, 28 | flag_verbose: bool, 29 | flag_debug: bool, 30 | flag_check_http: bool, 31 | flag_forbid_http: bool, 32 | flag_ignore_fragments: bool, 33 | } 34 | 35 | impl From<&MainArgs> for CheckContext { 36 | fn from(args: &MainArgs) -> CheckContext { 37 | let check_http = if args.flag_check_http { 38 | HttpCheck::Enabled 39 | } else if args.flag_forbid_http { 40 | HttpCheck::Forbidden 41 | } else { 42 | HttpCheck::Ignored 43 | }; 44 | CheckContext { 45 | check_http, 46 | verbose: args.flag_debug, 47 | check_fragments: !args.flag_ignore_fragments, 48 | check_intra_doc_links: false, 49 | } 50 | } 51 | } 52 | 53 | fn parse_args() -> Result { 54 | let mut args = pico_args::Arguments::from_env(); 55 | if args.contains(["-V", "--version"]) { 56 | println!(concat!("deadlinks ", env!("CARGO_PKG_VERSION"))); 57 | std::process::exit(0); 58 | } else if args.contains(["-h", "--help"]) { 59 | println!("{}", MAIN_USAGE); 60 | std::process::exit(0); 61 | } 62 | let args = MainArgs { 63 | flag_verbose: args.contains(["-v", "--verbose"]), 64 | flag_debug: args.contains("--debug"), 65 | flag_ignore_fragments: args.contains("--ignore-fragments"), 66 | flag_check_http: args.contains("--check-http"), 67 | flag_forbid_http: args.contains("--forbid-http"), 68 | arg_directory: args.free_os()?.into_iter().map(Into::into).collect(), 69 | }; 70 | if args.flag_forbid_http && args.flag_check_http { 71 | Err(pico_args::Error::ArgumentParsingFailed { 72 | cause: "--check-http and --forbid-http are mutually incompatible".into(), 73 | } 74 | .into()) 75 | } else { 76 | Ok(args) 77 | } 78 | } 79 | 80 | fn main() { 81 | let args = match parse_args() { 82 | Ok(args) => args, 83 | Err(err) => { 84 | println!("error: {}", err); 85 | process::exit(1); 86 | } 87 | }; 88 | if args.arg_directory.is_empty() { 89 | eprintln!("error: missing argument"); 90 | process::exit(1); 91 | } 92 | shared::init_logger(args.flag_debug, args.flag_verbose, "deadlinks"); 93 | 94 | let mut errors = false; 95 | let ctx = CheckContext::from(&args); 96 | for relative_dir in args.arg_directory { 97 | let dir = match relative_dir.canonicalize() { 98 | Ok(dir) => dir, 99 | Err(_) => { 100 | println!("Could not find directory {:?}.", relative_dir); 101 | process::exit(1); 102 | } 103 | }; 104 | log::info!("checking directory {:?}", dir); 105 | errors |= walk_dir(&dir, &ctx); 106 | } 107 | if errors { 108 | process::exit(1); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/bin/shared.rs: -------------------------------------------------------------------------------- 1 | use log::LevelFilter; 2 | use pico_args::Error; 3 | use std::fmt::{self, Display}; 4 | 5 | /// Initalizes the logger according to the provided config flags. 6 | pub fn init_logger(debug: bool, verbose: bool, krate: &str) { 7 | let mut builder = env_logger::Builder::new(); 8 | match (debug, verbose) { 9 | (true, _) => { 10 | builder.filter(Some(krate), LevelFilter::Debug); 11 | builder.filter(Some("cargo_deadlinks"), LevelFilter::Debug); 12 | } 13 | (false, true) => { 14 | builder.filter(Some(krate), LevelFilter::Info); 15 | builder.filter(Some("cargo_deadlinks"), LevelFilter::Info); 16 | } 17 | _ => {} 18 | } 19 | builder.parse_default_env().init(); 20 | } 21 | 22 | // See https://github.com/RazrFalcon/pico-args/pull/26 23 | pub struct PicoError(pub Error); 24 | 25 | impl Display for PicoError { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | match &self.0 { 28 | Error::ArgumentParsingFailed { cause } => { 29 | write!(f, "failed to parse arguments: {}", cause) 30 | } 31 | Error::Utf8ArgumentParsingFailed { value, cause } => { 32 | write!(f, "failed to parse '{}': {}", value, cause) 33 | } 34 | _ => self.0.fmt(f), 35 | } 36 | } 37 | } 38 | 39 | impl From for PicoError { 40 | fn from(err: Error) -> Self { 41 | Self(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/check.rs: -------------------------------------------------------------------------------- 1 | //! Provides functionality for checking the availablility of URLs. 2 | use std::collections::HashSet; 3 | use std::fmt; 4 | use std::fs::read_to_string; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use log::{debug, info, warn}; 8 | use once_cell::sync::Lazy; 9 | use regex::Regex; 10 | use url::Url; 11 | 12 | use cached::cached_key_result; 13 | use cached::SizedCache; 14 | 15 | use super::CheckContext; 16 | 17 | use crate::{ 18 | parse::{parse_fragments, parse_redirect}, 19 | HttpCheck, 20 | }; 21 | 22 | const PREFIX_BLACKLIST: [&str; 1] = ["https://doc.rust-lang.org"]; 23 | 24 | #[derive(Debug)] 25 | pub enum IoError { 26 | HttpUnexpectedStatus(ureq::Response), 27 | HttpFetch(ureq::Transport), 28 | FileIo(String, std::io::Error), 29 | } 30 | 31 | impl fmt::Display for IoError { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | match self { 34 | IoError::HttpUnexpectedStatus(resp) => write!( 35 | f, 36 | "Unexpected HTTP status fetching {}: {}", 37 | resp.get_url(), 38 | resp.status_text() 39 | ), 40 | IoError::HttpFetch(e) => write!(f, "Error fetching {}", e), 41 | IoError::FileIo(url, e) => write!(f, "Error fetching {}: {}", url, e), 42 | } 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | pub enum Link { 48 | File(PathBuf), 49 | Http(Url), 50 | } 51 | 52 | impl fmt::Display for Link { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | match self { 55 | Link::File(path) => write!(f, "{}", path.display()), 56 | Link::Http(url) => f.write_str(url.as_str()), 57 | } 58 | } 59 | } 60 | 61 | impl Link { 62 | /// Removes the fragment 63 | fn without_fragment(&self) -> Link { 64 | match self { 65 | Link::Http(url) => { 66 | let mut url = url.clone(); 67 | url.set_fragment(None); 68 | 69 | Link::Http(url) 70 | } 71 | _ => self.clone(), 72 | } 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | pub enum CheckError { 78 | /// An intra-doc link went unresolved by rustdoc and ended up in the final HTML 79 | IntraDocLink(String), 80 | /// A relatively linked file did not exist 81 | File(PathBuf), 82 | /// A linked HTTP URL did not exist 83 | Http(Url), 84 | /// An HTTP URL was encountered, but HTTP checking was forbidden 85 | HttpForbidden(Url), 86 | /// The linked file existed, but was missing the linked HTML anchor 87 | Fragment(Link, String, Option>), 88 | /// An error occured while trying to find whether the file or URL existed 89 | Io(Box), 90 | } 91 | 92 | impl From for CheckError { 93 | fn from(err: ureq::Error) -> Self { 94 | let io_err = match err { 95 | ureq::Error::Status(_, response) => IoError::HttpUnexpectedStatus(response), 96 | ureq::Error::Transport(err) => IoError::HttpFetch(err), 97 | }; 98 | CheckError::Io(Box::new(io_err)) 99 | } 100 | } 101 | 102 | impl fmt::Display for CheckError { 103 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 104 | match self { 105 | CheckError::IntraDocLink(text) => { 106 | write!(f, "Broken intra-doc link to {}!", text) 107 | } 108 | CheckError::File(path) => { 109 | write!(f, "Linked file at path {} does not exist!", path.display()) 110 | } 111 | CheckError::Http(url) => write!(f, "Linked URL {} does not exist!", url), 112 | CheckError::HttpForbidden(url) => write!( 113 | f, 114 | "Found HTTP link {}, but HTTP checking is forbidden!", 115 | url 116 | ), 117 | CheckError::Fragment(link, fragment, missing_parts) => match missing_parts { 118 | Some(missing_parts) => write!( 119 | f, 120 | "Fragments #{} as expected by ranged fragment #{} at {} do not exist!\n\ 121 | This is likely a bug in rustdoc itself.", 122 | missing_parts.join(", #"), 123 | fragment, 124 | link 125 | ), 126 | None => write!(f, "Fragment #{} at {} does not exist!", fragment, link), 127 | }, 128 | CheckError::Io(err) => err.fmt(f), 129 | } 130 | } 131 | } 132 | 133 | /// Check a single URL for availability. Returns `false` if it is unavailable. 134 | pub fn is_available(url: &Url, ctx: &CheckContext) -> Result<(), CheckError> { 135 | match url.scheme() { 136 | "file" => check_file_url(url, ctx), 137 | "http" | "https" => check_http_url(url, ctx), 138 | scheme @ "javascript" => { 139 | debug!("Not checking URL scheme {:?}", scheme); 140 | Ok(()) 141 | } 142 | other => { 143 | debug!("Unrecognized URL scheme {:?}", other); 144 | Ok(()) 145 | } 146 | } 147 | } 148 | 149 | cached_key_result! { 150 | CHECK_FILE: SizedCache> = SizedCache::with_size(100); 151 | Key = { link.without_fragment().to_string() }; 152 | // `fetch_html` is different depending on whether the link is being 153 | // loaded from disk or from the network. 154 | fn fragments_from( 155 | link: &Link, 156 | fetch_html: impl Fn() -> Result 157 | ) -> Result, CheckError> = { 158 | fetch_html().map(|html| parse_fragments(&html)) 159 | } 160 | } 161 | 162 | fn is_fragment_available( 163 | link: &Link, 164 | fragment: &str, 165 | fetch_html: impl Fn() -> Result, 166 | ) -> Result<(), CheckError> { 167 | // Empty fragments (e.g. file.html#) are commonly used to reach the top 168 | // of the document, see https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-fragid 169 | if fragment.is_empty() { 170 | return Ok(()); 171 | } 172 | 173 | let fragments = fragments_from(link, fetch_html)?; 174 | 175 | if fragments.contains(fragment) { 176 | return Ok(()); 177 | } 178 | 179 | // Try again with percent-decoding. 180 | // NOTE: This isn't done unconditionally because it's possible the fragment it's linking to was also percent-encoded. 181 | match percent_encoding::percent_decode(fragment.as_bytes()).decode_utf8() { 182 | Ok(cow) => { 183 | if fragments.contains(&*cow) { 184 | return Ok(()); 185 | } 186 | } 187 | // If this was invalid UTF8 after percent-decoding, it can't be in the file (since we have a `String`, not opaque bytes). 188 | // Assume it wasn't meant to be url-encoded. 189 | Err(err) => warn!("{} url-decoded to invalid UTF8: {}", fragment, err), 190 | } 191 | 192 | // Rust documentation uses `#n-m` fragments and JavaScript to highlight 193 | // a range of lines in HTML of source code, an element with `id` 194 | // attribute of (literal) "#n-m" will not exist, but elements with 195 | // `id`s n through m should, this parses the ranged n-m anchor and 196 | // checks if elements with `id`s n through m do exist 197 | static RUST_LINE_HIGLIGHT_RX: Lazy = 198 | Lazy::new(|| Regex::new(r#"^(?P[0-9]+)-(?P[0-9]+)$"#).unwrap()); 199 | match RUST_LINE_HIGLIGHT_RX.captures(fragment) { 200 | Some(capture) => match (capture.name("start"), capture.name("end")) { 201 | (Some(start_str), Some(end_str)) => { 202 | // NOTE: assumes there are less than 2.pow(32) lines in a source file 203 | let start = start_str.as_str().parse::().unwrap(); 204 | let end = end_str.as_str().parse::().unwrap(); 205 | let missing = (start..=end) 206 | .map(|i| i.to_string()) 207 | .filter(|i| !fragments.contains(i)) 208 | .collect::>(); 209 | if !missing.is_empty() { 210 | Err(CheckError::Fragment( 211 | link.clone(), 212 | fragment.to_string(), 213 | Some(missing), 214 | )) 215 | } else { 216 | Ok(()) 217 | } 218 | } 219 | _ => unreachable!("if the regex matches, it should have capture groups"), 220 | }, 221 | None => Err(CheckError::Fragment( 222 | link.clone(), 223 | fragment.to_string(), 224 | None, 225 | )), 226 | } 227 | } 228 | 229 | /// Check a URL with the "file" scheme for availability. Returns `false` if it is unavailable. 230 | fn check_file_url(url: &Url, ctx: &CheckContext) -> Result<(), CheckError> { 231 | let path = url.to_file_path().unwrap(); 232 | 233 | // determine the full path by looking if the path points to a directory, 234 | // and if so append `index.html`, this is needed as we'll try to read 235 | // the file, so `expanded_path` should point to a file not a directory 236 | let index_html; 237 | let expanded_path = if path.is_file() { 238 | &path 239 | } else if path.is_dir() && path.join("index.html").is_file() { 240 | index_html = path.join("index.html"); 241 | &index_html 242 | } else { 243 | debug!("Linked file at path {} does not exist!", path.display()); 244 | return Err(CheckError::File(path)); 245 | }; 246 | 247 | if !ctx.check_fragments { 248 | return Ok(()); 249 | } 250 | 251 | // The URL might contain a fragment. In that case we need a full GET 252 | // request to check if the fragment exists. 253 | match url.fragment() { 254 | Some(fragment) => check_file_fragment(&path, expanded_path, fragment), 255 | None => Ok(()), 256 | } 257 | } 258 | 259 | fn check_file_fragment( 260 | path: &Path, 261 | expanded_path: &Path, 262 | fragment: &str, 263 | ) -> Result<(), CheckError> { 264 | debug!( 265 | "Checking fragment {} of file {}.", 266 | fragment, 267 | expanded_path.display() 268 | ); 269 | 270 | fn get_html(expanded_path: &Path) -> Result { 271 | read_to_string(expanded_path).map_err(|err| { 272 | CheckError::Io(Box::new(IoError::FileIo( 273 | expanded_path.to_string_lossy().to_string(), 274 | err, 275 | ))) 276 | }) 277 | } 278 | 279 | let fetch_html = || { 280 | let html = get_html(expanded_path)?; 281 | if let Some(redirect) = parse_redirect(&html) { 282 | get_html(&expanded_path.parent().unwrap().join(redirect)) 283 | } else { 284 | Ok(html) 285 | } 286 | }; 287 | is_fragment_available(&Link::File(path.to_path_buf()), fragment, fetch_html) 288 | } 289 | 290 | /// Check a URL with "http" or "https" scheme for availability. Returns `Err` if it is unavailable. 291 | fn check_http_url(url: &Url, ctx: &CheckContext) -> Result<(), CheckError> { 292 | if ctx.check_http == HttpCheck::Ignored { 293 | warn!( 294 | "Skip checking {} as checking of http URLs is turned off", 295 | url 296 | ); 297 | return Ok(()); 298 | } 299 | 300 | for blacklisted_prefix in PREFIX_BLACKLIST.iter() { 301 | if url.as_str().starts_with(blacklisted_prefix) { 302 | warn!( 303 | "Skip checking {} as URL prefix is on the builtin blacklist", 304 | url 305 | ); 306 | return Ok(()); 307 | } 308 | } 309 | 310 | if ctx.check_http == HttpCheck::Forbidden { 311 | return Err(CheckError::HttpForbidden(url.clone())); 312 | } 313 | 314 | // The URL might contain a fragment. In that case we need a full GET 315 | // request to check if the fragment exists. 316 | if url.fragment().is_none() || !ctx.check_fragments { 317 | info!("Check URL {url}"); 318 | match ureq::head(url.as_str()).call() { 319 | Err(ureq::Error::Status(405, _)) => { 320 | // If HEAD isn't allowed, try sending a GET instead 321 | ureq::get(url.as_str()).call()?; 322 | Ok(()) 323 | } 324 | Err(other) => Err(other.into()), 325 | Ok(_) => Ok(()), 326 | } 327 | } else { 328 | // the URL might contain a fragment, in that case we need to check if 329 | // the fragment exists, this issues a GET request 330 | check_http_fragment(url, url.fragment().unwrap()) 331 | } 332 | } 333 | 334 | fn check_http_fragment(url: &Url, fragment: &str) -> Result<(), CheckError> { 335 | info!("Checking fragment {} of URL {}.", fragment, url.as_str()); 336 | 337 | fn get_html(url: &Url) -> Result { 338 | let resp = ureq::get(url.as_str()).call()?; 339 | Ok(resp.into_string().unwrap()) 340 | } 341 | 342 | let fetch_html = || { 343 | let html = get_html(url)?; 344 | // NOTE: only handles one level of nesting. Maybe we should have multiple levels? 345 | let redirect = parse_redirect(&html).and_then(|s| { 346 | Url::parse(&s) 347 | .map_err(|err| { 348 | warn!("failed to parse Rustdoc redirect: {}", err); 349 | }) 350 | .ok() 351 | }); 352 | if let Some(redirect) = redirect { 353 | get_html(&redirect) 354 | } else { 355 | Ok(html) 356 | } 357 | }; 358 | 359 | is_fragment_available(&Link::Http(url.clone()), fragment, fetch_html)?; 360 | Ok(()) 361 | } 362 | 363 | #[cfg(test)] 364 | mod test { 365 | use crate::HttpCheck; 366 | 367 | use super::{check_file_url, is_available, CheckContext, CheckError, Link}; 368 | use mockito::{self, mock}; 369 | use std::env; 370 | use url::Url; 371 | 372 | fn url_for(path: &str) -> Url { 373 | let cwd = env::current_dir().unwrap(); 374 | let mut parts = path.split('#'); 375 | let file_path = parts.next().unwrap(); 376 | 377 | let mut url = if file_path.ends_with('/') { 378 | Url::from_directory_path(cwd.join(file_path)) 379 | } else { 380 | Url::from_file_path(cwd.join(file_path)) 381 | } 382 | .unwrap(); 383 | 384 | url.set_fragment(parts.next()); 385 | assert_eq!(parts.count(), 0); // make sure the anchor was valid, not `a.html#x#y` 386 | 387 | url 388 | } 389 | 390 | fn test_check_file_url(path: &str) -> Result<(), CheckError> { 391 | check_file_url(&url_for(path), &CheckContext::default()) 392 | } 393 | 394 | #[test] 395 | fn test_file_path() { 396 | test_check_file_url("tests/html/index.html").unwrap(); 397 | } 398 | 399 | #[test] 400 | fn test_directory_path() { 401 | test_check_file_url("tests/html/").unwrap(); 402 | } 403 | 404 | #[test] 405 | fn test_anchors() { 406 | test_check_file_url("tests/html/anchors.html#h1").unwrap(); 407 | } 408 | 409 | #[test] 410 | fn test_hash_fragment() { 411 | test_check_file_url("tests/html/anchors.html#").unwrap(); 412 | } 413 | 414 | #[test] 415 | fn test_missing_anchors() { 416 | match test_check_file_url("tests/html/anchors.html#nonexistent") { 417 | Err(CheckError::Fragment(Link::File(path), fragment, None)) => { 418 | assert!(path.ends_with("tests/html/anchors.html")); 419 | assert_eq!("nonexistent", fragment); 420 | } 421 | x => panic!( 422 | "Expected to report missing anchor (Err(CheckError::FileAnchor)), got {:?}", 423 | x 424 | ), 425 | } 426 | } 427 | 428 | #[test] 429 | fn test_range_anchor() { 430 | test_check_file_url("tests/html/range.html#2-4").unwrap(); 431 | } 432 | 433 | #[test] 434 | fn test_missing_range_anchor() { 435 | match test_check_file_url("tests/html/range.html#4-6") { 436 | Err(CheckError::Fragment(Link::File(path), fragment, Some(missing_parts))) => { 437 | assert!(path.ends_with("tests/html/range.html")); 438 | assert_eq!("4-6", fragment); 439 | assert_eq!(missing_parts.len(), 1); 440 | assert!(missing_parts.contains(&"6".to_string())); 441 | } 442 | x => panic!( 443 | "Expected to report missing anchor (Err(CheckError::FileAnchorRange)), got {:?}", 444 | x 445 | ), 446 | } 447 | } 448 | 449 | #[test] 450 | fn test_is_available_file_path() { 451 | is_available( 452 | &url_for("tests/html/index.html#i1"), 453 | &CheckContext::default(), 454 | ) 455 | .unwrap(); 456 | } 457 | 458 | #[test] 459 | fn test_is_available_directory_path() { 460 | is_available(&url_for("tests/html/#i1"), &CheckContext::default()).unwrap(); 461 | } 462 | 463 | #[test] 464 | fn test_missing_dir_index_fragment() { 465 | match is_available( 466 | &url_for("tests/html/missing_index/#i1"), 467 | &CheckContext::default(), 468 | ) { 469 | Err(CheckError::File(path)) => assert!(path.ends_with("tests/html/missing_index")), 470 | x => panic!( 471 | "Expected to report missing anchor (Err(CheckError::File)), got {:?}", 472 | x 473 | ), 474 | } 475 | } 476 | 477 | #[test] 478 | fn test_http_check() { 479 | let root = mock("HEAD", "/test_http_check").with_status(200).create(); 480 | 481 | let mut url = mockito::server_url(); 482 | url.push_str("/test_http_check"); 483 | 484 | is_available( 485 | &Url::parse(&url).unwrap(), 486 | &CheckContext { 487 | check_http: HttpCheck::Enabled, 488 | ..CheckContext::default() 489 | }, 490 | ) 491 | .unwrap(); 492 | 493 | root.assert(); 494 | } 495 | 496 | #[test] 497 | fn test_http_check_fragment() { 498 | let root = mock("GET", "/test_http_check_fragment") 499 | .with_status(200) 500 | .with_header("content-type", "text/html") 501 | .with_body( 502 | r#" 503 | 504 | 505 | "#, 506 | ) 507 | .create(); 508 | 509 | let mut url = mockito::server_url(); 510 | url.push_str("/test_http_check_fragment#r1"); 511 | 512 | is_available( 513 | &Url::parse(&url).unwrap(), 514 | &CheckContext { 515 | check_http: HttpCheck::Enabled, 516 | ..CheckContext::default() 517 | }, 518 | ) 519 | .unwrap(); 520 | 521 | root.assert(); 522 | } 523 | 524 | #[test] 525 | fn test_missing_http_fragment() { 526 | let root = mock("GET", "/test_missing_http_fragment") 527 | .with_status(200) 528 | .with_header("content-type", "text/html") 529 | .with_body( 530 | r#" 531 | "#, 532 | ) 533 | .create(); 534 | 535 | let mut url = mockito::server_url(); 536 | url.push_str("/test_missing_http_fragment#missing"); 537 | 538 | match is_available( 539 | &Url::parse(&url).unwrap(), 540 | &CheckContext { 541 | check_http: HttpCheck::Enabled, 542 | ..CheckContext::default() 543 | }, 544 | ) { 545 | Err(CheckError::Fragment(Link::Http(url), fragment, None)) => { 546 | assert_eq!( 547 | "http://127.0.0.1:1234/test_missing_http_fragment#missing", 548 | url.to_string() 549 | ); 550 | assert_eq!("missing", fragment); 551 | } 552 | x => panic!( 553 | "Expected to report missing anchor (Err(CheckError::File)), got {:?}", 554 | x 555 | ), 556 | } 557 | 558 | root.assert(); 559 | } 560 | 561 | #[test] 562 | fn test_disabling_fragment_checks_file() { 563 | check_file_url( 564 | &url_for("tests/html/anchors.html#nonexistent"), 565 | &CheckContext { 566 | check_fragments: false, 567 | ..CheckContext::default() 568 | }, 569 | ) 570 | .unwrap(); 571 | } 572 | 573 | #[test] 574 | fn test_disabling_fragment_checks_http() { 575 | let root = mock("HEAD", "/test_disabling_fragment_checks_http") 576 | .with_status(200) 577 | .create(); 578 | 579 | let mut url = mockito::server_url(); 580 | url.push_str("/test_disabling_fragment_checks_http#missing"); 581 | 582 | is_available( 583 | &Url::parse(&url).unwrap(), 584 | &CheckContext { 585 | check_http: HttpCheck::Enabled, 586 | check_fragments: false, 587 | ..CheckContext::default() 588 | }, 589 | ) 590 | .unwrap(); 591 | 592 | root.assert(); 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::result_large_err)] 2 | 3 | use std::{ 4 | fmt, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use log::info; 9 | use rayon::prelude::*; 10 | use rayon::ThreadPoolBuilder; 11 | use url::Url; 12 | use walkdir::{DirEntry, WalkDir}; 13 | 14 | use check::is_available; 15 | 16 | pub use check::{CheckError, IoError}; 17 | 18 | mod check; 19 | mod parse; 20 | 21 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 22 | /// What behavior should deadlinks use for HTTP links? 23 | pub enum HttpCheck { 24 | /// Make an internet request to ensure the link works 25 | Enabled, 26 | /// Do nothing when encountering a link 27 | Ignored, 28 | /// Give an error when encountering a link. 29 | /// 30 | /// Note that even when HTTP links are forbidden, `doc.rust-lang.org` links are still assumed to 31 | /// be valid. 32 | Forbidden, 33 | } 34 | 35 | // NOTE: this could be Copy, but we intentionally choose not to guarantee that. 36 | #[derive(Clone, Debug)] 37 | pub struct CheckContext { 38 | pub verbose: bool, 39 | pub check_http: HttpCheck, 40 | pub check_fragments: bool, 41 | pub check_intra_doc_links: bool, 42 | } 43 | 44 | impl Default for CheckContext { 45 | fn default() -> Self { 46 | CheckContext { 47 | check_http: HttpCheck::Ignored, 48 | verbose: false, 49 | check_fragments: true, 50 | check_intra_doc_links: false, 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct FileError { 57 | pub path: PathBuf, 58 | pub errors: Vec, 59 | } 60 | 61 | impl fmt::Display for FileError { 62 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 63 | write!(f, "Found invalid urls in {}:", self.path.display())?; 64 | for e in &self.errors { 65 | write!(f, "\n\t{}", e)?; 66 | } 67 | Ok(()) 68 | } 69 | } 70 | 71 | /// Traverses a given path recursively, checking all *.html files found. 72 | /// 73 | /// For each error that occurred, print an error message. 74 | /// Returns whether an error occurred. 75 | pub fn walk_dir(dir_path: &Path, ctx: &CheckContext) -> bool { 76 | let pool = ThreadPoolBuilder::new() 77 | .num_threads(num_cpus::get()) 78 | .build() 79 | .unwrap(); 80 | 81 | pool.install(|| { 82 | unavailable_urls(dir_path, ctx) 83 | .map(|mut err| { 84 | if !ctx.verbose { 85 | err.shorten_all(dir_path); 86 | } 87 | println!("{}", err); 88 | true 89 | }) 90 | // |||||| 91 | .reduce(|| false, |initial, new| initial || new) 92 | }) 93 | } 94 | 95 | impl FileError { 96 | fn shorten_all(&mut self, prefix: &Path) { 97 | use check::Link; 98 | 99 | if let Ok(shortened) = self.path.strip_prefix(prefix) { 100 | self.path = shortened.to_path_buf(); 101 | }; 102 | for mut e in &mut self.errors { 103 | if let CheckError::File(epath) | CheckError::Fragment(Link::File(epath), _, _) = &mut e 104 | { 105 | if let Ok(shortened) = epath.strip_prefix(prefix) { 106 | *epath = shortened.to_path_buf(); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | fn is_html_file(entry: &DirEntry) -> bool { 114 | match entry.path().extension() { 115 | Some(e) => e.to_str().map(|ext| ext == "html").unwrap_or(false), 116 | None => false, 117 | } 118 | } 119 | 120 | pub fn unavailable_urls<'a>( 121 | dir_path: &'a Path, 122 | ctx: &'a CheckContext, 123 | ) -> impl ParallelIterator + 'a { 124 | let root_url = Url::from_directory_path(dir_path).unwrap(); 125 | 126 | WalkDir::new(dir_path) 127 | .into_iter() 128 | .par_bridge() 129 | .filter_map(Result::ok) 130 | .filter(|entry| entry.file_type().is_file() && is_html_file(entry)) 131 | .flat_map(move |entry| { 132 | let path = entry.path(); 133 | info!("Checking doc page at {}", path.display()); 134 | let html = std::fs::read_to_string(path) 135 | .unwrap_or_else(|e| panic!("{} did not contain valid UTF8: {}", path.display(), e)); 136 | 137 | let file_url = Url::from_file_path(path).unwrap(); 138 | let urls = parse::parse_a_hrefs(&html, &root_url, &file_url); 139 | let broken_intra_doc_links = if ctx.check_intra_doc_links { 140 | parse::broken_intra_doc_links(&html) 141 | } else { 142 | Vec::new() 143 | }; 144 | let errors = urls 145 | .into_iter() 146 | .filter_map(|url| is_available(&url, ctx).err()) 147 | .chain(broken_intra_doc_links) 148 | .collect::>(); 149 | 150 | if errors.is_empty() { 151 | None 152 | } else { 153 | let path = entry.path().to_owned(); 154 | Some(FileError { path, errors }) 155 | } 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use log::debug; 4 | use lol_html::{element, RewriteStrSettings}; 5 | use once_cell::sync::Lazy; 6 | use regex::Regex; 7 | use url::Url; 8 | 9 | use crate::CheckError; 10 | 11 | /// Return all broken intra-doc links in the source (of the form ``[`x`]``), 12 | /// which presumably should have been resolved by rustdoc. 13 | pub fn broken_intra_doc_links(html: &str) -> Vec { 14 | static BROKEN_INTRA_DOC_LINK: Lazy = 15 | Lazy::new(|| Regex::new(r#"\[(.*)\]"#).unwrap()); 16 | BROKEN_INTRA_DOC_LINK 17 | .captures_iter(html) 18 | .map(|captures| CheckError::IntraDocLink(captures.get(0).unwrap().as_str().to_owned())) 19 | .collect() 20 | } 21 | 22 | /// Return all links in the HTML file, whether or not they are broken. 23 | /// 24 | /// `root_url` is a fixed path relative to the documentation directory. For `target/doc/crate_x/y`, it's `crate_x`. 25 | /// `file_url` is the file path relative to the documentation directory; it's different for each file. 26 | /// For `target/doc/crate_x/y`, it's `crate_x/y`. 27 | /// In general, `file_url.starts_with(root_url)` should always be true. 28 | pub fn parse_a_hrefs(html: &str, root_url: &Url, file_url: &Url) -> HashSet { 29 | let mut urls = HashSet::new(); 30 | lol_html::rewrite_str( 31 | html, 32 | RewriteStrSettings { 33 | element_content_handlers: vec![element!("a[href]", |el| { 34 | let href = el.get_attribute("href").unwrap(); 35 | // base is the file path, unless path is absolute (starts with /) 36 | let (base, href) = if let Some(absolute) = href.strip_prefix('/') { 37 | // Treat absolute paths as absolute with respect to the `root_url`, not with respect to the file system. 38 | (&root_url, absolute) 39 | } else { 40 | (&file_url, href.as_str()) 41 | }; 42 | 43 | if let Ok(link) = base.join(href) { 44 | debug!("link is {:?}", link); 45 | urls.insert(link); 46 | } else { 47 | debug!("unparsable link {:?}", href); 48 | } 49 | Ok(()) 50 | })], 51 | ..RewriteStrSettings::default() 52 | }, 53 | ) 54 | .expect("html rewriting failed"); 55 | 56 | urls 57 | } 58 | 59 | /// Parses the given string as HTML and returns values of all element's id attributes 60 | pub(crate) fn parse_fragments(html: &str) -> HashSet { 61 | let mut fragments = HashSet::new(); 62 | lol_html::rewrite_str( 63 | html, 64 | RewriteStrSettings { 65 | element_content_handlers: vec![element!("*[id]", |el| { 66 | let id = el.get_attribute("id").unwrap(); 67 | fragments.insert(id); 68 | Ok(()) 69 | })], 70 | ..RewriteStrSettings::default() 71 | }, 72 | ) 73 | .expect("html rewriting failed"); 74 | 75 | fragments 76 | } 77 | 78 | pub(crate) fn parse_redirect(html: &str) -> Option { 79 | let mut url = None; 80 | lol_html::rewrite_str( 81 | html, 82 | RewriteStrSettings { 83 | element_content_handlers: vec![element!( 84 | r#"head > meta[http-equiv="refresh"]"#, 85 | |el| { 86 | let content = el.get_attribute("content").unwrap(); 87 | assert!(url.is_none(), "multiple `http-equiv` meta tags"); 88 | url = content.split("URL=").nth(1).map(|s| s.to_owned()); 89 | Ok(()) 90 | } 91 | )], 92 | ..RewriteStrSettings::default() 93 | }, 94 | ) 95 | .expect("invalid HTML"); 96 | url 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::{parse_a_hrefs, parse_fragments}; 102 | use url::Url; 103 | 104 | #[test] 105 | fn test_parse_a_hrefs() { 106 | let html = r#" 107 | 108 | 109 | 110 | a 111 | a 112 | 113 | "#; 114 | 115 | let urls = parse_a_hrefs( 116 | html, 117 | &Url::from_directory_path("/base").unwrap(), 118 | &Url::from_file_path("/base/test.html").unwrap(), 119 | ); 120 | 121 | assert!(urls.contains(&Url::from_file_path("/base/a.html").unwrap())); 122 | assert!(urls.contains(&Url::from_file_path("/base/b/c.html").unwrap())); 123 | } 124 | 125 | #[test] 126 | fn test_parse_a_hrefs_in_subdirectory() { 127 | let html = r#" 128 | 129 | 130 | 131 | a 132 | a 133 | d 134 | 135 | "#; 136 | 137 | let urls = parse_a_hrefs( 138 | html, 139 | &Url::from_directory_path("/root").unwrap(), 140 | &Url::from_file_path("/root/base/test.html").unwrap(), 141 | ); 142 | 143 | assert!(urls.contains(&Url::from_file_path("/root/base/a.html").unwrap())); 144 | assert!(urls.contains(&Url::from_file_path("/root/b/c.html").unwrap())); 145 | assert!(urls.contains(&Url::from_file_path("/root/d.html").unwrap())); 146 | } 147 | 148 | #[test] 149 | fn test_parse_fragments() { 150 | let html = r#" 151 | 152 | 153 | 154 | a 155 |

h1

156 | 157 | "#; 158 | 159 | let fragments = parse_fragments(html); 160 | 161 | assert!(fragments.contains("a")); 162 | assert!(fragments.contains("h1")); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/broken_links.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cmd; 2 | 3 | use assert_cmd::prelude::*; 4 | use predicates::prelude::PredicateBooleanExt; 5 | use predicates::str::contains; 6 | use std::process::Command; 7 | 8 | #[test] 9 | fn reports_broken_links() { 10 | Command::cargo_bin("cargo-deadlinks") 11 | .unwrap() 12 | .arg("deadlinks") 13 | .arg("--check-intra-doc-links") 14 | .current_dir("./tests/broken_links") 15 | .assert() 16 | .failure() 17 | // make sure warnings are emitted 18 | .stderr(contains("unresolved link")) 19 | .stdout( 20 | contains("Linked file at path fn.not_here.html does not exist") 21 | .and(contains("Linked file at path links does not exist!")) 22 | .and(contains("Broken intra-doc link to [links]!")) 23 | .and(contains( 24 | "Fragment #fragments at index.html does not exist!", 25 | )) 26 | .and(contains("Fragment #%FF at index.html does not exist!")), 27 | ); 28 | } 29 | 30 | #[test] 31 | fn does_not_check_intra_doc_by_default() { 32 | Command::cargo_bin("cargo-deadlinks") 33 | .unwrap() 34 | .arg("deadlinks") 35 | .current_dir("./tests/broken_links") 36 | .assert() 37 | .failure() 38 | .stderr(contains("unresolved link")) 39 | .stdout( 40 | contains("Linked file at path fn.not_here.html does not exist") 41 | .and(contains("Broken intra-doc links").not()), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /tests/broken_links/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "broken_links" 3 | version = "0.1.0" 4 | authors = ["Joshua Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/broken_links/hardcoded-target/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/broken_links/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Links to [not here](./fn.not_here.html) 2 | //! with [intra-doc](links) that will be emitted as HTML 3 | //! and intra-doc [`links`][x] that won't. 4 | //! It also has [links to](#fragments). 5 | //! [Non-unicode link](#%FF) 6 | -------------------------------------------------------------------------------- /tests/cli_args/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli_args" 3 | version = "0.1.0" 4 | authors = ["Joshua Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/cli_args/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Links to [Private] 2 | struct Private; 3 | -------------------------------------------------------------------------------- /tests/html/anchors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test HTML file 5 | 6 | 7 |

h1

8 | Go to h1 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test HTML file 5 | 6 | 7 |

Hi there

8 | to anchors h1 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/html/missing_index/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadlinks/cargo-deadlinks/627a08137a131b9cf251ad599cbc00d7fb3e99eb/tests/html/missing_index/.gitkeep -------------------------------------------------------------------------------- /tests/html/range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test HTML file 5 | 6 | 7 |
    8 |
  1. 1
  2. 9 |
  3. 2
  4. 10 |
  5. 3
  6. 11 |
  7. 4
  8. 12 |
  9. 5
  10. 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/non_existent_http_link.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cmd; 2 | 3 | use assert_cmd::prelude::*; 4 | use predicates::str::contains; 5 | use std::process::Command; 6 | 7 | mod non_existent_http_link { 8 | use super::*; 9 | 10 | #[test] 11 | fn fails_for_broken_http_link() { 12 | match std::fs::remove_dir_all("./tests/non_existent_http_link/target") { 13 | Ok(_) => {} 14 | Err(err) => match err.kind() { 15 | std::io::ErrorKind::NotFound => {} 16 | _ => panic!( 17 | "Unexpected error when trying do remove target directory: {:?}", 18 | err 19 | ), 20 | }, 21 | }; 22 | 23 | // generate docs 24 | Command::new("cargo") 25 | .arg("doc") 26 | .current_dir("./tests/non_existent_http_link") 27 | .assert() 28 | .success(); 29 | 30 | // succeeds without --check-http flag 31 | Command::cargo_bin("cargo-deadlinks") 32 | .unwrap() 33 | .arg("deadlinks") 34 | .current_dir("./tests/non_existent_http_link") 35 | .assert() 36 | .success(); 37 | 38 | // fails with --check-http flag 39 | Command::cargo_bin("cargo-deadlinks") 40 | .unwrap() 41 | .args(["deadlinks", "--check-http"]) 42 | .current_dir("./tests/non_existent_http_link") 43 | .assert() 44 | .failure() 45 | .stdout(contains( 46 | "Unexpected HTTP status fetching http://example.com/this/does/not/exist: Not Found", 47 | )); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/non_existent_http_link/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /tests/non_existent_http_link/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "non_existent_http_link" 3 | version = "0.1.0" 4 | authors = ["Maximilian Goisser "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /tests/non_existent_http_link/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Foo function 2 | /// 3 | /// Has something to do with [some website](http://example.com/this/does/not/exist). 4 | pub fn foo() {} 5 | 6 | /// Bar function 7 | pub fn bar() {} 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | #[test] 12 | fn it_works() { 13 | assert_eq!(2 + 2, 4); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/renamed_package/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "renamed_package" 3 | version = "0.1.0" 4 | authors = ["Joshua Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | [[bin]] 9 | name = "renamed-package-x" 10 | path = "src/main.rs" 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /tests/renamed_package/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/simple_project.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cmd; 2 | extern crate predicates; 3 | 4 | use assert_cmd::prelude::*; 5 | use predicate::str::{contains, starts_with}; 6 | use predicates::prelude::*; 7 | use std::env; 8 | use std::path::Path; 9 | use std::process::Command; 10 | 11 | fn remove_all(path: &str) { 12 | match std::fs::remove_dir_all(path) { 13 | Ok(_) => {} 14 | Err(err) => match err.kind() { 15 | std::io::ErrorKind::NotFound => {} 16 | _ => panic!( 17 | "Unexpected error when trying do remove target directory: {:?}", 18 | err 19 | ), 20 | }, 21 | }; 22 | } 23 | 24 | fn deadlinks() -> Command { 25 | let mut cmd = Command::cargo_bin("cargo-deadlinks").unwrap(); 26 | cmd.arg("deadlinks").env_remove("CARGO_TARGET_DIR"); 27 | cmd 28 | } 29 | 30 | #[must_use = "Assert does nothing until you specify an assert"] 31 | fn assert_doc(dir: impl AsRef, env: &[(&str, &str)]) -> assert_cmd::assert::Assert { 32 | let dir = dir.as_ref(); 33 | 34 | // generate docs 35 | Command::new("cargo") 36 | .arg("doc") 37 | .env_remove("CARGO_TARGET_DIR") 38 | .envs(env.iter().copied()) 39 | .current_dir(dir) 40 | .assert() 41 | .success(); 42 | 43 | // succeeds with generated docs 44 | deadlinks() 45 | .envs(env.iter().copied()) 46 | .current_dir(dir) 47 | .assert() 48 | } 49 | 50 | mod simple_project { 51 | use super::*; 52 | 53 | #[test] 54 | fn it_gives_help_if_cargo_toml_missing() { 55 | deadlinks() 56 | .current_dir(env::temp_dir()) 57 | .assert() 58 | .failure() 59 | .stderr( 60 | contains("help: if this is not a cargo directory, use `--dir`") 61 | .and(contains("error: could not find `Cargo.toml`")), 62 | ); 63 | } 64 | 65 | #[test] 66 | fn it_checks_okay_project_correctly() { 67 | // cargo-deadlinks generates the documentation if it does not yet exist 68 | remove_all("./tests/simple_project/target"); 69 | 70 | deadlinks() 71 | .current_dir("./tests/simple_project") 72 | .assert() 73 | .success(); 74 | 75 | assert_doc("./tests/simple_project", &[]).success(); 76 | 77 | // TODO: test that the docs aren't rebuilt 78 | remove_all("./tests/simple_project/target2"); 79 | assert_doc("./tests/simple_project", &[("CARGO_TARGET_DIR", "target2")]).success(); 80 | 81 | let target: &str = option_env!("TARGET").unwrap_or("x86_64-unknown-linux-gnu"); 82 | 83 | remove_all("./tests/simple_project/target"); 84 | assert_doc("./tests/simple_project", &[("CARGO_BUILD_TARGET", target)]).success(); 85 | 86 | // fn it_shortens_path_on_error 87 | remove_all("./tests/simple_project/target"); 88 | assert_doc("./tests/simple_project", &[]).success(); 89 | std::fs::remove_file("./tests/simple_project/target/doc/simple_project/fn.bar.html") 90 | .unwrap(); 91 | 92 | // without --debug, paths are shortened 93 | // NOTE: uses `deadlinks` to avoid rebuilding the docs 94 | Command::cargo_bin("deadlinks") 95 | .unwrap() 96 | .arg("./tests/simple_project/target/doc/simple_project") 97 | .assert() 98 | .failure() 99 | .stdout( 100 | contains("Linked file at path fn.bar.html does not exist!") 101 | .and(contains("Found invalid urls in fn.foo.html:")), 102 | ); 103 | 104 | // with --debug, paths are not shortened 105 | Command::cargo_bin("deadlinks") 106 | .unwrap() 107 | .arg("--debug") 108 | .arg("./tests/simple_project/target/doc/simple_project") 109 | .assert() 110 | .failure() 111 | .stdout( 112 | contains("tests/simple_project/target/doc/simple_project/fn.foo.html:") 113 | .and(contains( 114 | "tests/simple_project/target/doc/simple_project/fn.bar.html does not exist!", 115 | )), 116 | ); 117 | } 118 | } 119 | 120 | mod renamed_project { 121 | use super::*; 122 | 123 | #[test] 124 | fn it_follows_package_renames() { 125 | remove_all("./tests/renamed_package/target"); 126 | assert_doc("./tests/renamed_package", &[]).success(); 127 | } 128 | } 129 | 130 | mod workspace { 131 | use super::*; 132 | 133 | #[test] 134 | fn it_checks_workspaces() { 135 | remove_all("./tests/workspace/target"); 136 | assert_doc("./tests/workspace", &[]).success(); 137 | } 138 | } 139 | 140 | mod cli_args { 141 | use super::*; 142 | 143 | #[test] 144 | fn it_passes_arguments_through_to_cargo() { 145 | remove_all("./tests/cli_args/target"); 146 | deadlinks() 147 | .current_dir("./tests/cli_args") 148 | .arg("--") 149 | .arg("--document-private-items") 150 | .assert() 151 | .success(); 152 | assert!(Path::new("./tests/cli_args/target/doc/cli_args/struct.Private.html").exists()); 153 | } 154 | 155 | #[test] 156 | fn it_exits_with_success_on_info_queries() { 157 | for arg in &["-h", "--help", "-V", "--version"] { 158 | deadlinks().arg(arg).assert().success(); 159 | } 160 | } 161 | 162 | #[test] 163 | fn dir_works() { 164 | deadlinks() 165 | .arg("--dir") 166 | .arg("./tests/broken_links/hardcoded-target") 167 | .assert() 168 | .failure() 169 | .stdout(contains("Found invalid urls")); 170 | } 171 | 172 | #[test] 173 | fn missing_deadlinks_gives_helpful_error() { 174 | Command::cargo_bin("cargo-deadlinks") 175 | .unwrap() 176 | .assert() 177 | .failure() 178 | .stderr(contains("should be run as `cargo deadlinks`")); 179 | } 180 | 181 | #[test] 182 | fn too_many_args_is_an_error() { 183 | deadlinks() 184 | .arg("x") 185 | .assert() 186 | .failure() 187 | .stderr(contains("error:").and(contains("x"))); 188 | } 189 | 190 | #[test] 191 | fn version_contains_binary_name() { 192 | Command::cargo_bin("deadlinks") 193 | .unwrap() 194 | .arg("--version") 195 | .assert() 196 | .stdout(starts_with("deadlinks ")); 197 | Command::cargo_bin("cargo-deadlinks") 198 | .unwrap() 199 | .arg("deadlinks") 200 | .arg("--version") 201 | .assert() 202 | .stdout(starts_with("cargo-deadlinks ")); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tests/simple_project/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /tests/simple_project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple_project" 3 | version = "0.1.0" 4 | authors = ["Maximilian Goisser "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /tests/simple_project/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [Non-ascii link](#†) 2 | //! 3 | //!
Some text
4 | 5 | /// Foo function 6 | /// 7 | /// Has something to do with [bar](./fn.bar.html). 8 | pub fn foo() {} 9 | 10 | // not sure how to do this with intra-doc links, but this is close enough to test deadlinks. 11 | /// Bar function that links to [S](./inner/struct.S.html#associatedtype.Item) 12 | pub fn bar() {} 13 | 14 | /// [Correct link](crate::bar) 15 | pub struct Tmp {} 16 | 17 | mod inner { 18 | // This generates a link from inner/S to the crate-level S 19 | pub struct S; 20 | 21 | impl Iterator for S { 22 | type Item = (); 23 | fn next(&mut self) -> Option<()> { 24 | None 25 | } 26 | } 27 | } 28 | 29 | pub use inner::S; 30 | -------------------------------------------------------------------------------- /tests/working_http_check.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cmd; 2 | 3 | use assert_cmd::prelude::*; 4 | use predicates::prelude::PredicateBooleanExt; 5 | use predicates::str::contains; 6 | use std::process::Command; 7 | 8 | mod working_http_check { 9 | use super::*; 10 | 11 | fn remove_target(relative_path: &'static str) { 12 | match std::fs::remove_dir_all(format!("./tests/working_http_check/{}", relative_path)) { 13 | Ok(_) => {} 14 | Err(err) => match err.kind() { 15 | std::io::ErrorKind::NotFound => {} 16 | _ => panic!( 17 | "Unexpected error when trying do remove target directory: {:?}", 18 | err 19 | ), 20 | }, 21 | } 22 | } 23 | 24 | #[test] 25 | fn works() { 26 | remove_target("target"); 27 | // generate docs 28 | Command::new("cargo") 29 | .arg("doc") 30 | .current_dir("./tests/working_http_check") 31 | .assert() 32 | .success(); 33 | 34 | // succeeds with --check-http flag 35 | Command::cargo_bin("cargo-deadlinks") 36 | .unwrap() 37 | .args(["deadlinks", "--check-http"]) 38 | .current_dir("./tests/working_http_check") 39 | .assert() 40 | .success(); 41 | } 42 | 43 | #[test] 44 | fn forbid_checking() { 45 | remove_target("target2"); 46 | Command::cargo_bin("cargo-deadlinks") 47 | .unwrap() 48 | .args([ 49 | "deadlinks", 50 | "--forbid-http", 51 | "--", 52 | "--target-dir", 53 | "target2", 54 | ]) 55 | .current_dir("./tests/working_http_check") 56 | .assert() 57 | .failure() 58 | .stdout( 59 | contains("HTTP checking is forbidden").and(contains("doc.rust-lang.org").not()), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/working_http_check/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /tests/working_http_check/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "working_http_check" 3 | version = "0.1.0" 4 | authors = ["Maximilian Goisser "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /tests/working_http_check/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Foo function 2 | /// 3 | /// Has something to do with [some website](http://example.com). 4 | /// 5 | /// Should also follow 308 redirects: . 6 | /// If HEAD gives a 405 error, fall back to GET for . 7 | /// If --forbid-http is passed, it should still be ok to link to . 8 | pub fn foo() {} 9 | 10 | /// Bar function 11 | pub fn bar() {} 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn it_works() { 17 | assert_eq!(2 + 2, 4); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["a", "b"] 3 | -------------------------------------------------------------------------------- /tests/workspace/a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a" 3 | version = "0.1.0" 4 | authors = ["Joshua Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/workspace/a/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | assert_eq!(2 + 2, 4); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/workspace/b/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "b" 3 | version = "0.1.0" 4 | authors = ["Joshua Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/workspace/b/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | assert_eq!(2 + 2, 4); 6 | } 7 | } 8 | --------------------------------------------------------------------------------