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