git-checks 4.3.2

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use derive_builder::Builder;
use git_checks_core::impl_prelude::*;

use crate::binary_format;

/// Reject binary files.
///
/// This check detects binaries in various formats and rejects content where they are found.
///
/// Note that new formats may be detected in the future, so this check may start rejecting commits
/// which had previously been accepted.
#[derive(Builder, Debug, Default, Clone, Copy)]
#[builder(field(private))]
pub struct RejectBinaries {}

impl RejectBinaries {
    /// Create a new builder.
    pub fn builder() -> RejectBinariesBuilder {
        Default::default()
    }
}

impl ContentCheck for RejectBinaries {
    fn name(&self) -> &str {
        "reject-binaries"
    }

    fn check(
        &self,
        ctx: &CheckGitContext,
        content: &dyn Content,
    ) -> Result<CheckResult, Box<dyn Error>> {
        let mut result = CheckResult::new();

        for diff in content.diffs() {
            match diff.status {
                StatusChange::Added | StatusChange::Modified(_) => (),
                _ => continue,
            }

            let binary_attr = ctx.check_attr("hooks-allow-binary", diff.name.as_path())?;

            let allowed_binary_type = match binary_attr {
                // Set without a value means "any".
                AttributeState::Set => continue,
                AttributeState::Value(ref v) => Some(v),
                // Any other setting means "no binary allowed".
                _ => None,
            };

            let binary_type = {
                let cat_file = ctx
                    .git()
                    .arg("cat-file")
                    .arg("blob")
                    .arg(diff.new_blob.as_str())
                    .output()
                    .map_err(|err| GitError::subcommand("cat-file", err))?;

                let stdout = cat_file.stdout;
                binary_format::detect_binary_format(stdout)
            };

            if let Some(binary_type) = binary_type {
                let type_name = binary_type.name();
                if let Some(allowed_binary_type) = allowed_binary_type {
                    if allowed_binary_type != type_name {
                        result.add_error(format!(
                            "{}adds the {} (not {}) binary `{}`.",
                            commit_prefix(content),
                            type_name,
                            allowed_binary_type,
                            diff.name,
                        ));
                    }
                } else {
                    result.add_error(format!(
                        "{}adds the {} binary `{}`.",
                        commit_prefix(content),
                        type_name,
                        diff.name,
                    ));
                }
            }
        }

        Ok(result)
    }
}

#[cfg(feature = "config")]
pub(crate) mod config {
    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
    use serde::Deserialize;
    #[cfg(test)]
    use serde_json::json;

    use crate::RejectBinaries;

    /// Configuration for the `RejectBinaries` check.
    ///
    /// No configuration available.
    ///
    /// This check is registered as a commit check with the name `"reject_binaries"` and a topic
    /// check with the name `"reject_binaries/topic"`.
    #[derive(Deserialize, Debug)]
    pub struct RejectBinariesConfig {}

    impl IntoCheck for RejectBinariesConfig {
        type Check = RejectBinaries;

        fn into_check(self) -> Self::Check {
            Default::default()
        }
    }

    register_checks! {
        RejectBinariesConfig {
            "reject_binaries" => CommitCheckConfig,
            "reject_binaries/topic" => TopicCheckConfig,
        },
    }

    #[test]
    fn test_reject_binaries_config_empty() {
        let json = json!({});
        let check: RejectBinariesConfig = serde_json::from_value(json).unwrap();

        let _ = check.into_check();
    }
}

#[cfg(test)]
mod tests {
    use git_checks_core::{Check, TopicCheck};

    use crate::test::*;
    use crate::RejectBinaries;

