fix(cadcraft-cli): reject unknown options (#120)

This commit is contained in:
cyfung1031 2026-10-11 01:26:40 +09:00 committed by GitHub
parent 348d3b6724
commit 8b13fa3b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 55 additions and 6 deletions

View File

@ -138,6 +138,33 @@ fn open(s: &mut Session, path: &str) -> Result<(), String> {
s.execute("open", &json!({ "path": path })).map(|_| ()).map_err(|e| e.to_string())
}
#[derive(Clone, Copy)]
struct OptionSpec {
name: &'static str,
takes_value: bool,
}
fn validate_options(command: &str, args: &[String], specs: &[OptionSpec]) -> Result<(), String> {
let mut i = 0;
while let Some(arg) = args.get(i) {
if let Some(name) = arg.strip_prefix("--") {
let Some(spec) = specs.iter().find(|spec| spec.name == arg) else {
return Err(format!("{command}: unknown option --{name}"));
};
if spec.takes_value && args.get(i + 1).is_some_and(|value| !value.starts_with("--")) {
i += 2;
} else {
i += 1;
}
} else {
i += 1;
}
}
Ok(())
}
const NO_OPTIONS: &[OptionSpec] = &[];
fn info(path: &str) -> Result<(), String> {
let mut s = Session::empty();
open(&mut s, path)?;
@ -489,12 +516,12 @@ fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let rest: Vec<String> = args.iter().skip(1).cloned().collect();
let r = match args.first().map(String::as_str) {
Some("info") => rest.first().ok_or_else(|| USAGE.to_string()).and_then(|p| info(p)),
Some("info") => validate_options("info", &rest, NO_OPTIONS).and_then(|_| rest.first().ok_or_else(|| USAGE.to_string())).and_then(|p| info(p)),
Some("convert") => convert(&rest),
Some("run") => run(&rest),
Some("perf") => perf(&rest),
Some("sample") => sample(&rest),
Some("commands") => {
Some("perf") => validate_options("perf", &rest, NO_OPTIONS).and_then(|_| perf(&rest)),
Some("sample") => validate_options("sample", &rest, NO_OPTIONS).and_then(|_| sample(&rest)),
Some("commands") => validate_options("commands", &rest, NO_OPTIONS).map(|_| {
let s = Session::new();
let f = rest.first().map(|x| x.to_ascii_lowercase());
let v: Vec<Value> = cadcraft_engine::command_specs()
@ -503,8 +530,7 @@ fn main() -> ExitCode {
.map(|c| serde_json::to_value(c.info(&s)).unwrap_or_default())
.collect();
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
Ok(())
}
}),
Some("mcp") => mcp(&rest),
Some("--version" | "-V") => {
println!("cadcraft-cli {}", env!("CARGO_PKG_VERSION"));
@ -678,4 +704,27 @@ mod tests {
assert!(!dir.join("bad.png").exists());
let _ = std::fs::remove_dir_all(&dir);
}
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).into()).collect()
}
#[test]
fn validators_accept_only_the_documented_options() {
assert!(validate_options("info", &args(&["file.dxf"]), NO_OPTIONS).is_ok());
assert!(validate_options("perf", &args(&["100"]), NO_OPTIONS).is_ok());
assert!(validate_options("sample", &args(&["bracket", "out.dxf"]), NO_OPTIONS).is_ok());
assert!(validate_options("commands", &args(&["line"]), NO_OPTIONS).is_ok());
for (command, specs) in [("info", NO_OPTIONS), ("perf", NO_OPTIONS), ("sample", NO_OPTIONS), ("commands", NO_OPTIONS)] {
assert!(validate_options(command, &args(&["--typo"]), specs).is_err());
}
}
#[test]
fn unknown_options_are_rejected_before_side_effects() {
assert_eq!(validate_options("info", &args(&["missing.dxf", "--typo"]), NO_OPTIONS), Err("info: unknown option --typo".into()));
assert_eq!(validate_options("perf", &args(&["1", "--typo"]), NO_OPTIONS), Err("perf: unknown option --typo".into()));
assert_eq!(validate_options("sample", &args(&["bracket", "out.dxf", "--typo"]), NO_OPTIONS), Err("sample: unknown option --typo".into()));
}
}