Skip to main content

fluidattacks_core_git/
errors.rs

1use std::io;
2use std::path::PathBuf;
3use std::process::ExitStatus;
4use std::time::Duration;
5
6/// A file call failed at `path`; `source` keeps the errno.
7#[derive(Debug, thiserror::Error)]
8#[error("{action} {}: {source}", path.display())]
9pub struct FileError {
10    pub action: &'static str,
11    pub path: PathBuf,
12    #[source]
13    pub source: io::Error,
14}
15
16/// Removing the symlinks or the out-of-scope files failed.
17#[derive(Debug, thiserror::Error)]
18pub enum PruneError {
19    #[error(transparent)]
20    File(#[from] FileError),
21    /// The git that lists the out-of-scope files failed.
22    #[error(transparent)]
23    Git(#[from] GitCallError),
24}
25
26/// Why `reset_repo` failed.
27#[derive(Debug, thiserror::Error)]
28pub enum ResetError {
29    /// `git reset --hard` failed, as it does for a path that is not the root.
30    #[error(transparent)]
31    Git(#[from] GitCallError),
32    #[error(transparent)]
33    Symlinks(#[from] FileError),
34}
35
36/// Why an `HttpsAccess` could not be built, worded as the python worded it.
37#[derive(Debug, PartialEq, Eq, thiserror::Error)]
38pub enum HttpsAccessError {
39    /// git reads a credential line by line: a line break could add a field, a nul cuts it short.
40    #[error("Invalid credentials: a line break or a nul byte")]
41    CredentialLineBreak,
42    #[error("expected an http or https url")]
43    NotHttp,
44    /// The url stays out of the message: it may carry a user and password.
45    #[error("Invalid url: its host or port could not be parsed")]
46    Unparsable,
47    #[error("expected an https credential")]
48    NotHttpsCredential,
49}
50
51/// Why `clone_to_temp` failed, by the step that failed.
52#[derive(Debug, thiserror::Error)]
53pub enum CloneError {
54    #[error(transparent)]
55    Access(#[from] HttpsAccessError),
56    #[error(transparent)]
57    Git(#[from] GitCallError),
58    /// The mirror cloned but its `.info.json` could not be written, so the clone was removed.
59    #[error(transparent)]
60    Info(#[from] FileError),
61    #[error("the SSH key is not base64: {0}")]
62    Key(#[from] base64::DecodeError),
63    #[error("the SSH key could not be written: {0}")]
64    KeyFile(#[source] io::Error),
65    #[error("the SSH key path cannot be quoted for the shell: {0}")]
66    KeyPath(#[from] shlex::QuoteError),
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum GitCallError {
71    #[error("git {command} could not start: {source}")]
72    NotStarted {
73        command: &'static str,
74        source: io::Error,
75    },
76    #[error("git {command} did not finish within {after:?}")]
77    TimedOut {
78        command: &'static str,
79        after: Duration,
80    },
81    /// git ran and refused, as it does for a missing repository or revision.
82    #[error("git {command} exited with {status}: {stderr}")]
83    Refused {
84        command: &'static str,
85        status: ExitStatus,
86        stderr: String,
87    },
88    /// No process can receive a NUL, so std turns the call down before it starts.
89    #[error("git {command} got an argument with an embedded null byte")]
90    NullByte { command: &'static str },
91    #[error("git {command} printed something unreadable: {reason}")]
92    Malformed {
93        command: &'static str,
94        reason: String,
95    },
96}
97
98/// Why `download_and_extract_repo` failed, by the step that failed.
99#[derive(Debug, thiserror::Error)]
100pub enum DownloadError {
101    /// The parent of the destination or the archive could not be created or
102    /// written.
103    #[error("writing the archive: {0}")]
104    Write(#[source] io::Error),
105    #[error(transparent)]
106    Fetch(#[from] FetchError),
107    #[error(transparent)]
108    Extract(#[from] ExtractError),
109    /// `git reset --hard` did not restore the working tree.
110    #[error(transparent)]
111    Reset(#[from] GitCallError),
112    #[error("pruning the working tree: {0}")]
113    Prune(#[from] PruneError),
114    /// The repository could not take the place of the destination, or the
115    /// work directory beside it could not be made.
116    #[error("placing the repository: {0}")]
117    Place(#[source] io::Error),
118}
119
120/// How the request for the archive failed.
121#[derive(Debug, thiserror::Error)]
122pub enum FetchError {
123    #[error("invalid url {url}: {reason}")]
124    InvalidUrl { url: String, reason: String },
125    #[error("not an http url: {0}")]
126    NonHttpUrl(String),
127    /// DNS, a refused connection or a TLS handshake that failed.
128    #[error("could not connect: {0}")]
129    Connect(#[source] reqwest::Error),
130    #[error("connecting timed out: {0}")]
131    ConnectTimeout(#[source] reqwest::Error),
132    /// The total timeout ran out before the response headers arrived.
133    #[error("no response in time: {0}")]
134    Timeout(#[source] reqwest::Error),
135    /// The server closed the connection before it answered.
136    #[error("the server disconnected: {0}")]
137    Disconnected(#[source] reqwest::Error),
138    /// The socket failed while the request was sent; `errno` is the one of
139    /// the socket call.
140    #[error("the socket failed: {source}")]
141    Socket {
142        reset: bool,
143        errno: Option<i32>,
144        source: reqwest::Error,
145    },
146    #[error("malformed response: {0}")]
147    Malformed(#[source] reqwest::Error),
148    #[error("the server answered {0}")]
149    Status(reqwest::StatusCode),
150    /// The body ended before its length, or the connection broke mid-read.
151    #[error("the body was cut: {0}")]
152    BodyCut(#[source] reqwest::Error),
153    /// The total timeout ran out while the body was read.
154    #[error("the body did not arrive in time: {0}")]
155    BodyTimeout(#[source] reqwest::Error),
156    #[error("the request failed: {0}")]
157    Other(#[source] reqwest::Error),
158}
159
160/// How turning the archive into a repository failed.
161#[derive(Debug, thiserror::Error)]
162pub enum ExtractError {
163    /// The archive is not gzip, its tar or deflate data is broken, or its
164    /// checksum does not match.
165    #[error("corrupt archive: {0}")]
166    Corrupt(#[source] io::Error),
167    /// The gzip stream ends before its trailer.
168    #[error("truncated archive: {0}")]
169    Truncated(#[source] io::Error),
170    /// The archive holds no top directory with a real `.git` directory.
171    #[error("the archive holds no repository: {0}")]
172    NoRepository(String),
173    #[error("extracting the archive: {0}")]
174    Io(#[source] io::Error),
175}