fluidattacks-core-git 0.68.0

Fluid Attacks Core Git
Documentation
use std::io;
use std::path::PathBuf;
use std::process::ExitStatus;
use std::time::Duration;

/// A file call failed at `path`; `source` keeps the errno.
#[derive(Debug, thiserror::Error)]
#[error("{action} {}: {source}", path.display())]
pub struct FileError {
    pub action: &'static str,
    pub path: PathBuf,
    #[source]
    pub source: io::Error,
}

/// Removing the symlinks or the out-of-scope files failed.
#[derive(Debug, thiserror::Error)]
pub enum PruneError {
    #[error(transparent)]
    File(#[from] FileError),
    /// The git that lists the out-of-scope files failed.
    #[error(transparent)]
    Git(#[from] GitCallError),
}

/// Why `reset_repo` failed.
#[derive(Debug, thiserror::Error)]
pub enum ResetError {
    /// `git reset --hard` failed, as it does for a path that is not the root.
    #[error(transparent)]
    Git(#[from] GitCallError),
    #[error(transparent)]
    Symlinks(#[from] FileError),
}

/// Why an `HttpsAccess` could not be built, worded as the python worded it.
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum HttpsAccessError {
    /// git reads a credential line by line: a line break could add a field, a nul cuts it short.
    #[error("Invalid credentials: a line break or a nul byte")]
    CredentialLineBreak,
    #[error("expected an http or https url")]
    NotHttp,
    /// The url stays out of the message: it may carry a user and password.
    #[error("Invalid url: its host or port could not be parsed")]
    Unparsable,
    #[error("expected an https credential")]
    NotHttpsCredential,
}

/// Why `clone_to_temp` failed, by the step that failed.
#[derive(Debug, thiserror::Error)]
pub enum CloneError {
    #[error(transparent)]
    Access(#[from] HttpsAccessError),
    #[error(transparent)]
    Git(#[from] GitCallError),
    /// The mirror cloned but its `.info.json` could not be written, so the clone was removed.
    #[error(transparent)]
    Info(#[from] FileError),
    #[error(transparent)]
    SshKey(#[from] SshKeyError),
    #[error(transparent)]
    Sts(#[from] StsError),
}

/// Why the SSH key could not be handed to git.
#[derive(Debug, thiserror::Error)]
pub enum SshKeyError {
    #[error("the SSH key is not base64: {0}")]
    Decode(#[from] base64::DecodeError),
    #[error("the SSH key could not be written: {0}")]
    File(#[source] io::Error),
    #[error("the SSH key path cannot be quoted for the shell: {0}")]
    Path(#[from] shlex::QuoteError),
}

/// Why an ls-remote over ssh could not ask; a refusal or a timeout is an answer, not this.
#[derive(Debug, thiserror::Error)]
pub enum LsRemoteError {
    #[error(transparent)]
    SshKey(#[from] SshKeyError),
    #[error(transparent)]
    Git(#[from] GitCallError),
}

/// Why `validate_ssh_host` refused a url.
#[derive(Debug, thiserror::Error)]
pub enum SshHostError {
    /// Not one of the ssh forms integrates stores, or a host past the DNS limits.
    #[error("Invalid ssh url: expected user@host:path or ssh://user@host[:port]/path")]
    Malformed,
    #[error("Could not resolve the hostname {host}: {source}")]
    Unresolved {
        host: String,
        #[source]
        source: io::Error,
    },
    #[error("No IP addresses resolved for hostname: {host}")]
    NoAddresses { host: String },
    #[error("SSH host validation failed for {host}: {reason}")]
    Local {
        host: String,
        reason: crate::local_address::LocalAddress,
    },
}

/// Why no session came out of STS for a role.
#[derive(Debug, thiserror::Error)]
pub enum StsError {
    /// STS answered with an error, as it does for a role that does not trust us.
    #[error("STS refused the role {arn}: {code}: {message}")]
    Refused {
        arn: String,
        code: String,
        message: String,
    },
    #[error("STS took too long to connect for the role {arn}: {reason}")]
    ConnectTimeout { arn: String, reason: String },
    /// STS took the request and did not answer in time.
    #[error("STS took too long to answer the role {arn}: {reason}")]
    ReadTimeout { arn: String, reason: String },
    /// STS closed the connection before it answered.
    #[error("STS closed the connection for the role {arn}: {reason}")]
    ConnectionClosed { arn: String, reason: String },
    /// No connection opened: no network, or a host that does not resolve or refuses it.
    #[error("STS could not be connected to for the role {arn}: {reason}")]
    NoConnection { arn: String, reason: String },
    /// Any other request that got no answer, as one without local credentials to sign it.
    #[error("STS could not be asked for the role {arn}: {reason}")]
    Unreachable { arn: String, reason: String },
    #[error("STS answered the role {arn} without credentials")]
    NoCredentials { arn: String },
}

/// Why an ls-remote over codecommit could not ask; a refusal or a timeout of git is an answer, not this.
#[derive(Debug, thiserror::Error)]
pub enum CodecommitLsRemoteError {
    #[error(transparent)]
    Sts(#[from] StsError),
    #[error(transparent)]
    Git(#[from] GitCallError),
}

/// Why an ls-remote over https could not ask; a refusal or a timeout of git is an answer, not this.
#[derive(Debug, thiserror::Error)]
pub enum HttpsLsRemoteError {
    #[error(transparent)]
    Access(#[from] HttpsAccessError),
    #[error(transparent)]
    Git(#[from] GitCallError),
}

#[derive(Debug, thiserror::Error)]
pub enum GitCallError {
    #[error("git {command} could not start: {source}")]
    NotStarted {
        command: &'static str,
        source: io::Error,
    },
    #[error("git {command} did not finish within {after:?}")]
    TimedOut {
        command: &'static str,
        after: Duration,
    },
    /// git ran and refused, as it does for a missing repository or revision.
    #[error("git {command} exited with {status}: {stderr}")]
    Refused {
        command: &'static str,
        status: ExitStatus,
        stderr: String,
    },
    /// No process can receive a NUL, so std turns the call down before it starts.
    #[error("git {command} got an argument with an embedded null byte")]
    NullByte { command: &'static str },
    #[error("git {command} printed something unreadable: {reason}")]
    Malformed {
        command: &'static str,
        reason: String,
    },
}

/// Why `download_and_extract_repo` failed, by the step that failed.
#[derive(Debug, thiserror::Error)]
pub enum DownloadError {
    /// The parent of the destination or the archive could not be created or
    /// written.
    #[error("writing the archive: {0}")]
    Write(#[source] io::Error),
    #[error(transparent)]
    Fetch(#[from] FetchError),
    #[error(transparent)]
    Extract(#[from] ExtractError),
    /// `git reset --hard` did not restore the working tree.
    #[error(transparent)]
    Reset(#[from] GitCallError),
    #[error("pruning the working tree: {0}")]
    Prune(#[from] PruneError),
    /// The repository could not take the place of the destination, or the
    /// work directory beside it could not be made.
    #[error("placing the repository: {0}")]
    Place(#[source] io::Error),
}

/// How the request for the archive failed.
#[derive(Debug, thiserror::Error)]
pub enum FetchError {
    #[error("invalid url {url}: {reason}")]
    InvalidUrl { url: String, reason: String },
    #[error("not an http url: {0}")]
    NonHttpUrl(String),
    /// DNS, a refused connection or a TLS handshake that failed.
    #[error("could not connect: {0}")]
    Connect(#[source] reqwest::Error),
    #[error("connecting timed out: {0}")]
    ConnectTimeout(#[source] reqwest::Error),
    /// The total timeout ran out before the response headers arrived.
    #[error("no response in time: {0}")]
    Timeout(#[source] reqwest::Error),
    /// The server closed the connection before it answered.
    #[error("the server disconnected: {0}")]
    Disconnected(#[source] reqwest::Error),
    /// The socket failed while the request was sent; `errno` is the one of
    /// the socket call.
    #[error("the socket failed: {source}")]
    Socket {
        reset: bool,
        errno: Option<i32>,
        source: reqwest::Error,
    },
    #[error("malformed response: {0}")]
    Malformed(#[source] reqwest::Error),
    #[error("the server answered {0}")]
    Status(reqwest::StatusCode),
    /// The body ended before its length, or the connection broke mid-read.
    #[error("the body was cut: {0}")]
    BodyCut(#[source] reqwest::Error),
    /// The total timeout ran out while the body was read.
    #[error("the body did not arrive in time: {0}")]
    BodyTimeout(#[source] reqwest::Error),
    #[error("the request failed: {0}")]
    Other(#[source] reqwest::Error),
}

/// How turning the archive into a repository failed.
#[derive(Debug, thiserror::Error)]
pub enum ExtractError {
    /// The archive is not gzip, its tar or deflate data is broken, or its
    /// checksum does not match.
    #[error("corrupt archive: {0}")]
    Corrupt(#[source] io::Error),
    /// The gzip stream ends before its trailer.
    #[error("truncated archive: {0}")]
    Truncated(#[source] io::Error),
    /// The archive holds no top directory with a real `.git` directory.
    #[error("the archive holds no repository: {0}")]
    NoRepository(String),
    #[error("extracting the archive: {0}")]
    Io(#[source] io::Error),
}