    const BAD_COMMIT: &str = "8ca74bdcc09a1a9e0b44c0b9e6cd8e6af9097c89";
    const BAD_COMMIT_EXE: &str = "1de03da52b04d5394b75d222f246d3524572bd12";
    const DELETE_COMMIT: &str = "141bc952382f9e9276e46a58dbb79aa8fe3ea435";
    const ATTR_COMMIT: &str = "f27e6907d6c0d112e47f63b2b91178c1c23c9f4d";
    const ATTR_COMMIT_BAD: &str = "2a9649acf5a4af7d1e8e393c11a7ab0c268a9b44";
    const FIX_ATTR_COMMIT: &str = "1c53216257cb8af8fb0dc7b1577ef63605305318";

    #[test]
    fn test_reject_binaries_builder_default() {
        assert!(RejectBinaries::builder().build().is_ok());
    }

    #[test]
    fn test_reject_binaries_name_commit() {
        let check = RejectBinaries::default();
        assert_eq!(Check::name(&check), "reject-binaries");
    }

    #[test]
    fn test_reject_binaries_name_topic() {
        let check = RejectBinaries::default();
        assert_eq!(TopicCheck::name(&check), "reject-binaries");
    }

    #[test]
    fn test_reject_binaries() {
        let check = RejectBinaries::default();
        let result = run_check("test_reject_binaries", BAD_COMMIT, check);
        test_result_errors(result, &[
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the ELF binary `elf-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-cigam-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-fat-cigam-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-fat-magic-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-magic-header`.",
            "commit 8326e6bcc8cd6718e367d889bcb64739982b6c66 adds the AR binary `ar-header`.",
            "commit 8ca74bdcc09a1a9e0b44c0b9e6cd8e6af9097c89 adds the PE binary `pe-le-header`.",
        ]);
    }

    #[test]
    fn test_reject_binaries_plus_x() {
        let check = RejectBinaries::default();
        let result = run_check("test_reject_binaries_plus_x", BAD_COMMIT_EXE, check);
        test_result_errors(result, &[
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the ELF binary `elf-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-cigam-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-fat-cigam-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-fat-magic-header`.",
            "commit e5e1e2c8db62ac8f50f249d3cf3f334ddf158936 adds the Mach-O binary `macho-magic-header`.",
            "commit 261577469c6790190d866a928dc3bd8e91d238cf adds the ELF binary `elf-header`.",
            "commit 261577469c6790190d866a928dc3bd8e91d238cf adds the Mach-O binary `macho-cigam-header`.",
            "commit 261577469c6790190d866a928dc3bd8e91d238cf adds the Mach-O binary `macho-fat-cigam-header`.",
            "commit 261577469c6790190d866a928dc3bd8e91d238cf adds the Mach-O binary `macho-fat-magic-header`.",
            "commit 261577469c6790190d866a928dc3bd8e91d238cf adds the Mach-O binary `macho-magic-header`.",
            "commit b8c710757905c59cf880ade2af288b6891b5723c adds the AR binary `ar-header`.",
            "commit 1de03da52b04d5394b75d222f246d3524572bd12 adds the PE binary `pe-le-header`.",
        ]);
    }

    #[test]
    fn test_reject_binaries_delete_file() {
        let check = RejectBinaries::default();
        let conf = make_check_conf(&check);

        let result = test_check_base(
            "test_reject_binaries_delete_file",
            DELETE_COMMIT,
            BAD_COMMIT,
            &conf,
        );
        test_result_ok(result);
    }

    #[test]
    fn test_reject_binaries_delete_file_topic() {
        let check = RejectBinaries::default();
        run_topic_check_ok(
            "test_reject_binaries_delete_file_topic",
            DELETE_COMMIT,
            check,
        );
    }

    #[test]
    fn test_reject_binaries_attr_ok() {
        let check = RejectBinaries::default();
        run_check_ok("test_reject_binaries_attr_ok", ATTR_COMMIT, check)
    }

