fluidattacks-core-git 0.48.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> {
    let output = git.output().await.map_err(|source| {
        // 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 }
        }
    })?;
    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"
        ));
    }
}