alint_rules/
executable_has_shebang.rs1use alint_core::{Context, Error, Level, Result, Rule, RuleSpec, Scope, Violation};
15
16#[derive(Debug)]
17pub struct ExecutableHasShebangRule {
18 id: String,
19 level: Level,
20 policy_url: Option<String>,
21 message: Option<String>,
22 scope: Scope,
23}
24
25impl Rule for ExecutableHasShebangRule {
26 fn id(&self) -> &str {
27 &self.id
28 }
29 fn level(&self) -> Level {
30 self.level
31 }
32 fn policy_url(&self) -> Option<&str> {
33 self.policy_url.as_deref()
34 }
35
36 #[cfg(unix)]
37 fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
38 use std::os::unix::fs::PermissionsExt;
39
40 let mut violations = Vec::new();
41 for entry in ctx.index.files() {
42 if !self.scope.matches(&entry.path) {
43 continue;
44 }
45 let full = ctx.root.join(&entry.path);
46 let Ok(meta) = std::fs::metadata(&full) else {
47 continue;
48 };
49 if meta.permissions().mode() & 0o111 == 0 {
50 continue;
51 }
52 let Ok(bytes) = std::fs::read(&full) else {
53 continue;
54 };
55 if !bytes.starts_with(b"#!") {
56 let msg = self
57 .message
58 .clone()
59 .unwrap_or_else(|| "executable file has no shebang (#!)".to_string());
60 violations.push(Violation::new(msg).with_path(&entry.path));
61 }
62 }
63 Ok(violations)
64 }
65
66 #[cfg(not(unix))]
67 fn evaluate(&self, _ctx: &Context<'_>) -> Result<Vec<Violation>> {
68 Ok(Vec::new())
69 }
70}
71
72pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
73 let paths = spec.paths.as_ref().ok_or_else(|| {
74 Error::rule_config(&spec.id, "executable_has_shebang requires a `paths` field")
75 })?;
76 if spec.fix.is_some() {
77 return Err(Error::rule_config(
78 &spec.id,
79 "executable_has_shebang has no fix op — add a shebang or clear +x is a human call",
80 ));
81 }
82 Ok(Box::new(ExecutableHasShebangRule {
83 id: spec.id.clone(),
84 level: spec.level,
85 policy_url: spec.policy_url.clone(),
86 message: spec.message.clone(),
87 scope: Scope::from_paths_spec(paths)?,
88 }))
89}