Skip to main content

fluidattacks_core_git/
errors.rs

1use std::io;
2use std::path::PathBuf;
3use std::process::ExitStatus;
4use std::string::FromUtf8Error;
5use std::time::Duration;
6
7/// A file call failed at `path`; `source` keeps the errno.
8#[derive(Debug, thiserror::Error)]
9#[error("{action} {}: {source}", path.display())]
10pub struct FileError {
11    pub action: &'static str,
12    pub path: PathBuf,
13    #[source]
14    pub source: io::Error,
15}
16
17/// Removing the symlinks or the out-of-scope files failed.
18#[derive(Debug, thiserror::Error)]
19pub enum PruneError {
20    #[error(transparent)]
21    File(#[from] FileError),
22    /// The git that lists the out-of-scope files failed.
23    #[error(transparent)]
24    Git(#[from] GitCallError),
25}
26
27/// Why `reset_repo` failed.
28#[derive(Debug, thiserror::Error)]
29pub enum ResetError {
30    /// `git reset --hard` failed, as it does for a path that is not the root.
31    #[error(transparent)]
32    Git(#[from] GitCallError),
33    #[error(transparent)]
34    Symlinks(#[from] FileError),
35}
36
37/// Why an `HttpsAccess` could not be built, worded as the python worded it.
38#[derive(Debug, PartialEq, Eq, thiserror::Error)]
39pub enum HttpsAccessError {
40    /// git reads a credential line by line: a line break could add a field, a nul cuts it short.
41    #[error("Invalid credentials: a line break or a nul byte")]
42    CredentialLineBreak,
43    #[error("expected an http or https url")]
44    NotHttp,
45    /// The url stays out of the message: it may carry a user and password.
46    #[error("Invalid url: its host or port could not be parsed")]
47    Unparsable,
48    #[error("expected an https credential")]
49    NotHttpsCredential,
50}
51
52/// Why `clone_to_temp` failed, by the step that failed.
53#[derive(Debug, thiserror::Error)]
54pub enum CloneError {
55    #[error(transparent)]
56    Access(#[from] HttpsAccessError),
57    #[error(transparent)]
58    Git(#[from] GitCallError),
59    /// The mirror cloned but its `.info.json` could not be written, so the clone was removed.
60    #[error(transparent)]
61    Info(#[from] FileError),
62    #[error(transparent)]
63    SshKey(#[from] SshKeyError),
64    #[error(transparent)]
65    Sts(#[from] StsError),
66}
67
68/// Why the SSH key could not be handed to git.
69#[derive(Debug, thiserror::Error)]
70pub enum SshKeyError {
71    #[error("the SSH key is not base64: {0}")]
72    Decode(#[from] base64::DecodeError),
73    #[error("the SSH key is empty")]
74    Empty,
75    #[error("the SSH key could not be written: {0}")]
76    File(#[source] io::Error),
77    #[error("the SSH key path cannot be quoted for the shell: {0}")]
78    Path(#[from] shlex::QuoteError),
79}
80
81/// Why an ls-remote could not ask, by the step that failed.
82#[derive(Debug, thiserror::Error)]
83pub enum LsRemoteError {
84    #[error(transparent)]
85    SshKey(#[from] SshKeyError),
86    #[error(transparent)]
87    Sts(#[from] StsError),
88    #[error(transparent)]
89    Access(#[from] HttpsAccessError),
90    #[error(transparent)]
91    Git(#[from] GitCallError),
92}
93
94/// Why the access to a remote could not be prepared.
95#[derive(Debug, thiserror::Error)]
96pub(crate) enum RemoteAccessError {
97    #[error(transparent)]
98    SshKey(#[from] SshKeyError),
99    #[error(transparent)]
100    Sts(#[from] StsError),
101    #[error(transparent)]
102    Access(#[from] HttpsAccessError),
103}
104
105impl From<RemoteAccessError> for LsRemoteError {
106    fn from(error: RemoteAccessError) -> Self {
107        match error {
108            RemoteAccessError::SshKey(error) => Self::SshKey(error),
109            RemoteAccessError::Sts(error) => Self::Sts(error),
110            RemoteAccessError::Access(error) => Self::Access(error),
111        }
112    }
113}
114
115/// Why an address is not one to reach from outside.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
117pub enum LocalAddress {
118    #[error("Requests to loopback addresses are not allowed")]
119    Loopback,
120    #[error("Requests to the local network are not allowed")]
121    LocalNetwork,
122}
123
124/// Why `https_ls_remote` could not ask, by the step that failed.
125#[derive(Debug, thiserror::Error)]
126pub enum HttpsLsRemoteError {
127    /// Only when asked to reach public hosts alone, and before anything else is tried.
128    #[error(transparent)]
129    Host(#[from] HostError),
130    #[error(transparent)]
131    Access(#[from] HttpsAccessError),
132    #[error(transparent)]
133    Git(#[from] GitCallError),
134}
135
136/// Why a host was refused before anything connected to it.
137#[derive(Debug, thiserror::Error)]
138pub enum HostError {
139    #[error("Invalid url: its host could not be read")]
140    Unparsable,
141    #[error("could not resolve the hostname: {0}")]
142    Unresolved(#[source] io::Error),
143    #[error("no IP addresses resolved for the hostname")]
144    NoAddresses,
145    #[error(transparent)]
146    Local(LocalAddress),
147}
148
149/// Why `redirected_url` could not tell where a repository lives.
150#[derive(Debug, thiserror::Error)]
151pub enum RedirectError {
152    /// The url, or a `Location` it answered, could not be read.
153    #[error("Invalid url: {0}")]
154    Unparsable(String),
155    #[error("expected an http or https url")]
156    NotHttp,
157    #[error("Hostname or IP address invalid")]
158    InvalidHost,
159    #[error("expected an https credential")]
160    NotHttpsCredential,
161    #[error("Invalid credentials: a line break or another character a header cannot carry")]
162    CredentialNotAHeader,
163    #[error(transparent)]
164    Local(LocalAddress),
165    #[error("no response in time: {0}")]
166    Timeout(#[source] reqwest::Error),
167    /// DNS, a refused connection, a TLS handshake that failed or a server that hung up.
168    #[error("the request failed: {0}")]
169    Request(#[source] reqwest::Error),
170}
171
172/// Why no session came out of STS for a role.
173#[derive(Debug, thiserror::Error)]
174pub enum StsError {
175    /// STS answered with an error, as it does for a role that does not trust us.
176    #[error("STS refused the role {arn}: {code}: {message}")]
177    Refused {
178        arn: String,
179        code: String,
180        message: String,
181    },
182    #[error("STS took too long to connect for the role {arn}: {reason}")]
183    ConnectTimeout { arn: String, reason: String },
184    /// STS took the request and did not answer in time.
185    #[error("STS took too long to answer the role {arn}: {reason}")]
186    ReadTimeout { arn: String, reason: String },
187    /// STS closed the connection before it answered.
188    #[error("STS closed the connection for the role {arn}: {reason}")]
189    ConnectionClosed { arn: String, reason: String },
190    /// No connection opened: no network, or a host that does not resolve or refuses it.
191    #[error("STS could not be connected to for the role {arn}: {reason}")]
192    NoConnection { arn: String, reason: String },
193    /// Any other request that got no answer, as one without local credentials to sign it.
194    #[error("STS could not be asked for the role {arn}: {reason}")]
195    Unreachable { arn: String, reason: String },
196    #[error("STS answered the role {arn} without credentials")]
197    NoCredentials { arn: String },
198}
199
200#[derive(Debug, thiserror::Error)]
201pub enum GitCallError {
202    #[error("git {command} could not start: {source}")]
203    NotStarted {
204        command: &'static str,
205        source: io::Error,
206    },
207    #[error("git {command} did not finish within {after:?}")]
208    TimedOut {
209        command: &'static str,
210        after: Duration,
211    },
212    /// git ran and refused, as it does for a missing repository or revision.
213    #[error("git {command} exited with {status}: {stderr}")]
214    Refused {
215        command: &'static str,
216        status: ExitStatus,
217        stderr: String,
218    },
219    /// No process can receive a NUL, so std turns the call down before it starts.
220    #[error("git {command} got an argument with an embedded null byte")]
221    NullByte { command: &'static str },
222    #[error("git {command} printed something unreadable: {reason}")]
223    Malformed {
224        command: &'static str,
225        reason: String,
226    },
227}
228
229/// A `warp-cli` call that did not answer.
230#[derive(Debug, thiserror::Error)]
231pub enum WarpCliError {
232    #[error("warp-cli could not start: {source}")]
233    NotStarted { source: io::Error },
234    /// The process is killed.
235    #[error("warp-cli did not finish within {after:?}")]
236    TimedOut { after: Duration },
237    /// warp-cli could not reach its daemon, as while the daemon restarts.
238    /// The text is the stderr alone, which callers match on.
239    #[error("{stderr}")]
240    DaemonUnreachable { status: ExitStatus, stderr: String },
241    /// The text is the stderr alone, which callers match on.
242    #[error("{stderr}")]
243    Refused { status: ExitStatus, stderr: String },
244    #[error("warp-cli printed something unreadable: {source}")]
245    Malformed { source: FromUtf8Error },
246}
247
248/// Why the id of a virtual network could not be read.
249#[derive(Debug, thiserror::Error)]
250pub enum VnetLookupError {
251    #[error(transparent)]
252    Cli(#[from] WarpCliError),
253    #[error("Failed to find virtual network {name}")]
254    UnknownNetwork { name: String },
255    /// Listed, but with an empty id.
256    #[error("virtual network {name} has no id")]
257    Unidentified { name: String },
258}
259
260/// Why warp could not connect.
261#[derive(Debug, thiserror::Error)]
262pub enum WarpConnectError {
263    #[error(transparent)]
264    Cli(#[from] WarpCliError),
265    #[error("Failed to resolve DNS")]
266    DnsUnresolved,
267}
268
269/// Why `download_and_extract_repo` failed, by the step that failed.
270#[derive(Debug, thiserror::Error)]
271pub enum DownloadError {
272    /// The parent of the destination or the archive could not be created or
273    /// written.
274    #[error("writing the archive: {0}")]
275    Write(#[source] io::Error),
276    #[error(transparent)]
277    Fetch(#[from] FetchError),
278    #[error(transparent)]
279    Extract(#[from] ExtractError),
280    /// `git reset --hard` did not restore the working tree.
281    #[error(transparent)]
282    Reset(#[from] GitCallError),
283    #[error("pruning the working tree: {0}")]
284    Prune(#[from] PruneError),
285    /// The repository could not take the place of the destination, or the
286    /// work directory beside it could not be made.
287    #[error("placing the repository: {0}")]
288    Place(#[source] io::Error),
289}
290
291/// How the request for the archive failed.
292#[derive(Debug, thiserror::Error)]
293pub enum FetchError {
294    #[error("invalid url {url}: {reason}")]
295    InvalidUrl { url: String, reason: String },
296    #[error("not an http url: {0}")]
297    NonHttpUrl(String),
298    /// DNS, a refused connection or a TLS handshake that failed.
299    #[error("could not connect: {0}")]
300    Connect(#[source] reqwest::Error),
301    #[error("connecting timed out: {0}")]
302    ConnectTimeout(#[source] reqwest::Error),
303    /// The total timeout ran out before the response headers arrived.
304    #[error("no response in time: {0}")]
305    Timeout(#[source] reqwest::Error),
306    /// The server closed the connection before it answered.
307    #[error("the server disconnected: {0}")]
308    Disconnected(#[source] reqwest::Error),
309    /// The socket failed while the request was sent; `errno` is the one of
310    /// the socket call.
311    #[error("the socket failed: {source}")]
312    Socket {
313        reset: bool,
314        errno: Option<i32>,
315        source: reqwest::Error,
316    },
317    #[error("malformed response: {0}")]
318    Malformed(#[source] reqwest::Error),
319    #[error("the server answered {0}")]
320    Status(reqwest::StatusCode),
321    /// The body ended before its length, or the connection broke mid-read.
322    #[error("the body was cut: {0}")]
323    BodyCut(#[source] reqwest::Error),
324    /// The total timeout ran out while the body was read.
325    #[error("the body did not arrive in time: {0}")]
326    BodyTimeout(#[source] reqwest::Error),
327    #[error("the request failed: {0}")]
328    Other(#[source] reqwest::Error),
329}
330
331/// How turning the archive into a repository failed.
332#[derive(Debug, thiserror::Error)]
333pub enum ExtractError {
334    /// The archive is not gzip, its tar or deflate data is broken, or its
335    /// checksum does not match.
336    #[error("corrupt archive: {0}")]
337    Corrupt(#[source] io::Error),
338    /// The gzip stream ends before its trailer.
339    #[error("truncated archive: {0}")]
340    Truncated(#[source] io::Error),
341    /// The archive holds no top directory with a real `.git` directory.
342    #[error("the archive holds no repository: {0}")]
343    NoRepository(String),
344    #[error("extracting the archive: {0}")]
345    Io(#[source] io::Error),
346}