1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub struct ProcessExecCapability {
6 /// The command to execute.
7 pub command: String,
8 /// The arguments to pass to the command. Use `*` for a single wildcard argument.
9 /// If the last element is `**`, then any trailing arguments are allowed.
10 pub args: Vec<String>,
11}
12
13impl ProcessExecCapability {
14 /// Returns whether the capability allows the given command and arguments.
15 pub fn allows(
16 &self,
17 desired_command: &str,
18 desired_args: &[impl AsRef<str> + std::fmt::Debug],
19 ) -> bool {
20 if self.command != desired_command && self.command != "*" {
21 return false;
22 }
23
24 for (ix, arg) in self.args.iter().enumerate() {
25 if arg == "**" {
26 return true;
27 }
28
29 if ix >= desired_args.len() {
30 return false;
31 }
32
33 if arg != "*" && arg != desired_args[ix].as_ref() {
34 return false;
35 }
36 }
37
38 if self.args.len() < desired_args.len() {
39 return false;
40 }
41
42 true
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use pretty_assertions::assert_eq;
49
50 use super::*;
51
52 #[test]
53 fn test_allows_with_exact_match() {
54 let capability = ProcessExecCapability {
55 command: "ls".to_string(),
56 args: vec!["-la".to_string()],
57 };
58
59 assert_eq!(capability.allows("ls", &["-la"]), true);
60 assert_eq!(capability.allows("ls", &["-l"]), false);
61 assert_eq!(capability.allows("pwd", &[] as &[&str]), false);
62 }
63
64 #[test]
65 fn test_allows_with_wildcard_arg() {
66 let capability = ProcessExecCapability {
67 command: "git".to_string(),
68 args: vec!["*".to_string()],
69 };
70
71 assert_eq!(capability.allows("git", &["status"]), true);
72 assert_eq!(capability.allows("git", &["commit"]), true);
73 // Too many args.
74 assert_eq!(capability.allows("git", &["status", "-s"]), false);
75 // Wrong command.
76 assert_eq!(capability.allows("npm", &["install"]), false);
77 }
78
79 #[test]
80 fn test_allows_with_double_wildcard() {
81 let capability = ProcessExecCapability {
82 command: "cargo".to_string(),
83 args: vec!["test".to_string(), "**".to_string()],
84 };
85
86 assert_eq!(capability.allows("cargo", &["test"]), true);
87 assert_eq!(capability.allows("cargo", &["test", "--all"]), true);
88 assert_eq!(
89 capability.allows("cargo", &["test", "--all", "--no-fail-fast"]),
90 true
91 );
92 // Wrong first arg.
93 assert_eq!(capability.allows("cargo", &["build"]), false);
94 }
95
96 #[test]
97 fn test_allows_with_mixed_wildcards() {
98 let capability = ProcessExecCapability {
99 command: "docker".to_string(),
100 args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
101 };
102
103 assert_eq!(capability.allows("docker", &["run", "nginx"]), true);
104 assert_eq!(capability.allows("docker", &["run"]), false);
105 assert_eq!(
106 capability.allows("docker", &["run", "ubuntu", "bash"]),
107 true
108 );
109 assert_eq!(
110 capability.allows("docker", &["run", "alpine", "sh", "-c", "echo hello"]),
111 true
112 );
113 // Wrong first arg.
114 assert_eq!(capability.allows("docker", &["ps"]), false);
115 }
116}