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 could not be written: {0}")]
73    File(#[source] io::Error),
74    #[error("the SSH key path cannot be quoted for the shell: {0}")]
75    Path(#[from] shlex::QuoteError),
76}
77
78/// Why an ls-remote over ssh could not ask; a refusal or a timeout is an answer, not this.
79#[derive(Debug, thiserror::Error)]
80pub enum LsRemoteError {
81    #[error(transparent)]
82    SshKey(#[from] SshKeyError),
83    #[error(transparent)]
84    Git(#[from] GitCallError),
85}
86
87/// Why `validate_ssh_host` refused a url.
88#[derive(Debug, thiserror::Error)]
89pub enum SshHostError {
90    /// Not one of the ssh forms integrates stores, or a host past the DNS limits.
91    #[error("Invalid ssh url: expected user@host:path or ssh://user@host[:port]/path")]
92    Malformed,
93    #[error("Could not resolve the hostname {host}: {source}")]
94    Unresolved {
95        host: String,
96        #[source]
97        source: io::Error,
98    },
99    #[error("No IP addresses resolved for hostname: {host}")]
100    NoAddresses { host: String },
101    #[error("SSH host validation failed for {host}: {reason}")]
102    Local {
103        host: String,
104        reason: crate::local_address::LocalAddress,
105    },
106}
107
108/// Why no session came out of STS for a role.
109#[derive(Debug, thiserror::Error)]
110pub enum StsError {
111    /// STS answered with an error, as it does for a role that does not trust us.
112    #[error("STS refused the role {arn}: {code}: {message}")]
113    Refused {
114        arn: String,
115        code: String,
116        message: String,
117    },
118    #[error("STS took too long to connect for the role {arn}: {reason}")]
119    ConnectTimeout { arn: String, reason: String },
120    /// STS took the request and did not answer in time.
121    #[error("STS took too long to answer the role {arn}: {reason}")]
122    ReadTimeout { arn: String, reason: String },
123    /// STS closed the connection before it answered.
124    #[error("STS closed the connection for the role {arn}: {reason}")]
125    ConnectionClosed { arn: String, reason: String },
126    /// No connection opened: no network, or a host that does not resolve or refuses it.
127    #[error("STS could not be connected to for the role {arn}: {reason}")]
128    NoConnection { arn: String, reason: String },
129    /// Any other request that got no answer, as one without local credentials to sign it.
130    #[error("STS could not be asked for the role {arn}: {reason}")]
131    Unreachable { arn: String, reason: String },
132    #[error("STS answered the role {arn} without credentials")]
133    NoCredentials { arn: String },
134}
135
136/// Why an ls-remote over codecommit could not ask; a refusal or a timeout of git is an answer, not this.
137#[derive(Debug, thiserror::Error)]
138pub enum CodecommitLsRemoteError {
139    #[error(transparent)]
140    Sts(#[from] StsError),
141    #[error(transparent)]
142    Git(#[from] GitCallError),
143}
144
145/// Why an ls-remote over https could not ask; a refusal or a timeout of git is an answer, not this.
146#[derive(Debug, thiserror::Error)]
147pub enum HttpsLsRemoteError {
148    #[error(transparent)]
149    Access(#[from] HttpsAccessError),
150    #[error(transparent)]
151    Git(#[from] GitCallError),
152}
153
154#[derive(Debug, thiserror::Error)]
155pub enum GitCallError {
156    #[error("git {command} could not start: {source}")]
157    NotStarted {
158        command: &'static str,
159        source: io::Error,
160    },
161    #[error("git {command} did not finish within {after:?}")]
162    TimedOut {
163        command: &'static str,
164        after: Duration,
165    },
166    /// git ran and refused, as it does for a missing repository or revision.
167    #[error("git {command} exited with {status}: {stderr}")]
168    Refused {
169        command: &'static str,
170        status: ExitStatus,
171        stderr: String,
172    },
173    /// No process can receive a NUL, so std turns the call down before it starts.
174    #[error("git {command} got an argument with an embedded null byte")]
175    NullByte { command: &'static str },
176    #[error("git {command} printed something unreadable: {reason}")]
177    Malformed {
178        command: &'static str,
179        reason: String,
180    },
181}
182
183/// Why `download_and_extract_repo` failed, by the step that failed.
184#[derive(Debug, thiserror::Error)]
185pub enum DownloadError {
186    /// The parent of the destination or the archive could not be created or
187    /// written.
188    #[error("writing the archive: {0}")]
189    Write(#[source] io::Error),
190    #[error(transparent)]
191    Fetch(#[from] FetchError),
192    #[error(transparent)]
193    Extract(#[from] ExtractError),
194    /// `git reset --hard` did not restore the working tree.
195    #[error(transparent)]
196    Reset(#[from] GitCallError),
197    #[error("pruning the working tree: {0}")]
198    Prune(#[from] PruneError),
199    /// The repository could not take the place of the destination, or the
200    /// work directory beside it could not be made.
201    #[error("placing the repository: {0}")]
202    Place(#[source] io::Error),
203}
204
205/// How the request for the archive failed.
206#[derive(Debug, thiserror::Error)]
207pub enum FetchError {
208    #[error("invalid url {url}: {reason}")]
209    InvalidUrl { url: String, reason: String },
210    #[error("not an http url: {0}")]
211    NonHttpUrl(String),
212    /// DNS, a refused connection or a TLS handshake that failed.
213    #[error("could not connect: {0}")]
214    Connect(#[source] reqwest::Error),
215    #[error("connecting timed out: {0}")]
216    ConnectTimeout(#[source] reqwest::Error),
217    /// The total timeout ran out before the response headers arrived.
218    #[error("no response in time: {0}")]
219    Timeout(#[source] reqwest::Error),
220    /// The server closed the connection before it answered.
221    #[error("the server disconnected: {0}")]
222    Disconnected(#[source] reqwest::Error),
223    /// The socket failed while the request was sent; `errno` is the one of
224    /// the socket call.
225    #[error("the socket failed: {source}")]
226    Socket {
227        reset: bool,
228        errno: Option<i32>,
229        source: reqwest::Error,
230    },
231    #[error("malformed response: {0}")]
232    Malformed(#[source] reqwest::Error),
233    #[error("the server answered {0}")]
234    Status(reqwest::StatusCode),
235    /// The body ended before its length, or the connection broke mid-read.
236    #[error("the body was cut: {0}")]
237    BodyCut(#[source] reqwest::Error),
238    /// The total timeout ran out while the body was read.
239    #[error("the body did not arrive in time: {0}")]
240    BodyTimeout(#[source] reqwest::Error),
241    #[error("the request failed: {0}")]
242    Other(#[source] reqwest::Error),
243}
244
245/// How turning the archive into a repository failed.
246#[derive(Debug, thiserror::Error)]
247pub enum ExtractError {
248    /// The archive is not gzip, its tar or deflate data is broken, or its
249    /// checksum does not match.
250    #[error("corrupt archive: {0}")]
251    Corrupt(#[source] io::Error),
252    /// The gzip stream ends before its trailer.
253    #[error("truncated archive: {0}")]
254    Truncated(#[source] io::Error),
255    /// The archive holds no top directory with a real `.git` directory.
256    #[error("the archive holds no repository: {0}")]
257    NoRepository(String),
258    #[error("extracting the archive: {0}")]
259    Io(#[source] io::Error),
260}