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(transparent)]
62    SshKey(#[from] SshKeyError),
63    #[error(transparent)]
64    Sts(#[from] StsError),
65}
66
67/// Why the SSH key could not be handed to git.
68#[derive(Debug, thiserror::Error)]
69pub enum SshKeyError {
70    #[error("the SSH key is not base64: {0}")]
71    Decode(#[from] base64::DecodeError),
72    #[error("the SSH key is empty")]
73    Empty,
74    #[error("the SSH key could not be written: {0}")]
75    File(#[source] io::Error),
76    #[error("the SSH key path cannot be quoted for the shell: {0}")]
77    Path(#[from] shlex::QuoteError),
78}
79
80/// Why an ls-remote could not ask, by the step that failed.
81#[derive(Debug, thiserror::Error)]
82pub enum LsRemoteError {
83    #[error(transparent)]
84    SshKey(#[from] SshKeyError),
85    #[error(transparent)]
86    Sts(#[from] StsError),
87    #[error(transparent)]
88    Access(#[from] HttpsAccessError),
89    #[error(transparent)]
90    Git(#[from] GitCallError),
91}
92
93/// Why the access to a remote could not be prepared.
94#[derive(Debug, thiserror::Error)]
95pub(crate) enum RemoteAccessError {
96    #[error(transparent)]
97    SshKey(#[from] SshKeyError),
98    #[error(transparent)]
99    Sts(#[from] StsError),
100    #[error(transparent)]
101    Access(#[from] HttpsAccessError),
102}
103
104impl From<RemoteAccessError> for LsRemoteError {
105    fn from(error: RemoteAccessError) -> Self {
106        match error {
107            RemoteAccessError::SshKey(error) => Self::SshKey(error),
108            RemoteAccessError::Sts(error) => Self::Sts(error),
109            RemoteAccessError::Access(error) => Self::Access(error),
110        }
111    }
112}
113
114/// Why an address is not one to reach from outside.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
116pub enum LocalAddress {
117    #[error("Requests to loopback addresses are not allowed")]
118    Loopback,
119    #[error("Requests to the local network are not allowed")]
120    LocalNetwork,
121}
122
123/// Why `https_ls_remote` could not ask, by the step that failed.
124#[derive(Debug, thiserror::Error)]
125pub enum HttpsLsRemoteError {
126    /// Only when asked to reach public hosts alone, and before anything else is tried.
127    #[error(transparent)]
128    Host(#[from] HostError),
129    #[error(transparent)]
130    Access(#[from] HttpsAccessError),
131    #[error(transparent)]
132    Git(#[from] GitCallError),
133}
134
135/// Why a host was refused before anything connected to it.
136#[derive(Debug, thiserror::Error)]
137pub enum HostError {
138    #[error("Invalid url: its host could not be read")]
139    Unparsable,
140    #[error("could not resolve the hostname: {0}")]
141    Unresolved(#[source] io::Error),
142    #[error("no IP addresses resolved for the hostname")]
143    NoAddresses,
144    #[error(transparent)]
145    Local(LocalAddress),
146}
147
148/// Why `redirected_url` could not tell where a repository lives.
149#[derive(Debug, thiserror::Error)]
150pub enum RedirectError {
151    /// The url, or a `Location` it answered, could not be read.
152    #[error("Invalid url: {0}")]
153    Unparsable(String),
154    #[error("expected an http or https url")]
155    NotHttp,
156    #[error("Hostname or IP address invalid")]
157    InvalidHost,
158    #[error("expected an https credential")]
159    NotHttpsCredential,
160    #[error("Invalid credentials: a line break or another character a header cannot carry")]
161    CredentialNotAHeader,
162    #[error(transparent)]
163    Local(LocalAddress),
164    #[error("no response in time: {0}")]
165    Timeout(#[source] reqwest::Error),
166    /// DNS, a refused connection, a TLS handshake that failed or a server that hung up.
167    #[error("the request failed: {0}")]
168    Request(#[source] reqwest::Error),
169}
170
171/// Why no session came out of STS for a role.
172#[derive(Debug, thiserror::Error)]
173pub enum StsError {
174    /// STS answered with an error, as it does for a role that does not trust us.
175    #[error("STS refused the role {arn}: {code}: {message}")]
176    Refused {
177        arn: String,
178        code: String,
179        message: String,
180    },
181    #[error("STS took too long to connect for the role {arn}: {reason}")]
182    ConnectTimeout { arn: String, reason: String },
183    /// STS took the request and did not answer in time.
184    #[error("STS took too long to answer the role {arn}: {reason}")]
185    ReadTimeout { arn: String, reason: String },
186    /// STS closed the connection before it answered.
187    #[error("STS closed the connection for the role {arn}: {reason}")]
188    ConnectionClosed { arn: String, reason: String },
189    /// No connection opened: no network, or a host that does not resolve or refuses it.
190    #[error("STS could not be connected to for the role {arn}: {reason}")]
191    NoConnection { arn: String, reason: String },
192    /// Any other request that got no answer, as one without local credentials to sign it.
193    #[error("STS could not be asked for the role {arn}: {reason}")]
194    Unreachable { arn: String, reason: String },
195    #[error("STS answered the role {arn} without credentials")]
196    NoCredentials { arn: String },
197}
198
199#[derive(Debug, thiserror::Error)]
200pub enum GitCallError {
201    #[error("git {command} could not start: {source}")]
202    NotStarted {
203        command: &'static str,
204        source: io::Error,
205    },
206    #[error("git {command} did not finish within {after:?}")]
207    TimedOut {
208        command: &'static str,
209        after: Duration,
210    },
211    /// git ran and refused, as it does for a missing repository or revision.
212    #[error("git {command} exited with {status}: {stderr}")]
213    Refused {
214        command: &'static str,
215        status: ExitStatus,
216        stderr: String,
217    },
218    /// No process can receive a NUL, so std turns the call down before it starts.
219    #[error("git {command} got an argument with an embedded null byte")]
220    NullByte { command: &'static str },
221    #[error("git {command} printed something unreadable: {reason}")]
222    Malformed {
223        command: &'static str,
224        reason: String,
225    },
226}
227
228/// Why `download_and_extract_repo` failed, by the step that failed.
229#[derive(Debug, thiserror::Error)]
230pub enum DownloadError {
231    /// The parent of the destination or the archive could not be created or
232    /// written.
233    #[error("writing the archive: {0}")]
234    Write(#[source] io::Error),
235    #[error(transparent)]
236    Fetch(#[from] FetchError),
237    #[error(transparent)]
238    Extract(#[from] ExtractError),
239    /// `git reset --hard` did not restore the working tree.
240    #[error(transparent)]
241    Reset(#[from] GitCallError),
242    #[error("pruning the working tree: {0}")]
243    Prune(#[from] PruneError),
244    /// The repository could not take the place of the destination, or the
245    /// work directory beside it could not be made.
246    #[error("placing the repository: {0}")]
247    Place(#[source] io::Error),
248}
249
250/// How the request for the archive failed.
251#[derive(Debug, thiserror::Error)]
252pub enum FetchError {
253    #[error("invalid url {url}: {reason}")]
254    InvalidUrl { url: String, reason: String },
255    #[error("not an http url: {0}")]
256    NonHttpUrl(String),
257    /// DNS, a refused connection or a TLS handshake that failed.
258    #[error("could not connect: {0}")]
259    Connect(#[source] reqwest::Error),
260    #[error("connecting timed out: {0}")]
261    ConnectTimeout(#[source] reqwest::Error),
262    /// The total timeout ran out before the response headers arrived.
263    #[error("no response in time: {0}")]
264    Timeout(#[source] reqwest::Error),
265    /// The server closed the connection before it answered.
266    #[error("the server disconnected: {0}")]
267    Disconnected(#[source] reqwest::Error),
268    /// The socket failed while the request was sent; `errno` is the one of
269    /// the socket call.
270    #[error("the socket failed: {source}")]
271    Socket {
272        reset: bool,
273        errno: Option<i32>,
274        source: reqwest::Error,
275    },
276    #[error("malformed response: {0}")]
277    Malformed(#[source] reqwest::Error),
278    #[error("the server answered {0}")]
279    Status(reqwest::StatusCode),
280    /// The body ended before its length, or the connection broke mid-read.
281    #[error("the body was cut: {0}")]
282    BodyCut(#[source] reqwest::Error),
283    /// The total timeout ran out while the body was read.
284    #[error("the body did not arrive in time: {0}")]
285    BodyTimeout(#[source] reqwest::Error),
286    #[error("the request failed: {0}")]
287    Other(#[source] reqwest::Error),
288}
289
290/// How turning the archive into a repository failed.
291#[derive(Debug, thiserror::Error)]
292pub enum ExtractError {
293    /// The archive is not gzip, its tar or deflate data is broken, or its
294    /// checksum does not match.
295    #[error("corrupt archive: {0}")]
296    Corrupt(#[source] io::Error),
297    /// The gzip stream ends before its trailer.
298    #[error("truncated archive: {0}")]
299    Truncated(#[source] io::Error),
300    /// The archive holds no top directory with a real `.git` directory.
301    #[error("the archive holds no repository: {0}")]
302    NoRepository(String),
303    #[error("extracting the archive: {0}")]
304    Io(#[source] io::Error),
305}