├── .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 ] [--cargo-dir ] [options] [-- ]
18 |
19 | Options:
20 | -h --help Print this message.
21 | --dir Specify a directory to check (default is all paths that have documentation generated by cargo).
22 | --cargo-dir Specify which directory to look in for the Cargo manifest (default is the current directory).
23 | --check-http Check 'http' and 'https' scheme links.
24 | --forbid-http Give an error if HTTP links are found. This is incompatible with --check-http.
25 | --check-intra-doc-links Check for broken intra-doc links.
26 | --ignore-fragments Don't check URL fragments.
27 | --no-build Do not call `cargo doc` before running link checking. By default, deadlinks will call `cargo doc` if `--dir` is not passed.
28 | --debug Use debug output. This option is deprecated; use `RUST_LOG=debug` instead.
29 | -v --verbose Use verbose output. This option is deprecated; use `RUST_LOG=info` instead.
30 | -V --version Print version info and exit.
31 |
32 | CARGO_ARGS will be passed verbatim to `cargo doc` (as long as `--no-build` is not passed).
33 | ";
34 |
35 | #[derive(Debug, Deserialize)]
36 | struct MainArgs {
37 | arg_directory: Option,
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
9 | - 2
10 | - 3
11 | - 4
12 | - 5
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 |
--------------------------------------------------------------------------------