diff --git a/socktop/src/main.rs b/socktop/src/main.rs index 746c0c9..2c7991a 100644 --- a/socktop/src/main.rs +++ b/socktop/src/main.rs @@ -124,6 +124,15 @@ pub(crate) fn parse_args>(args: I) -> Result { + // An unrecognized option must never fall through to the + // positional URL slot: an older binary handed a newer flag + // would otherwise "connect" to the flag text — and offer to + // save it over a named profile's URL. + if arg.starts_with('-') { + return Err(format!( + "Unknown option '{arg}'. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--no-kill] [ws://HOST:PORT/ws]" + )); + } if url.is_none() { url = Some(arg); } else { @@ -155,6 +164,11 @@ async fn main() -> Result<(), Box> { Ok(v) => v, Err(msg) => { eprintln!("{msg}"); + // --help produces the bare usage text and exits cleanly; real + // parse errors must be visible to scripts and CI as a failure. + if !msg.starts_with("Usage:") { + std::process::exit(2); + } return Ok(()); } }; diff --git a/socktop/tests/cli_args.rs b/socktop/tests/cli_args.rs index 6118a69..defcdce 100644 --- a/socktop/tests/cli_args.rs +++ b/socktop/tests/cli_args.rs @@ -156,3 +156,29 @@ fn test_no_kill_env_var_accepted() { String::from_utf8_lossy(&out.stderr) ); } + +#[test] +fn test_unknown_option_rejected_not_treated_as_url() { + // Regression guard for the socktop.io incident (Aug 2026): socktop 1.60.1 + // parsed the then-unknown --no-kill flag as the positional websocket URL, + // which made it prompt to overwrite the 'local' profile's URL with the + // literal string "--no-kill". Unknown options must fail loudly instead of + // falling through to the URL slot. + let exe = env!("CARGO_BIN_EXE_socktop"); + let out = Command::new(exe) + .args(["--not-a-real-flag", "--dry-run", "ws://127.0.0.1:3000/ws"]) + .output() + .expect("run socktop with unknown flag"); + assert_eq!( + out.status.code(), + Some(2), + "unknown option should exit 2, got: {:?}\nstderr: {}", + out.status.code(), + String::from_utf8_lossy(&out.stderr) + ); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("Unknown option '--not-a-real-flag'"), + "stderr should name the rejected option\n{err}" + ); +}