use crates::git_checks_core::impl_prelude::*;
use binary_format;
#[derive(Builder, Debug, Default, Clone)]
#[builder(field(private))]
pub struct CheckExecutablePermissions {
#[builder(private)]
#[builder(setter(name = "_extensions"))]
#[builder(default)]
extensions: Vec<String>,
}
impl CheckExecutablePermissionsBuilder {
pub fn extensions<I>(&mut self, extensions: I) -> &mut Self
where
I: IntoIterator,
I::Item: Into<String>,
{
self.extensions = Some(extensions.into_iter().map(Into::into).collect());
self
}
}
impl CheckExecutablePermissions {
pub fn builder() -> CheckExecutablePermissionsBuilder {
CheckExecutablePermissionsBuilder::default()
}
}
impl ContentCheck for CheckExecutablePermissions {
fn name(&self) -> &str {
"check-executable-permissions"
}
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,
}
if diff.old_mode == diff.new_mode {
continue;
}
let is_executable = match diff.new_mode.as_str() {
"100755" => true,
"100644" => false,
_ => continue,
};
let filter_attr = ctx.check_attr("filter", diff.name.as_path())?;
if let AttributeState::Value(filter_name) = filter_attr {
if filter_name == "lfs" {
continue;
}
}
let executable_ext = self
.extensions
.iter()
.any(|ext| diff.name.as_str().ends_with(ext));
let looks_executable = if executable_ext {
true
} else {
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 content = &cat_file.stdout;
let shebang = content.starts_with(b"#!/") || content.starts_with(b"#! /");
if shebang {
true
} else {
binary_format::detect_binary_format(content)
.map_or(false, |fmt| fmt.is_executable())
}
};
let err = match (is_executable, looks_executable) {
(true, false) => {
Some("with executable permissions, but the file does not look executable")
},
(false, true) => {
Some("without executable permissions, but the file looks executable")
},
_ => None,
};
if let Some(msg) = err {
result.add_error(format!(
"{}adds `{}` {}.",
commit_prefix(content),
diff.name,
msg,
));
}
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use crates::git_checks_config::{CommitCheckConfig, IntoCheck, TopicCheckConfig};
use crates::inventory;
#[cfg(test)]
use crates::serde_json;
use CheckExecutablePermissions;
#[derive(Deserialize, Debug)]
pub struct CheckExecutablePermissionsConfig {
#[serde(default)]
extensions: Option<Vec<String>>,
}
impl IntoCheck for CheckExecutablePermissionsConfig {
type Check = CheckExecutablePermissions;
fn into_check(self) -> Self::Check {
let mut builder = CheckExecutablePermissions::builder();
if let Some(extensions) = self.extensions {
builder.extensions(extensions);
}
builder
.build()
.expect("configuration mismatch for `CheckExecutablePermissions`")
}
}
register_checks! {
CheckExecutablePermissionsConfig {
"check_executable_permissions" => CommitCheckConfig,
"check_executable_permissions/topic" => TopicCheckConfig,
},
}
#[test]
fn test_check_executable_permissions_config_empty() {
let json = json!({});
let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.extensions, None);
}
#[test]
fn test_check_executable_permissions_config_all_fields() {
let exp_ext: String = "md".into();
let json = json!({
"extensions": [exp_ext.clone()],
});
let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
itertools::assert_equal(&check.extensions, &Some([exp_ext]));
}
}
#[cfg(test)]
mod tests {
use test::*;
use CheckExecutablePermissions;
const BAD_TOPIC: &str = "6ad8d4932466efc57ecccd3c80def3737b5d7e9a";
const BINARY_TOPIC: &str = "f3ea55a336feec4bc6c970695c5662fadea67054";
const FIX_TOPIC: &str = "bea46a67f75380f1c17c25c7f89ffa9f47b27c06";
const BINARY_FIX_TOPIC: &str = "02487305b25d5ef3ea3fbf77813f5fbc189ef27f";
const LFS_TOPIC: &str = "58b4868402bf3f2e6160af345052c812f4cbe36f";
#[test]
fn test_check_executable_permissions_builder_default() {
assert!(CheckExecutablePermissions::builder().build().is_ok());
}
fn check_executable_permissions_check(ext: &str) -> CheckExecutablePermissions {
CheckExecutablePermissions::builder()
.extensions([ext].iter().cloned())
.build()
.unwrap()
}
#[test]
fn test_check_executable_permissions() {
let check = check_executable_permissions_check(".exe");
let result = run_check("test_check_executable_permissions", BAD_TOPIC, check);
test_result_errors(
result,
&[
"commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `is-exec` with executable \
permissions, but the file does not look executable.",
"commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec-shebang` without \
executable permissions, but the file looks executable.",
"commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec.exe` without \
executable permissions, but the file looks executable.",
],
);
}
#[test]
fn test_check_executable_permissions_binary() {
let check = check_executable_permissions_check(".exe");
let result = run_check(
"test_check_executable_permissions_binary",
BINARY_TOPIC,
check,
);
test_result_errors(result, &[
"commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `elf-header` without executable \
permissions, but the file looks executable.",
"commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-cigam-header` without \
executable permissions, but the file looks executable.",
"commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-cigam-header` \
without executable permissions, but the file looks executable.",
"commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-magic-header` \
without executable permissions, but the file looks executable.",
"commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-magic-header` without \
executable permissions, but the file looks executable.",
"commit f3ea55a336feec4bc6c970695c5662fadea67054 adds `ar-header` with executable \
permissions, but the file does not look executable.",
]);
}
#[test]
fn test_check_executable_permissions_topic() {
let check = check_executable_permissions_check(".exe");
let result = run_topic_check("test_check_executable_permissions_topic", BAD_TOPIC, check);
test_result_errors(
result,
&[
"adds `is-exec` with executable permissions, but the file does not look \
executable.",
"adds `not-exec-shebang` without executable permissions, but the file looks \
executable.",
"adds `not-exec.exe` without executable permissions, but the file looks \
executable.",
],
);
}
#[test]
fn test_check_executable_permissions_topic_fixed() {
let check = check_executable_permissions_check(".exe");
run_topic_check_ok(
"test_check_executable_permissions_topic_fixed",
FIX_TOPIC,
check,
);
}
#[test]
fn test_check_executable_permissions_topic_binary_fixed() {
let check = check_executable_permissions_check(".exe");
run_topic_check_ok(
"test_check_executable_permissions_topic_binary_fixed",
BINARY_FIX_TOPIC,
check,
);
}
#[test]
fn test_check_executable_permissions_lfs() {
let check = check_executable_permissions_check(".lfs");
let conf = make_check_conf(&check);
let result = test_check_base(
"test_check_executable_permissions_lfs",
LFS_TOPIC,
BAD_TOPIC,
&conf,
);
test_result_ok(result);
}
}