1use http::StatusCode;
2use serde::{Serialize, Serializer};
3use std::error::Error;
4use std::hash::Hash;
5use std::{convert::Infallible, path::PathBuf};
6use thiserror::Error;
7use tokio::task::JoinError;
8
9use super::InputContent;
10use crate::types::StatusCodeSelectorError;
11use crate::{Uri, basic_auth::BasicAuthExtractorError, utils};
12
13#[derive(Error, Debug)]
16#[non_exhaustive]
17pub enum ErrorKind {
18 #[error("Network error")]
21 NetworkRequest(#[source] reqwest::Error),
22 #[error("Error reading response body: {0}")]
24 ReadResponseBody(#[source] reqwest::Error),
25 #[error("Error creating request client: {0}")]
27 BuildRequestClient(#[source] reqwest::Error),
28
29 #[error("Network error (GitHub client)")]
31 GithubRequest(#[from] Box<octocrab::Error>),
32
33 #[error("Task failed to execute to completion")]
35 RuntimeJoin(#[from] JoinError),
36
37 #[error("Cannot read input content from file `{1}`")]
39 ReadFileInput(#[source] std::io::Error, PathBuf),
40
41 #[error("Cannot read input content from stdin")]
43 ReadStdinInput(#[from] std::io::Error),
44
45 #[error("Attempted to interpret an invalid sequence of bytes as a string")]
47 Utf8(#[from] std::str::Utf8Error),
48
49 #[error("Error creating GitHub client")]
51 BuildGithubClient(#[source] Box<octocrab::Error>),
52
53 #[error("GitHub URL is invalid: {0}")]
55 InvalidGithubUrl(String),
56
57 #[error("URL cannot be empty")]
59 EmptyUrl,
60
61 #[error("Cannot parse string `{1}` as website url: {0}")]
63 ParseUrl(#[source] url::ParseError, String),
64
65 #[error("Cannot find file")]
67 InvalidFilePath(Uri),
68
69 #[error("Cannot find fragment")]
71 InvalidFragment(Uri),
72
73 #[error("Cannot find index file within directory")]
75 InvalidIndexFile(PathBuf),
76
77 #[error("Invalid path to URL conversion: {0}")]
79 InvalidUrlFromPath(PathBuf),
80
81 #[error("Unreachable mail address: {0}: {1}")]
83 UnreachableEmailAddress(Uri, String),
84
85 #[error("Header could not be parsed.")]
89 InvalidHeader(#[from] http::header::InvalidHeaderValue),
90
91 #[error("Error with base dir `{0}` : {1}")]
93 InvalidBase(String, String),
94
95 #[error("Cannot join '{0}' with the base URL")]
97 InvalidBaseJoin(String),
98
99 #[error("Cannot convert path '{0}' to a URI")]
101 InvalidPathToUri(String),
102
103 #[error("Root dir must be an absolute path: '{0}'")]
105 RootDirMustBeAbsolute(PathBuf),
106
107 #[error("Unsupported URI type: '{0}'")]
109 UnsupportedUriType(String),
110
111 #[error("Error remapping URL: `{0}`")]
113 InvalidUrlRemap(String),
114
115 #[error("Invalid file path: {0}")]
117 InvalidFile(PathBuf),
118
119 #[error("Cannot traverse input directory: {0}")]
121 DirTraversal(#[from] ignore::Error),
122
123 #[error("UNIX glob pattern is invalid")]
125 InvalidGlobPattern(#[from] glob::PatternError),
126
127 #[error(
129 "GitHub token not specified. To check GitHub links reliably, use `--github-token` flag / `GITHUB_TOKEN` env var."
130 )]
131 MissingGitHubToken,
132
133 #[error("This URI is available in HTTPS protocol, but HTTP is provided. Use '{0}' instead")]
135 InsecureURL(Uri),
136
137 #[error("Cannot send/receive message from channel")]
139 Channel(#[from] tokio::sync::mpsc::error::SendError<InputContent>),
140
141 #[error("URL is missing a host")]
143 InvalidUrlHost,
144
145 #[error("The given URI is invalid: {0}")]
147 InvalidURI(Uri),
148
149 #[error("Invalid status code: {0}")]
151 InvalidStatusCode(u16),
152
153 #[error(r#"Rejected status code (this depends on your "accept" configuration)"#)]
155 RejectedStatusCode(StatusCode),
156
157 #[error("Error when using regex engine: {0}")]
159 Regex(#[from] regex::Error),
160
161 #[error("Too many redirects")]
163 TooManyRedirects(#[source] reqwest::Error),
164
165 #[error("Basic auth extractor error")]
167 BasicAuthExtractorError(#[from] BasicAuthExtractorError),
168
169 #[error("Cannot load cookies")]
171 Cookies(String),
172
173 #[error("Status code range error")]
175 StatusCodeSelectorError(#[from] StatusCodeSelectorError),
176}
177
178impl ErrorKind {
179 #[must_use]
185 pub fn details(&self) -> Option<String> {
186 match self {
187 ErrorKind::NetworkRequest(e) => {
188 let details = utils::reqwest::trim_error_output(e);
190
191 if e.is_connect() {
193 Some(format!("{details} Maybe a certificate error?"))
194 } else {
195 Some(details)
196 }
197 }
198 ErrorKind::RejectedStatusCode(status) => Some(
199 status
200 .canonical_reason()
201 .unwrap_or("Unknown status code")
202 .to_string(),
203 ),
204 ErrorKind::GithubRequest(e) => {
205 if let octocrab::Error::GitHub { source, .. } = &**e {
206 Some(source.message.clone())
207 } else {
208 None
209 }
210 }
211 _ => self.source().map(ToString::to_string),
212 }
213 }
214
215 #[must_use]
220 #[allow(clippy::redundant_closure_for_method_calls)]
221 pub(crate) fn reqwest_error(&self) -> Option<&reqwest::Error> {
222 self.source()
223 .and_then(|e| e.downcast_ref::<reqwest::Error>())
224 }
225
226 #[must_use]
231 #[allow(clippy::redundant_closure_for_method_calls)]
232 pub(crate) fn github_error(&self) -> Option<&octocrab::Error> {
233 self.source()
234 .and_then(|e| e.downcast_ref::<octocrab::Error>())
235 }
236}
237
238#[allow(clippy::match_same_arms)]
239impl PartialEq for ErrorKind {
240 fn eq(&self, other: &Self) -> bool {
241 match (self, other) {
242 (Self::NetworkRequest(e1), Self::NetworkRequest(e2)) => {
243 e1.to_string() == e2.to_string()
244 }
245 (Self::ReadResponseBody(e1), Self::ReadResponseBody(e2)) => {
246 e1.to_string() == e2.to_string()
247 }
248 (Self::BuildRequestClient(e1), Self::BuildRequestClient(e2)) => {
249 e1.to_string() == e2.to_string()
250 }
251 (Self::RuntimeJoin(e1), Self::RuntimeJoin(e2)) => e1.to_string() == e2.to_string(),
252 (Self::ReadFileInput(e1, s1), Self::ReadFileInput(e2, s2)) => {
253 e1.kind() == e2.kind() && s1 == s2
254 }
255 (Self::ReadStdinInput(e1), Self::ReadStdinInput(e2)) => e1.kind() == e2.kind(),
256 (Self::GithubRequest(e1), Self::GithubRequest(e2)) => e1.to_string() == e2.to_string(),
257 (Self::InvalidGithubUrl(s1), Self::InvalidGithubUrl(s2)) => s1 == s2,
258 (Self::ParseUrl(s1, e1), Self::ParseUrl(s2, e2)) => s1 == s2 && e1 == e2,
259 (Self::UnreachableEmailAddress(u1, ..), Self::UnreachableEmailAddress(u2, ..)) => {
260 u1 == u2
261 }
262 (Self::InsecureURL(u1), Self::InsecureURL(u2)) => u1 == u2,
263 (Self::InvalidGlobPattern(e1), Self::InvalidGlobPattern(e2)) => {
264 e1.msg == e2.msg && e1.pos == e2.pos
265 }
266 (Self::InvalidHeader(_), Self::InvalidHeader(_))
267 | (Self::MissingGitHubToken, Self::MissingGitHubToken) => true,
268 (Self::InvalidStatusCode(c1), Self::InvalidStatusCode(c2)) => c1 == c2,
269 (Self::InvalidUrlHost, Self::InvalidUrlHost) => true,
270 (Self::InvalidURI(u1), Self::InvalidURI(u2)) => u1 == u2,
271 (Self::Regex(e1), Self::Regex(e2)) => e1.to_string() == e2.to_string(),
272 (Self::DirTraversal(e1), Self::DirTraversal(e2)) => e1.to_string() == e2.to_string(),
273 (Self::Channel(_), Self::Channel(_)) => true,
274 (Self::TooManyRedirects(e1), Self::TooManyRedirects(e2)) => {
275 e1.to_string() == e2.to_string()
276 }
277 (Self::BasicAuthExtractorError(e1), Self::BasicAuthExtractorError(e2)) => {
278 e1.to_string() == e2.to_string()
279 }
280 (Self::Cookies(e1), Self::Cookies(e2)) => e1 == e2,
281 (Self::InvalidFile(p1), Self::InvalidFile(p2)) => p1 == p2,
282 (Self::InvalidFilePath(u1), Self::InvalidFilePath(u2)) => u1 == u2,
283 (Self::InvalidFragment(u1), Self::InvalidFragment(u2)) => u1 == u2,
284 (Self::InvalidIndexFile(p1), Self::InvalidIndexFile(p2)) => p1 == p2,
285 (Self::InvalidUrlFromPath(p1), Self::InvalidUrlFromPath(p2)) => p1 == p2,
286 (Self::InvalidBase(b1, e1), Self::InvalidBase(b2, e2)) => b1 == b2 && e1 == e2,
287 (Self::InvalidUrlRemap(r1), Self::InvalidUrlRemap(r2)) => r1 == r2,
288 (Self::EmptyUrl, Self::EmptyUrl) => true,
289
290 _ => false,
291 }
292 }
293}
294
295impl Eq for ErrorKind {}
296
297#[allow(clippy::match_same_arms)]
298impl Hash for ErrorKind {
299 fn hash<H>(&self, state: &mut H)
300 where
301 H: std::hash::Hasher,
302 {
303 match self {
304 Self::RuntimeJoin(e) => e.to_string().hash(state),
305 Self::ReadFileInput(e, s) => (e.kind(), s).hash(state),
306 Self::ReadStdinInput(e) => e.kind().hash(state),
307 Self::NetworkRequest(e) => e.to_string().hash(state),
308 Self::ReadResponseBody(e) => e.to_string().hash(state),
309 Self::BuildRequestClient(e) => e.to_string().hash(state),
310 Self::BuildGithubClient(e) => e.to_string().hash(state),
311 Self::GithubRequest(e) => e.to_string().hash(state),
312 Self::InvalidGithubUrl(s) => s.hash(state),
313 Self::DirTraversal(e) => e.to_string().hash(state),
314 Self::InvalidFile(e) => e.to_string_lossy().hash(state),
315 Self::EmptyUrl => "Empty URL".hash(state),
316 Self::ParseUrl(e, s) => (e.to_string(), s).hash(state),
317 Self::InvalidURI(u) => u.hash(state),
318 Self::InvalidUrlFromPath(p) => p.hash(state),
319 Self::Utf8(e) => e.to_string().hash(state),
320 Self::InvalidFilePath(u) => u.hash(state),
321 Self::InvalidFragment(u) => u.hash(state),
322 Self::InvalidIndexFile(p) => p.hash(state),
323 Self::UnreachableEmailAddress(u, ..) => u.hash(state),
324 Self::InsecureURL(u, ..) => u.hash(state),
325 Self::InvalidBase(base, e) => (base, e).hash(state),
326 Self::InvalidBaseJoin(s) => s.hash(state),
327 Self::InvalidPathToUri(s) => s.hash(state),
328 Self::RootDirMustBeAbsolute(s) => s.hash(state),
329 Self::UnsupportedUriType(s) => s.hash(state),
330 Self::InvalidUrlRemap(remap) => (remap).hash(state),
331 Self::InvalidHeader(e) => e.to_string().hash(state),
332 Self::InvalidGlobPattern(e) => e.to_string().hash(state),
333 Self::InvalidStatusCode(c) => c.hash(state),
334 Self::RejectedStatusCode(c) => c.hash(state),
335 Self::Channel(e) => e.to_string().hash(state),
336 Self::MissingGitHubToken | Self::InvalidUrlHost => {
337 std::mem::discriminant(self).hash(state);
338 }
339 Self::Regex(e) => e.to_string().hash(state),
340 Self::TooManyRedirects(e) => e.to_string().hash(state),
341 Self::BasicAuthExtractorError(e) => e.to_string().hash(state),
342 Self::Cookies(e) => e.to_string().hash(state),
343 Self::StatusCodeSelectorError(e) => e.to_string().hash(state),
344 }
345 }
346}
347
348impl Serialize for ErrorKind {
349 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
350 where
351 S: Serializer,
352 {
353 serializer.collect_str(self)
354 }
355}
356
357impl From<Infallible> for ErrorKind {
358 fn from(_: Infallible) -> Self {
359 unreachable!()
361 }
362}