fluidattacks-core-git 0.68.0

Fluid Attacks Core Git
Documentation
use std::io::ErrorKind;
use std::process::Output;
use tokio::process::Command;

use crate::errors::GitCallError;

pub async fn run(command: &'static str, git: &mut Command) -> Result<Output, GitCallError> {
    outcome(command, git.output().await)
}

pub fn run_blocking(
    command: &'static str,
    git: &mut std::process::Command,
) -> Result<Output, GitCallError> {
    outcome(command, git.output())
}

/// Why a git `command` that could not be spawned did not run.
pub fn not_run(command: &'static str, source: std::io::Error) -> GitCallError {
    // How std turns down a NUL in an argument or the directory.
    if source.kind() == ErrorKind::InvalidInput {
        GitCallError::NullByte { command }
    } else {
        GitCallError::NotStarted { command, source }
    }
}

fn outcome(command: &'static str, output: std::io::Result<Output>) -> Result<Output, GitCallError> {
    let output = output.map_err(|source| not_run(command, source))?;
    if output.status.success() {
        return Ok(output);
    }
    Err(GitCallError::Refused {
        command,
        status: output.status,
        stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn a_git_that_cannot_start_is_told_apart_from_one_that_refused() {
        let outcome = run("log", &mut Command::new("git-that-is-not-installed")).await;

        assert!(matches!(
            outcome,
            Err(GitCallError::NotStarted { command: "log", source }) if source.kind() == std::io::ErrorKind::NotFound
        ));
    }

    #[tokio::test]
    async fn a_null_byte_in_an_argument_or_the_directory_stops_the_call() {
        let mut in_argument = Command::new("git");
        in_argument.args(["log", "a\0b"]);
        let mut in_directory = Command::new("git");
        in_directory.arg("log").current_dir("a\0b");

        for git in [&mut in_argument, &mut in_directory] {
            assert!(matches!(
                run("log", git).await,
                Err(GitCallError::NullByte { command: "log" })
            ));
        }
    }

    #[tokio::test]
    async fn a_git_that_exits_with_failure_is_refused() {
        let mut failing = Command::new("sh");
        failing.args(["-c", "echo nope >&2; exit 3"]);

        let outcome = run("config", &mut failing).await;

        assert!(matches!(
            outcome,
            Err(GitCallError::Refused { command: "config", status, stderr })
                if status.code() == Some(3) && stderr == "nope"
        ));
    }
}