Skip to main content

safe_chains/handlers/
mod.rs

1pub mod ai;
2pub mod android;
3pub mod containers;
4pub mod coreutils;
5pub mod dotnet;
6pub mod forges;
7pub mod go;
8pub mod jvm;
9pub mod magick;
10pub mod network;
11pub mod node;
12pub mod perl;
13pub mod php;
14pub mod python;
15pub mod r;
16pub mod ruby;
17pub mod rust;
18pub mod shell;
19pub mod swift;
20pub mod system;
21pub mod vcs;
22pub mod wrappers;
23pub mod xcode;
24
25use crate::parse::Token;
26use crate::verdict::Verdict;
27
28pub fn dispatch(tokens: &[Token]) -> Verdict {
29    let cmd = tokens[0].command_name();
30    None
31        .or_else(|| shell::dispatch(cmd, tokens))
32        .or_else(|| wrappers::dispatch(cmd, tokens))
33        .or_else(|| vcs::dispatch(cmd, tokens))
34        .or_else(|| forges::dispatch(cmd, tokens))
35        .or_else(|| node::dispatch(cmd, tokens))
36        .or_else(|| ruby::dispatch(cmd, tokens))
37        .or_else(|| python::dispatch(cmd, tokens))
38        .or_else(|| rust::dispatch(cmd, tokens))
39        .or_else(|| go::dispatch(cmd, tokens))
40        .or_else(|| jvm::dispatch(cmd, tokens))
41        .or_else(|| android::dispatch(cmd, tokens))
42        .or_else(|| php::dispatch(cmd, tokens))
43        .or_else(|| swift::dispatch(cmd, tokens))
44        .or_else(|| dotnet::dispatch(cmd, tokens))
45        .or_else(|| containers::dispatch(cmd, tokens))
46        .or_else(|| network::dispatch(cmd, tokens))
47        .or_else(|| ai::dispatch(cmd, tokens))
48        .or_else(|| system::dispatch(cmd, tokens))
49        .or_else(|| xcode::dispatch(cmd, tokens))
50        .or_else(|| perl::dispatch(cmd, tokens))
51        .or_else(|| r::dispatch(cmd, tokens))
52        .or_else(|| coreutils::dispatch(cmd, tokens))
53        .or_else(|| magick::dispatch(cmd, tokens))
54        .unwrap_or(Verdict::Denied)
55}
56
57#[cfg(test)]
58const HANDLED_CMDS: &[&str] = &[
59    "sh", "bash", "xargs", "timeout", "time", "env", "nice", "ionice", "hyperfine", "dotenv",
60    "git", "jj", "gh", "glab", "jjpr", "tea",
61    "npm", "yarn", "pnpm", "bun", "deno", "npx", "bunx", "nvm", "fnm", "volta",
62    "ruby", "ri", "bundle", "gem", "importmap", "rbenv",
63    "pip", "uv", "poetry", "pyenv", "conda",
64    "cargo", "rustup",
65    "go",
66    "gradle", "mvn", "mvnw", "ktlint", "detekt",
67    "javap", "jar", "keytool", "jarsigner",
68    "adb", "apkanalyzer", "apksigner", "bundletool", "aapt2",
69    "emulator", "avdmanager", "sdkmanager", "zipalign", "lint",
70    "fastlane", "firebase",
71    "composer", "craft",
72    "swift",
73    "dotnet",
74    "curl",
75    "docker", "podman", "kubectl", "orbctl", "qemu-img",
76    "ollama", "llm", "hf", "claude", "aider", "codex", "opencode", "vibe",
77    "ddev", "dcli",
78    "brew", "mise", "asdf", "crontab", "defaults", "pmset", "sysctl", "cmake", "psql", "pg_isready",
79    "terraform", "heroku", "vercel", "flyctl",
80    "overmind", "tailscale", "tmux", "wg",
81    "networksetup", "launchctl", "diskutil", "security", "csrutil", "log",
82    "xcodebuild", "plutil", "xcode-select", "xcrun", "pkgutil", "lipo", "codesign", "spctl",
83    "xcodegen", "tuist", "pod", "swiftlint", "swiftformat", "periphery", "xcbeautify", "agvtool", "simctl",
84    "perl",
85    "R", "Rscript",
86    "grep", "rg", "ag", "ack", "zgrep", "locate",
87    "cat", "head", "tail", "wc", "cut", "tr", "uniq", "less", "more", "zcat",
88    "diff", "comm", "paste", "tac", "rev", "nl",
89    "expand", "unexpand", "fold", "fmt", "col", "column", "iconv", "nroff",
90    "echo", "printf", "seq", "test", "expr", "bc", "factor", "bat",
91    "arch", "command", "hostname",
92    "find", "sed", "shuf", "sort", "yq", "xmllint", "awk", "gawk", "mawk", "nawk",
93    "magick",
94    "fd", "eza", "ls", "delta", "colordiff",
95    "dirname", "basename", "realpath", "readlink",
96    "file", "stat", "du", "df", "tree", "cmp", "zipinfo", "tar", "unzip", "gzip",
97    "true", "false",
98    "alias", "export", "printenv", "read", "type", "wait", "whereis", "which", "whoami", "date", "pwd", "cd", "unset",
99    "uname", "nproc", "uptime", "id", "groups", "tty", "locale", "cal", "sleep",
100    "who", "w", "last", "lastlog",
101    "ps", "top", "htop", "iotop", "procs", "dust", "lsof", "pgrep", "lsblk", "free",
102    "jq", "jaq", "gojq", "fx", "jless", "htmlq", "xq", "tomlq", "mlr", "dasel",
103    "base64", "xxd", "getconf", "uuidgen",
104    "md5sum", "md5", "sha256sum", "shasum", "sha1sum", "sha512sum",
105    "cksum", "b2sum", "sum", "strings", "hexdump", "od", "size", "sips",
106    "sw_vers", "mdls", "otool", "nm", "system_profiler", "ioreg", "vm_stat", "mdfind", "man",
107    "dig", "nslookup", "host", "whois", "netstat", "ss", "ifconfig", "route", "ping",
108    "xv",
109    "identify", "shellcheck", "cloc", "tokei", "cucumber", "branchdiff", "workon", "safe-chains",
110];
111
112pub fn handler_docs() -> Vec<crate::docs::CommandDoc> {
113    let mut docs = Vec::new();
114    docs.extend(vcs::command_docs());
115    docs.extend(forges::command_docs());
116    docs.extend(node::command_docs());
117    docs.extend(ruby::command_docs());
118    docs.extend(python::command_docs());
119    docs.extend(rust::command_docs());
120    docs.extend(go::command_docs());
121    docs.extend(jvm::command_docs());
122    docs.extend(android::command_docs());
123    docs.extend(php::command_docs());
124    docs.extend(swift::command_docs());
125    docs.extend(dotnet::command_docs());
126    docs.extend(containers::command_docs());
127    docs.extend(ai::command_docs());
128    docs.extend(network::command_docs());
129    docs.extend(system::command_docs());
130    docs.extend(xcode::command_docs());
131    docs.extend(perl::command_docs());
132    docs.extend(r::command_docs());
133    docs.extend(coreutils::command_docs());
134    docs.extend(shell::command_docs());
135    docs.extend(wrappers::command_docs());
136    docs.extend(magick::command_docs());
137    docs
138}
139
140#[cfg(test)]
141#[derive(Debug)]
142pub(crate) enum CommandEntry {
143    Positional { cmd: &'static str },
144    Custom { cmd: &'static str, valid_prefix: Option<&'static str> },
145    Subcommand { cmd: &'static str, subs: &'static [SubEntry], bare_ok: bool },
146    Delegation { cmd: &'static str },
147}
148
149#[cfg(test)]
150#[derive(Debug)]
151pub(crate) enum SubEntry {
152    Policy { name: &'static str },
153    Nested { name: &'static str, subs: &'static [SubEntry] },
154    Custom { name: &'static str, valid_suffix: Option<&'static str> },
155    Positional,
156    Guarded { name: &'static str, valid_suffix: &'static str },
157}
158
159use crate::command::CommandDef;
160
161const COMMAND_DEFS: &[&CommandDef] = &[
162    &ai::CODEX, &ai::OLLAMA, &ai::OPENCODE, &ai::LLM, &ai::HF,
163    &containers::DOCKER, &containers::PODMAN, &containers::KUBECTL, &containers::ORBCTL, &containers::QEMU_IMG,
164    &dotnet::DOTNET,
165    &go::GO,
166    &android::APKANALYZER, &android::APKSIGNER, &android::BUNDLETOOL, &android::AAPT2,
167    &android::AVDMANAGER,
168    &jvm::GRADLE, &jvm::KEYTOOL,
169    &magick::MAGICK,
170    &node::NPM, &node::PNPM, &node::BUN, &node::DENO,
171    &node::NVM, &node::FNM, &node::VOLTA,
172    &php::COMPOSER, &php::CRAFT,
173    &python::PIP, &python::UV, &python::POETRY,
174    &python::PYENV, &python::CONDA,
175    &ruby::BUNDLE, &ruby::GEM, &ruby::IMPORTMAP, &ruby::RBENV,
176    &rust::CARGO, &rust::RUSTUP,
177    &vcs::GIT,
178    &swift::SWIFT,
179    &system::BREW, &system::MISE, &system::ASDF, &system::DDEV, &system::DCLI, &system::CMAKE,
180    &system::DEFAULTS, &system::TERRAFORM, &system::HEROKU, &system::VERCEL,
181    &system::FLYCTL, &system::FASTLANE, &system::FIREBASE,
182    &system::OVERMIND, &system::TAILSCALE, &system::WG,
183    &system::SECURITY, &system::CSRUTIL, &system::DISKUTIL,
184    &system::LAUNCHCTL, &system::LOG,
185    &xcode::XCODEBUILD, &xcode::PLUTIL, &xcode::XCODE_SELECT,
186    &xcode::XCODEGEN, &xcode::TUIST, &xcode::POD, &xcode::SWIFTLINT,
187    &xcode::PERIPHERY, &xcode::AGVTOOL, &xcode::SIMCTL,
188];
189
190pub fn all_opencode_patterns() -> Vec<String> {
191    let mut patterns = Vec::new();
192    for def in COMMAND_DEFS {
193        patterns.extend(def.opencode_patterns());
194    }
195    for def in coreutils::all_flat_defs() {
196        patterns.extend(def.opencode_patterns());
197    }
198    for def in jvm::jvm_flat_defs() {
199        patterns.extend(def.opencode_patterns());
200    }
201    for def in android::android_flat_defs() {
202        patterns.extend(def.opencode_patterns());
203    }
204    for def in ai::ai_flat_defs() {
205        patterns.extend(def.opencode_patterns());
206    }
207    for def in ruby::ruby_flat_defs() {
208        patterns.extend(def.opencode_patterns());
209    }
210    for def in system::system_flat_defs() {
211        patterns.extend(def.opencode_patterns());
212    }
213    for def in xcode::xcbeautify_flat_defs() {
214        patterns.extend(def.opencode_patterns());
215    }
216    patterns.sort();
217    patterns.dedup();
218    patterns
219}
220
221#[cfg(test)]
222fn full_registry() -> Vec<&'static CommandEntry> {
223    let mut entries = Vec::new();
224    entries.extend(shell::REGISTRY);
225    entries.extend(wrappers::REGISTRY);
226    entries.extend(vcs::full_registry());
227    entries.extend(forges::full_registry());
228    entries.extend(node::full_registry());
229    entries.extend(jvm::full_registry());
230    entries.extend(android::full_registry());
231    entries.extend(network::REGISTRY);
232    entries.extend(system::full_registry());
233    entries.extend(xcode::full_registry());
234    entries.extend(perl::REGISTRY);
235    entries.extend(r::REGISTRY);
236    entries.extend(coreutils::full_registry());
237    entries
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use std::collections::HashSet;
244
245    const UNKNOWN_FLAG: &str = "--xyzzy-unknown-42";
246    const UNKNOWN_SUB: &str = "xyzzy-unknown-42";
247
248    fn check_entry(entry: &CommandEntry, failures: &mut Vec<String>) {
249        match entry {
250            CommandEntry::Positional { .. } | CommandEntry::Delegation { .. } => {}
251            CommandEntry::Custom { cmd, valid_prefix } => {
252                let base = valid_prefix.unwrap_or(cmd);
253                let test = format!("{base} {UNKNOWN_FLAG}");
254                if crate::is_safe_command(&test) {
255                    failures.push(format!("{cmd}: accepted unknown flag: {test}"));
256                }
257            }
258            CommandEntry::Subcommand { cmd, subs, bare_ok } => {
259                if !bare_ok && crate::is_safe_command(cmd) {
260                    failures.push(format!("{cmd}: accepted bare invocation"));
261                }
262                let test = format!("{cmd} {UNKNOWN_SUB}");
263                if crate::is_safe_command(&test) {
264                    failures.push(format!("{cmd}: accepted unknown subcommand: {test}"));
265                }
266                for sub in *subs {
267                    check_sub(cmd, sub, failures);
268                }
269            }
270        }
271    }
272
273    fn check_sub(prefix: &str, entry: &SubEntry, failures: &mut Vec<String>) {
274        match entry {
275            SubEntry::Policy { name } => {
276                let test = format!("{prefix} {name} {UNKNOWN_FLAG}");
277                if crate::is_safe_command(&test) {
278                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
279                }
280            }
281            SubEntry::Nested { name, subs } => {
282                let path = format!("{prefix} {name}");
283                let test = format!("{path} {UNKNOWN_SUB}");
284                if crate::is_safe_command(&test) {
285                    failures.push(format!("{path}: accepted unknown subcommand: {test}"));
286                }
287                for sub in *subs {
288                    check_sub(&path, sub, failures);
289                }
290            }
291            SubEntry::Custom { name, valid_suffix } => {
292                let base = match valid_suffix {
293                    Some(s) => format!("{prefix} {name} {s}"),
294                    None => format!("{prefix} {name}"),
295                };
296                let test = format!("{base} {UNKNOWN_FLAG}");
297                if crate::is_safe_command(&test) {
298                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
299                }
300            }
301            SubEntry::Positional => {}
302            SubEntry::Guarded { name, valid_suffix } => {
303                let test = format!("{prefix} {name} {valid_suffix} {UNKNOWN_FLAG}");
304                if crate::is_safe_command(&test) {
305                    failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
306                }
307            }
308        }
309    }
310
311    #[test]
312    fn all_commands_reject_unknown() {
313        let registry = full_registry();
314        let mut failures = Vec::new();
315        for entry in &registry {
316            check_entry(entry, &mut failures);
317        }
318        assert!(
319            failures.is_empty(),
320            "unknown flags/subcommands accepted:\n{}",
321            failures.join("\n")
322        );
323    }
324
325    #[test]
326    fn command_defs_reject_unknown() {
327        for def in COMMAND_DEFS {
328            def.auto_test_reject_unknown();
329        }
330    }
331
332    #[test]
333    fn flat_defs_reject_unknown() {
334        for def in coreutils::all_flat_defs() {
335            def.auto_test_reject_unknown();
336        }
337        for def in xcode::xcbeautify_flat_defs() {
338            def.auto_test_reject_unknown();
339        }
340        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
341            def.auto_test_reject_unknown();
342        }
343    }
344
345
346    #[test]
347    fn bare_false_rejects_bare_invocation() {
348        let check_def = |def: &crate::command::FlatDef| {
349            if !def.policy.bare {
350                assert!(
351                    !crate::is_safe_command(def.name),
352                    "{}: bare=false but bare invocation accepted",
353                    def.name,
354                );
355            }
356        };
357        for def in coreutils::all_flat_defs()
358            .into_iter()
359            .chain(xcode::xcbeautify_flat_defs())
360        {
361            check_def(def);
362        }
363        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
364            check_def(def);
365        }
366    }
367
368    fn visit_subs(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::command::SubDef)) {
369        for sub in subs {
370            visitor(prefix, sub);
371            if let crate::command::SubDef::Nested { name, subs: inner } = sub {
372                visit_subs(&format!("{prefix} {name}"), inner, visitor);
373            }
374        }
375    }
376
377    #[test]
378    fn guarded_subs_require_guard() {
379        let mut failures = Vec::new();
380        for def in COMMAND_DEFS {
381            visit_subs(def.name, def.subs, &mut |prefix, sub| {
382                if let crate::command::SubDef::Guarded { name, guard_long, .. } = sub {
383                    let without = format!("{prefix} {name}");
384                    if crate::is_safe_command(&without) {
385                        failures.push(format!("{without}: accepted without guard {guard_long}"));
386                    }
387                    let with = format!("{prefix} {name} {guard_long}");
388                    if !crate::is_safe_command(&with) {
389                        failures.push(format!("{with}: rejected with guard {guard_long}"));
390                    }
391                }
392            });
393        }
394        assert!(failures.is_empty(), "guarded sub issues:\n{}", failures.join("\n"));
395    }
396
397    #[test]
398    fn guarded_subs_accept_guard_short() {
399        let mut failures = Vec::new();
400        for def in COMMAND_DEFS {
401            visit_subs(def.name, def.subs, &mut |prefix, sub| {
402                if let crate::command::SubDef::Guarded { name, guard_short: Some(short), .. } = sub {
403                    let with_short = format!("{prefix} {name} {short}");
404                    if !crate::is_safe_command(&with_short) {
405                        failures.push(format!("{with_short}: rejected with guard_short"));
406                    }
407                }
408            });
409        }
410        assert!(failures.is_empty(), "guard_short issues:\n{}", failures.join("\n"));
411    }
412
413    #[test]
414    fn nested_subs_reject_bare() {
415        let mut failures = Vec::new();
416        for def in COMMAND_DEFS {
417            visit_subs(def.name, def.subs, &mut |prefix, sub| {
418                if let crate::command::SubDef::Nested { name, .. } = sub {
419                    let bare = format!("{prefix} {name}");
420                    if crate::is_safe_command(&bare) {
421                        failures.push(format!("{bare}: nested sub accepted bare invocation"));
422                    }
423                }
424            });
425        }
426        assert!(failures.is_empty(), "nested bare issues:\n{}", failures.join("\n"));
427    }
428
429    #[test]
430    fn process_substitution_blocked() {
431        let cmds = ["echo <(cat /etc/passwd)", "echo >(rm -rf /)", "grep pattern <(ls)"];
432        for cmd in &cmds {
433            assert!(
434                !crate::is_safe_command(cmd),
435                "process substitution not blocked: {cmd}",
436            );
437        }
438    }
439
440    #[test]
441    fn positional_style_accepts_unknown_args() {
442        use crate::policy::FlagStyle;
443        for def in coreutils::all_flat_defs() {
444            if def.policy.flag_style == FlagStyle::Positional {
445                let test = format!("{} --unknown-xyz", def.name);
446                assert!(
447                    crate::is_safe_command(&test),
448                    "{}: FlagStyle::Positional but rejected unknown arg",
449                    def.name,
450                );
451            }
452        }
453    }
454
455    fn visit_policies(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::policy::FlagPolicy)) {
456        for sub in subs {
457            match sub {
458                crate::command::SubDef::Policy { name, policy, .. } => {
459                    visitor(&format!("{prefix} {name}"), policy);
460                }
461                crate::command::SubDef::Guarded { name, guard_long, policy, .. } => {
462                    visitor(&format!("{prefix} {name} {guard_long}"), policy);
463                }
464                crate::command::SubDef::Nested { name, subs: inner } => {
465                    visit_policies(&format!("{prefix} {name}"), inner, visitor);
466                }
467                _ => {}
468            }
469        }
470    }
471
472    #[test]
473    fn valued_flags_accept_eq_syntax() {
474        let mut failures = Vec::new();
475
476        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
477            for flag in def.policy.valued.iter() {
478                let cmd = format!("{} {flag}=test_value", def.name);
479                if !crate::is_safe_command(&cmd) {
480                    failures.push(format!("{cmd}: valued flag rejected with = syntax"));
481                }
482            }
483        };
484        for def in coreutils::all_flat_defs()
485            .into_iter()
486            .chain(xcode::xcbeautify_flat_defs())
487        {
488            check_flat(def, &mut failures);
489        }
490        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
491            check_flat(def, &mut failures);
492        }
493
494        for def in COMMAND_DEFS {
495            visit_policies(def.name, def.subs, &mut |prefix, policy| {
496                for flag in policy.valued.iter() {
497                    let cmd = format!("{prefix} {flag}=test_value");
498                    if !crate::is_safe_command(&cmd) {
499                        failures.push(format!("{cmd}: valued flag rejected with = syntax"));
500                    }
501                }
502            });
503        }
504
505        assert!(failures.is_empty(), "valued = syntax issues:\n{}", failures.join("\n"));
506    }
507
508    #[test]
509    fn max_positional_enforced() {
510        let mut failures = Vec::new();
511
512        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
513            if let Some(max) = def.policy.max_positional {
514                let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
515                let cmd = format!("{} {}", def.name, args.join(" "));
516                if crate::is_safe_command(&cmd) {
517                    failures.push(format!(
518                        "{}: max_positional={max} but accepted {} positional args",
519                        def.name,
520                        max + 1,
521                    ));
522                }
523            }
524        };
525        for def in coreutils::all_flat_defs()
526            .into_iter()
527            .chain(xcode::xcbeautify_flat_defs())
528        {
529            check_flat(def, &mut failures);
530        }
531        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
532            check_flat(def, &mut failures);
533        }
534
535        for def in COMMAND_DEFS {
536            visit_policies(def.name, def.subs, &mut |prefix, policy| {
537                if let Some(max) = policy.max_positional {
538                    let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
539                    let cmd = format!("{prefix} {}", args.join(" "));
540                    if crate::is_safe_command(&cmd) {
541                        failures.push(format!(
542                            "{prefix}: max_positional={max} but accepted {} positional args",
543                            max + 1,
544                        ));
545                    }
546                }
547            });
548        }
549
550        assert!(failures.is_empty(), "max_positional issues:\n{}", failures.join("\n"));
551    }
552
553    #[test]
554    fn doc_generation_non_empty() {
555        let mut failures = Vec::new();
556
557        for def in COMMAND_DEFS {
558            let doc = def.to_doc();
559            if doc.description.trim().is_empty() {
560                failures.push(format!("{}: CommandDef produced empty doc", def.name));
561            }
562            if doc.url.is_empty() {
563                failures.push(format!("{}: CommandDef has empty URL", def.name));
564            }
565        }
566
567        let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
568            let doc = def.to_doc();
569            if doc.description.trim().is_empty() && !def.policy.bare {
570                failures.push(format!("{}: FlatDef produced empty doc", def.name));
571            }
572            if doc.url.is_empty() {
573                failures.push(format!("{}: FlatDef has empty URL", def.name));
574            }
575        };
576        for def in coreutils::all_flat_defs()
577            .into_iter()
578            .chain(xcode::xcbeautify_flat_defs())
579        {
580            check_flat(def, &mut failures);
581        }
582        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
583            check_flat(def, &mut failures);
584        }
585
586        assert!(failures.is_empty(), "doc generation issues:\n{}", failures.join("\n"));
587    }
588
589    #[test]
590    fn registry_covers_handled_commands() {
591        let registry = full_registry();
592        let mut all_cmds: HashSet<&str> = registry
593            .iter()
594            .map(|e| match e {
595                CommandEntry::Positional { cmd }
596                | CommandEntry::Custom { cmd, .. }
597                | CommandEntry::Subcommand { cmd, .. }
598                | CommandEntry::Delegation { cmd } => *cmd,
599            })
600            .collect();
601        for def in COMMAND_DEFS {
602            all_cmds.insert(def.name);
603        }
604        for def in coreutils::all_flat_defs() {
605            all_cmds.insert(def.name);
606        }
607        for def in xcode::xcbeautify_flat_defs() {
608            all_cmds.insert(def.name);
609        }
610        for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
611            all_cmds.insert(def.name);
612        }
613        let handled: HashSet<&str> = HANDLED_CMDS.iter().copied().collect();
614
615        let missing: Vec<_> = handled.difference(&all_cmds).collect();
616        assert!(missing.is_empty(), "not in registry or COMMAND_DEFS: {missing:?}");
617
618        let extra: Vec<_> = all_cmds.difference(&handled).collect();
619        assert!(extra.is_empty(), "not in HANDLED_CMDS: {extra:?}");
620    }
621
622}