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 `{}` {}.",
126 commit_prefix(content),
127 diff.name,
128 msg,
129 ));
130 }
131 }
132
133 Ok(result)
134 }
135}
136
137#[cfg(feature = "config")]
138pub(crate) mod config {
139 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
140 use serde::Deserialize;
141 #[cfg(test)]
142 use serde_json::json;
143
144 use crate::CheckExecutablePermissions;
145
146 #[derive(Deserialize, Debug)]
167 pub struct CheckExecutablePermissionsConfig {
168 #[serde(default)]
169 extensions: Option<Vec<String>>,
170 }
171
172 impl IntoCheck for CheckExecutablePermissionsConfig {
173 type Check = CheckExecutablePermissions;
174
175 fn into_check(self) -> Self::Check {
176 let mut builder = CheckExecutablePermissions::builder();
177
178 if let Some(extensions) = self.extensions {
179 builder.extensions(extensions);
180 }
181
182 builder
183 .build()
184 .expect("configuration mismatch for `CheckExecutablePermissions`")
185 }
186 }
187
188 register_checks! {
189 CheckExecutablePermissionsConfig {
190 "check_executable_permissions" => CommitCheckConfig,
191 "check_executable_permissions/topic" => TopicCheckConfig,
192 },
193 }
194
195 #[test]
196 fn test_check_executable_permissions_config_empty() {
197 let json = json!({});
198 let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
199
200 assert_eq!(check.extensions, None);
201
202 let check = check.into_check();
203
204 itertools::assert_equal(&check.extensions, &[] as &[&str]);
205 }
206
207 #[test]
208 fn test_check_executable_permissions_config_all_fields() {
209 let exp_ext: String = "md".into();
210 let json = json!({
211 "extensions": [exp_ext],
212 });
213 let check: CheckExecutablePermissionsConfig = serde_json::from_value(json).unwrap();
214
215 itertools::assert_equal(&check.extensions, &Some([exp_ext.clone()]));
216
217 let check = check.into_check();
218
219 itertools::assert_equal(&check.extensions, &[exp_ext]);
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use git_checks_core::{Check, TopicCheck};
226
227 use crate::test::*;
228 use crate::CheckExecutablePermissions;
229
230 const BAD_TOPIC: &str = "6ad8d4932466efc57ecccd3c80def3737b5d7e9a";
231 const BINARY_TOPIC: &str = "f81d125d44faeb76d8d06de1394f35ab03d4ebf8";
232 const DELETE_TOPIC: &str = "8e007a6e84b7b2ecc2f613a653997c436c6671f4";
233 const DELETE_BINARY_TOPIC: &str = "02ba9984453024e1ca70fb1ab4c51ebd41801c47";
234 const FIX_TOPIC: &str = "bea46a67f75380f1c17c25c7f89ffa9f47b27c06";
235 const BINARY_FIX_TOPIC: &str = "3ddce524eb8aff0e6e0b7b6475d64347d0d6a57f";
236 const LFS_TOPIC: &str = "58b4868402bf3f2e6160af345052c812f4cbe36f";
237 const SYMLINK_COMMIT: &str = "00ffdf352196c16a453970de022a8b4343610ccf";
238
239 #[test]
240 fn test_check_executable_permissions_builder_default() {
241 assert!(CheckExecutablePermissions::builder().build().is_ok());
242 }
243
244 #[test]
245 fn test_check_executable_permissions_name_commit() {
246 let check = CheckExecutablePermissions::default();
247 assert_eq!(Check::name(&check), "check-executable-permissions");
248 }
249
250 #[test]
251 fn test_check_executable_permissions_name_topic() {
252 let check = CheckExecutablePermissions::default();
253 assert_eq!(TopicCheck::name(&check), "check-executable-permissions");
254 }
255
256 fn check_executable_permissions_check(ext: &str) -> CheckExecutablePermissions {
257 CheckExecutablePermissions::builder()
258 .extensions([ext].iter().cloned())
259 .build()
260 .unwrap()
261 }
262
263 #[test]
264 fn test_check_executable_permissions() {
265 let check = check_executable_permissions_check(".exe");
266 let result = run_check("test_check_executable_permissions", BAD_TOPIC, check);
267 test_result_errors(
268 result,
269 &[
270 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `is-exec` with executable \
271 permissions, but the file does not look executable.",
272 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec-shebang` without \
273 executable permissions, but the file looks executable.",
274 "commit 6ad8d4932466efc57ecccd3c80def3737b5d7e9a adds `not-exec.exe` without \
275 executable permissions, but the file looks executable.",
276 ],
277 );
278 }
279
280 #[test]
281 fn test_check_executable_permissions_binary() {
282 let check = check_executable_permissions_check(".exe");
283 let result = run_check(
284 "test_check_executable_permissions_binary",
285 BINARY_TOPIC,
286 check,
287 );
288 test_result_errors(result, &[
289 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `elf-header` without executable \
290 permissions, but the file looks executable.",
291 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-cigam-header` without \
292 executable permissions, but the file looks executable.",
293 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-cigam-header` \
294 without executable permissions, but the file looks executable.",
295 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-fat-magic-header` \
296 without executable permissions, but the file looks executable.",
297 "commit f5fd493ca51556d6cd0c42dfc8003925d77441f3 adds `macho-magic-header` without \
298 executable permissions, but the file looks executable.",
299 "commit f3ea55a336feec4bc6c970695c5662fadea67054 adds `ar-header` with executable \
300 permissions, but the file does not look executable.",
301 "commit f81d125d44faeb76d8d06de1394f35ab03d4ebf8 adds `pe-le-header` without \
302 executable permissions, but the file looks executable.",
303 ]);
304 }
305
306 #[test]
307 fn test_check_executable_permissions_delete_files() {
308 let check = check_executable_permissions_check(".exe");
309 let conf = make_check_conf(&check);
310
311 let result = test_check_base(
312 "test_check_executable_permissions_delete_files",
313 DELETE_TOPIC,
314 BAD_TOPIC,
315 &conf,
316 );
317 test_result_ok(result);
318 }
319
320 #[test]
321 fn test_check_executable_permissions_binary_delete_files() {
322 let check = check_executable_permissions_check(".exe");
323 let conf = make_check_conf(&check);
324
325 let result = test_check_base(
326 "test_check_executable_permissions_binary_delete_files",
327 DELETE_BINARY_TOPIC,
328 BINARY_TOPIC,
329 &conf,
330 );
331 test_result_ok(result);
332 }
333
334 #[test]
335 fn test_check_executable_permissions_topic() {
336 let check = check_executable_permissions_check(".exe");
337 let result = run_topic_check("test_check_executable_permissions_topic", BAD_TOPIC, check);
338 test_result_errors(
339 result,
340 &[
341 "adds `is-exec` with executable permissions, but the file does not look \
342 executable.",
343 "adds `not-exec-shebang` without executable permissions, but the file looks \
344 executable.",
345 "adds `not-exec.exe` without executable permissions, but the file looks \
346 executable.",
347 ],
348 );
349 }
350
351 #[test]
352 fn test_check_executable_permissions_topic_delete_files() {
353 let check = check_executable_permissions_check(".exe");
354 run_topic_check_ok(
355 "test_check_executable_permissions_topic_delete_files",
356 DELETE_TOPIC,
357 check,
358 );
359 }
360
361 #[test]
362 fn test_check_executable_permissions_topic_binary_delete_files() {
363 let check = check_executable_permissions_check(".exe");
364 run_topic_check_ok(
365 "test_check_executable_permissions_topic_binary_delete_files",
366 DELETE_BINARY_TOPIC,
367 check,
368 );
369 }
370
371 #[test]
372 fn test_check_executable_permissions_topic_fixed() {
373 let check = check_executable_permissions_check(".exe");
374 run_topic_check_ok(
375 "test_check_executable_permissions_topic_fixed",
376 FIX_TOPIC,
377 check,
378 );
379 }
380
381 #[test]
382 fn test_check_executable_permissions_topic_binary_fixed() {
383 let check = check_executable_permissions_check(".exe");
384 run_topic_check_ok(
385 "test_check_executable_permissions_topic_binary_fixed",
386 BINARY_FIX_TOPIC,
387 check,
388 );
389 }
390
391 #[test]
392 fn test_check_executable_permissions_lfs() {
393 let check = check_executable_permissions_check(".lfs");
394 let conf = make_check_conf(&check);
395 let result = test_check_base(
396 "test_check_executable_permissions_lfs",
397 LFS_TOPIC,
398 BAD_TOPIC,
399 &conf,
400 );
401 test_result_ok(result);
402 }
403
404 #[test]
405 fn test_check_executable_permissions_ignore_symlinks() {
406 let check = CheckExecutablePermissions::default();
407 run_check_ok(
408 "test_check_executable_permissions_ignore_symlinks",
409 SYMLINK_COMMIT,
410 check,
411 );
412 }
413
414 #[test]
415 fn test_check_executable_permissions_ignore_symlinks_topic() {
416 let check = CheckExecutablePermissions::default();
417 run_topic_check_ok(
418 "test_check_executable_permissions_ignore_symlinks_topic",
419 SYMLINK_COMMIT,
420 check,
421 );
422 }
423}