    #[test]
    fn test_reject_binaries_attr_bad() {
        let check = RejectBinaries::default();
        let result = run_check("test_reject_binaries_attr_bad", ATTR_COMMIT_BAD, check);
        test_result_errors(result, &[
            "commit 71fa463c4bedeb40807b6c73b08ce207b0fe0309 adds the ELF (not Mach-O) binary `elf-header`.",
            "commit 71fa463c4bedeb40807b6c73b08ce207b0fe0309 adds the Mach-O (not ELF) binary `macho-cigam-header`.",
            "commit 71fa463c4bedeb40807b6c73b08ce207b0fe0309 adds the Mach-O (not ELF) binary `macho-fat-cigam-header`.",
            "commit 71fa463c4bedeb40807b6c73b08ce207b0fe0309 adds the Mach-O (not ELF) binary `macho-fat-magic-header`.",
            "commit 71fa463c4bedeb40807b6c73b08ce207b0fe0309 adds the Mach-O (not ELF) binary `macho-magic-header`.",
            "commit 107c49759fe7d603d83264cd2e013054f730f534 adds the AR (not ELF) binary `ar-header`.",
            "commit 2a9649acf5a4af7d1e8e393c11a7ab0c268a9b44 adds the PE (not ELF) binary `pe-le-header`.",
        ]);
    }

    #[test]
    fn test_reject_binaries_topic() {
        let check = RejectBinaries::default();
        let result = run_topic_check("test_reject_binaries_topic", BAD_COMMIT, check);
        test_result_errors(
            result,
            &[
                "adds the AR binary `ar-header`.",
                "adds the ELF binary `elf-header`.",
                "adds the Mach-O binary `macho-cigam-header`.",
                "adds the Mach-O binary `macho-fat-cigam-header`.",
                "adds the Mach-O binary `macho-fat-magic-header`.",
                "adds the Mach-O binary `macho-magic-header`.",
                "adds the PE binary `pe-le-header`.",
            ],
        );
    }

    #[test]
    fn test_reject_binaries_topic_attr_ok() {
        let check = RejectBinaries::default();
        run_topic_check_ok("test_reject_binaries_topic_attr_ok", ATTR_COMMIT, check)
    }

    #[test]
    fn test_reject_binaries_topic_attr_bad() {
        let check = RejectBinaries::default();
        let result = run_topic_check(
            "test_reject_binaries_topic_attr_bad",
            ATTR_COMMIT_BAD,
            check,
        );
        test_result_errors(
            result,
            &[
                "adds the AR (not ELF) binary `ar-header`.",
                "adds the ELF (not Mach-O) binary `elf-header`.",
                "adds the Mach-O (not ELF) binary `macho-cigam-header`.",
                "adds the Mach-O (not ELF) binary `macho-fat-cigam-header`.",
                "adds the Mach-O (not ELF) binary `macho-fat-magic-header`.",
                "adds the Mach-O (not ELF) binary `macho-magic-header`.",
                "adds the PE (not ELF) binary `pe-le-header`.",
            ],
        );
    }

    #[test]
    fn test_reject_binaries_topic_plus_x() {
        let check = RejectBinaries::default();
        let result = run_topic_check("test_reject_binaries_topic_plus_x", BAD_COMMIT_EXE, check);
        test_result_errors(
            result,
            &[
                "adds the AR binary `ar-header`.",
                "adds the ELF binary `elf-header`.",
                "adds the Mach-O binary `macho-cigam-header`.",
                "adds the Mach-O binary `macho-fat-cigam-header`.",
                "adds the Mach-O binary `macho-fat-magic-header`.",
                "adds the Mach-O binary `macho-magic-header`.",
                "adds the PE binary `pe-le-header`.",
            ],
        );
    }

    #[test]
    fn test_reject_binaries_topic_fixed() {
        let check = RejectBinaries::default();
        run_topic_check_ok("test_reject_binaries_topic_fixed", DELETE_COMMIT, check);
    }

    #[test]
    fn test_reject_binaries_topic_attr_fixed() {
        let check = RejectBinaries::default();
        run_topic_check_ok(
            "test_reject_binaries_topic_attr_fixed",
            FIX_ATTR_COMMIT,
            check,
        );
    }
}