1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11
12use crate::binary_format;
13
14#[derive(Builder, Debug, Default, Clone)]
18#[builder(field(private))]
19pub struct CheckExecutablePermissions {
20 #[builder(private)]
21 #[builder(setter(name = "_extensions"))]
22 #[builder(default)]
23 extensions: Vec<String>,
24}
25
26impl CheckExecutablePermissionsBuilder {
27 pub fn extensions<I>(&mut self, extensions: I) -> &mut Self
34 where
35 I: IntoIterator,
36 I::Item: Into<String>,
37 {
38 self.extensions = Some(extensions.into_iter().map(Into::into).collect());
39 self
40 }
41}
42
43impl CheckExecutablePermissions {
44 pub fn builder() -> CheckExecutablePermissionsBuilder {
46 Default::default()
47 }
48}
49
50impl ContentCheck for CheckExecutablePermissions {
51 fn name(&self) -> &str {
52 "check-executable-permissions"
53 }
54
55 fn check(
56 &self,
57 ctx: &CheckGitContext,
58 content: &dyn Content,
59 ) -> Result<CheckResult, Box<dyn Error>> {
60 let mut result = CheckResult::new();
61
62 for diff in content.diffs() {
63 match diff.status {
64 StatusChange::Added | StatusChange::Modified(_) => (),
65 _ => continue,
66 }
67
68 if diff.old_mode == diff.new_mode {
70 continue;
71 }
72
73 let is_executable = match diff.new_mode.as_str() {
74 "100755" => true,
75 "100644" => false,
76 _ => continue,
77 };
78
79 let filter_attr = ctx.check_attr("filter", diff.name.as_path())?;
83 if let AttributeState::Value(filter_name) = filter_attr {
84 if filter_name == "lfs" {
85 continue;
86 }
87 }
88
89 let executable_ext = self
90 .extensions
91 .iter()
92 .any(|ext| diff.name.as_str().ends_with(ext));
93 let looks_executable = if executable_ext {
94 true
95 } else {
96 let cat_file = ctx
97 .git()
98 .arg("cat-file")
99 .arg("blob")
100 .arg(diff.new_blob.as_str())
101 .output()
102 .map_err(|err| GitError::subcommand("cat-file", err))?;
103 let content = &cat_file.stdout;
104 let shebang = content.starts_with(b"#!/") || content.starts_with(b"#! /");
105 if shebang {
106 true
107 } else {
108 binary_format::detect_binary_format(content)
109 .is_some_and(|fmt| fmt.is_executable())
110 }
111 };
112
113 let err = match (is_executable, looks_executable) {
114 (true, false) => {
115 Some("with executable permissions, but the file does not look executable")
116 },
117 (false, true) => {
118 Some("without executable permissions, but the file looks executable")
119 },
120 _ => None,
121 };
122
123 if let Some(msg) = err {
124 result.add_error(format!(
125 "{}adds `{}` {msg}.",
126 commit_prefix(content),
127 diff.name,
128 ));
129 }
130 }
131
132 Ok(result)
133 }
134}
135
136#[cfg(feature = "config")]
137pub(crate) mod config {
138 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
139 use serde::Deserialize;
140 #[cfg(test)]
141 use serde_json::json;
142
143 use crate::CheckExecutablePermissions;
144
145 #[derive(Deserialize, Debug)]
166 pub struct CheckExecutablePermissionsConfig {
167 #[serde(default)]
168 extensions: Option<Vec<String>>,
169 }
170
171 impl IntoCheck for CheckExecutablePermissionsConfig {
172 type Check = CheckExecutablePermissions;
173
174 fn into_check(self) -> Self::Check {
175 let mut builder = CheckExecutablePermissions::builder();
176
177 if let Some(extensions) = self.extensions {
178 builder.extensions(extensions);
179 }
180
181 builder
182 .build()
183 .expect("configuration mismatch for `CheckExecutablePermissions`")
184 }
185 }
186
187 register_checks! {
188 CheckExecutablePermissionsConfig {
189 "check_executable_permissions" => CommitCheckConfig,
190 "check_executable_permissions/topic" => TopicCheckConfig,
191 },
192 }
193
194 #[test]
195 fn test_check_executable_permissions_config_empty() {
196 let json = json!({});
197 let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
198
199 assert_eq!(check.extensions, None);
200
201 let check = check.into_check();
202
203 itertools::assert_equal(&check.extensions, &[] as &[&str]);
204 }
205
206 #[test]
207 fn test_check_executable_permissions_config_all_fields() {
208 let exp_ext: String = "md".into();
209 let json = json!({
210 "extensions": [exp_ext],
211 });
212 let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
213
214 itertools::assert_equal(&check.extensions, &Some([exp_ext.clone()]));
215
216 let check = check.into_check();
217
218 itertools::assert_equal(&check.extensions, &[exp_ext]);
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use git_checks_core::{Check, TopicCheck};
225
226 use crate::test::*;
227 use crate::CheckExecutablePermissions;
228
229 const BAD_TOPIC: &str = "6ad8d4932466efc57ecccd3c80def3737b5d7e9a";
230 const BINARY_TOPIC: &str = "f81d125d44faeb76d8d06de1394f35ab03d4ebf8";
231 const DELETE_TOPIC: &str = "8e007a6e84b7b2ecc2f613a653997c436c6671f4";
232 const DELETE_BINARY_TOPIC: &str = "02ba9984453024e1ca70fb1ab4c51ebd41801c47";
233 const FIX_TOPIC: &str = "bea46a67f75380f1c17c25c7f89ffa9f47b27c06";
234 const BINARY_FIX_TOPIC: &str = "3ddce524eb8aff0e6e0b7b6475d64347d0d6a57f";
235 const LFS_TOPIC: &str = "58b4868402bf3f2e6160af345052c812f4cbe36f";
236 const SYMLINK_COMMIT: &str = "00ffdf352196c16a453970de022a8b4343610ccf";
237
238 #[test]
239 fn test_check_executable_permissions_builder_default() {
240 assert!(CheckExecutablePermissions::builder().build().is_ok());
241 }
242
243 #[test]
244 fn test_check_executable_permissions_name_commit() {
245 let check = CheckExecutablePermissions::default();
246 assert_eq!(Check::name(&check), "check-executable-permissions");
247 }
248
249 #[test]
250 fn test_check_executable_permissions_name_topic() {
251 let check = CheckExecutablePermissions::default();
252 assert_eq!(TopicCheck::name(&check), "check-executable-permissions");
253 }
254
255 fn check_executable_permissions_check(ext: &str) -> CheckExecutablePermissions {
256 CheckExecutablePermissions::builder()
257 .extensions([ext].iter().cloned())
258 .build()
259 .unwrap()
260 }
261
262 #[test]
263 fn test_check_executable_permissions() {
264 let check = check_executable_permissions_check(".exe");
265 let result = run_check("test_check_executable_permissions", BAD_TOPIC, check);
266 test_result_errors(
267 result,
268 &[
269 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `is-exec` with executable \
270 permissions, but the file does not look executable.",
271 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec-shebang` without \
272 executable permissions, but the file looks executable.",
273 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec.exe` without \
274 executable permissions, but the file looks executable.",
275 ],
276 );
277 }
278
279 #[test]
280 fn test_check_executable_permissions_binary() {
281 let check = check_executable_permissions_check(".exe");
282 let result = run_check(
283 "test_check_executable_permissions_binary",
284 BINARY_TOPIC,
285 check,
286 );
287 test_result_errors(result, &[
288 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `elf-header` without executable \
289 permissions, but the file looks executable.",
290 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-cigam-header` without \
291 executable permissions, but the file looks executable.",
292 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-cigam-header` \
293 without executable permissions, but the file looks executable.",
294 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-magic-header` \
295 without executable permissions, but the file looks executable.",
296 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-magic-header` without \
297 executable permissions, but the file looks executable.",
298 "commit f3ea55a336feec4bc6c970695c5662fadea67054 adds `ar-header` with executable \
299 permissions, but the file does not look executable.",
300 "commit f81d125d44faeb76d8d06de1394f35ab03d4ebf8 adds `pe-le-header` without \
301 executable permissions, but the file looks executable.",
302 ]);
303 }
304
305 #[test]
306 fn test_check_executable_permissions_delete_files() {
307 let check = check_executable_permissions_check(".exe");
308 let conf = make_check_conf(&check);
309
310 let result = test_check_base(
311 "test_check_executable_permissions_delete_files",
312 DELETE_TOPIC,
313 BAD_TOPIC,
314 &conf,
315 );
316 test_result_ok(result);
317 }
318
319 #[test]
320 fn test_check_executable_permissions_binary_delete_files() {
321 let check = check_executable_permissions_check(".exe");
322 let conf = make_check_conf(&check);
323
324 let result = test_check_base(
325 "test_check_executable_permissions_binary_delete_files",
326 DELETE_BINARY_TOPIC,
327 BINARY_TOPIC,
328 &conf,
329 );
330 test_result_ok(result);
331 }
332
333 #[test]
334 fn test_check_executable_permissions_topic() {
335 let check = check_executable_permissions_check(".exe");
336 let result = run_topic_check("test_check_executable_permissions_topic", BAD_TOPIC, check);
337 test_result_errors(
338 result,
339 &[
340 "adds `is-exec` with executable permissions, but the file does not look \
341 executable.",
342 "adds `not-exec-shebang` without executable permissions, but the file looks \
343 executable.",
344 "adds `not-exec.exe` without executable permissions, but the file looks \
345 executable.",
346 ],
347 );
348 }
349
350 #[test]
351 fn test_check_executable_permissions_topic_delete_files() {
352 let check = check_executable_permissions_check(".exe");
353 run_topic_check_ok(
354 "test_check_executable_permissions_topic_delete_files",
355 DELETE_TOPIC,
356 check,
357 );
358 }
359
360 #[test]
361 fn test_check_executable_permissions_topic_binary_delete_files() {
362 let check = check_executable_permissions_check(".exe");
363 run_topic_check_ok(
364 "test_check_executable_permissions_topic_binary_delete_files",
365 DELETE_BINARY_TOPIC,
366 check,
367 );
368 }
369
370 #[test]
371 fn test_check_executable_permissions_topic_fixed() {
372 let check = check_executable_permissions_check(".exe");
373 run_topic_check_ok(
374 "test_check_executable_permissions_topic_fixed",
375 FIX_TOPIC,
376 check,
377 );
378 }
379
380 #[test]
381 fn test_check_executable_permissions_topic_binary_fixed() {
382 let check = check_executable_permissions_check(".exe");
383 run_topic_check_ok(
384 "test_check_executable_permissions_topic_binary_fixed",
385 BINARY_FIX_TOPIC,
386 check,
387 );
388 }
389
390 #[test]
391 fn test_check_executable_permissions_lfs() {
392 let check = check_executable_permissions_check(".lfs");
393 let conf = make_check_conf(&check);
394 let result = test_check_base(
395 "test_check_executable_permissions_lfs",
396 LFS_TOPIC,
397 BAD_TOPIC,
398 &conf,
399 );
400 test_result_ok(result);
401 }
402
403 #[test]
404 fn test_check_executable_permissions_ignore_symlinks() {
405 let check = CheckExecutablePermissions::default();
406 run_check_ok(
407 "test_check_executable_permissions_ignore_symlinks",
408 SYMLINK_COMMIT,
409 check,
410 );
411 }
412
413 #[test]
414 fn test_check_executable_permissions_ignore_symlinks_topic() {
415 let check = CheckExecutablePermissions::default();
416 run_topic_check_ok(
417 "test_check_executable_permissions_ignore_symlinks_topic",
418 SYMLINK_COMMIT,
419 check,
420 );
421 }
422}