1use std::collections::HashSet;
2use std::sync::LazyLock;
3
4static SIMPLE_SAFE: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
7 HashSet::from([
8 "cat",
10 "head",
11 "tail",
12 "less",
13 "more",
14 "bat",
15 "hexdump",
16 "strings",
17 "xxd",
18 "od",
19 "zcat",
21 "bzcat",
22 "xzcat",
23 "zstdcat",
24 "nm",
26 "objdump",
27 "readelf",
28 "ldd",
29 "otool",
30 "size",
31 "file",
32 "ls",
34 "tree",
35 "exa",
36 "eza",
37 "lsd",
38 "stat",
40 "wc",
41 "du",
42 "df",
43 "grep",
45 "rg",
46 "ag",
47 "diff",
48 "cut",
49 "tr",
50 "uniq",
52 "paste",
53 "join",
54 "comm",
55 "fold",
56 "fmt",
57 "nl",
58 "column",
59 "expand",
60 "unexpand",
61 "rev",
62 "tac",
63 "shuf",
64 "base64",
66 "base32",
67 "md5sum",
68 "sha1sum",
69 "sha256sum",
70 "sha512sum",
71 "cksum",
72 "sum",
73 "locate",
75 "which",
76 "whereis",
77 "type",
78 "whence",
79 "whoami",
81 "hostname",
82 "uname",
83 "id",
84 "groups",
85 "uptime",
86 "pwd",
87 "date",
88 "printenv",
90 "locale",
91 "ps",
93 "top",
94 "htop",
95 "lsof",
96 "vmstat",
97 "iostat",
98 "free",
99 "pgrep",
100 "ping",
102 "dig",
103 "nslookup",
104 "traceroute",
105 "tracepath",
106 "netstat",
107 "ss",
108 "host",
110 "getent",
111 "man",
113 "info",
114 "whatis",
115 "apropos",
116 "tldr",
117 "help",
118 "echo",
120 "printf",
121 "true",
122 "false",
123 "test",
124 "[",
125 ":",
126 "basename",
128 "dirname",
129 "realpath",
130 "readlink",
131 "bc",
133 "expr",
134 "seq",
135 "tty",
137 "stty",
138 "tput",
139 "yes",
140 "sleep",
141 "nproc",
143 "getconf",
144 "arch",
145 "lsb_release",
146 "jq",
148 "fzf",
150 "tokei",
151 "cloc",
152 "scc",
153 "hyperfine",
154 "iconv",
156 "dos2unix",
157 "unix2dos",
158 "mount",
160 "findmnt",
161 "lsblk",
162 "blkid",
163 ])
165});
166
167static WRAPPER_COMMANDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
169 HashSet::from([
170 "time", "timeout", "nice", "strace", "ltrace", "nohup", "command", "builtin",
171 ])
172});
173
174#[must_use]
176pub fn is_simple_safe(cmd: &str) -> bool {
177 SIMPLE_SAFE.contains(cmd)
178}
179
180#[must_use]
182pub fn is_wrapper(cmd: &str) -> bool {
183 WRAPPER_COMMANDS.contains(cmd)
184}
185
186#[must_use]
188pub fn simple_safe_count() -> usize {
189 SIMPLE_SAFE.len()
190}
191
192#[must_use]
194pub fn wrapper_count() -> usize {
195 WRAPPER_COMMANDS.len()
196}
197
198#[must_use]
200pub fn all_simple_safe() -> Vec<&'static str> {
201 let mut cmds: Vec<_> = SIMPLE_SAFE.iter().copied().collect();
202 cmds.sort_unstable();
203 cmds
204}
205
206#[must_use]
208pub fn all_wrappers() -> Vec<&'static str> {
209 let mut cmds: Vec<_> = WRAPPER_COMMANDS.iter().copied().collect();
210 cmds.sort_unstable();
211 cmds
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn known_safe_commands() {
220 assert!(is_simple_safe("cat"));
221 assert!(is_simple_safe("ls"));
222 assert!(is_simple_safe("grep"));
223 assert!(is_simple_safe("whoami"));
224 assert!(is_simple_safe("jq"));
225 }
226
227 #[test]
228 fn unknown_commands_not_safe() {
229 assert!(!is_simple_safe("rm"));
230 assert!(!is_simple_safe("sudo"));
231 assert!(!is_simple_safe("arbitrary_command"));
232 }
233
234 #[test]
235 fn wrapper_commands() {
236 assert!(is_wrapper("time"));
237 assert!(is_wrapper("timeout"));
238 assert!(is_wrapper("nice"));
239 assert!(is_wrapper("nohup"));
240 assert!(!is_wrapper("cat"));
241 }
242}