pub mod args;
pub mod commands;
pub mod ecosystem_dispatch;
pub mod json_envelope;
pub mod output;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "socket-patch",
about = "CLI tool for applying security patches to dependencies",
version,
propagate_version = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Apply(commands::apply::ApplyArgs),
Rollback(commands::rollback::RollbackArgs),
#[command(visible_alias = "download")]
Get(commands::get::GetArgs),
Scan(commands::scan::ScanArgs),
List(commands::list::ListArgs),
Remove(commands::remove::RemoveArgs),
Setup(commands::setup::SetupArgs),
#[command(visible_alias = "gc")]
Repair(commands::repair::RepairArgs),
Unlock(commands::unlock::UnlockArgs),
Vex(commands::vex::VexArgs),
}
pub fn looks_like_uuid(s: &str) -> bool {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 5 {
return false;
}
let expected = [8, 4, 4, 4, 12];
parts
.iter()
.zip(expected.iter())
.all(|(p, &len)| p.len() == len && p.chars().all(|c| c.is_ascii_hexdigit()))
}
pub fn parse_with_uuid_fallback(argv: Vec<String>) -> Result<Cli, clap::Error> {
match Cli::try_parse_from(&argv) {
Ok(cli) => Ok(cli),
Err(err) => {
if argv.len() >= 2 && looks_like_uuid(&argv[1]) {
let mut new_args = vec![argv[0].clone(), "get".into()];
new_args.extend_from_slice(&argv[1..]);
match Cli::try_parse_from(&new_args) {
Ok(cli) => Ok(cli),
Err(_) => Err(err),
}
} else {
Err(err)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn looks_like_uuid_accepts_canonical_lowercase() {
assert!(looks_like_uuid("80630680-4da6-45f9-bba8-b888e0ffd58c"));
}
#[test]
fn looks_like_uuid_accepts_uppercase() {
assert!(looks_like_uuid("80630680-4DA6-45F9-BBA8-B888E0FFD58C"));
}
#[test]
fn looks_like_uuid_accepts_mixed_case() {
assert!(looks_like_uuid("80630680-4Da6-45F9-bBa8-B888e0FfD58c"));
}
#[test]
fn looks_like_uuid_rejects_four_groups() {
assert!(!looks_like_uuid("80630680-4da6-45f9-bba8"));
}
#[test]
fn looks_like_uuid_rejects_six_groups() {
assert!(!looks_like_uuid(
"80630680-4da6-45f9-bba8-b888e0ffd58c-extra"
));
}
#[test]
fn looks_like_uuid_rejects_8_4_4_4_13_group_lengths() {
assert!(!looks_like_uuid("80630680-4da6-45f9-bba8-b888e0ffd58cc"));
}
#[test]
fn looks_like_uuid_rejects_7_4_4_4_12_group_lengths() {
assert!(!looks_like_uuid("8063068-4da6-45f9-bba8-b888e0ffd58c0"));
}
#[test]
fn looks_like_uuid_rejects_non_hex_chars() {
assert!(!looks_like_uuid("g0630680-4da6-45f9-bba8-b888e0ffd58c"));
assert!(!looks_like_uuid("80630680-4dz6-45f9-bba8-b888e0ffd58c"));
assert!(!looks_like_uuid("80630680-4da6-45f9-bba8-b888e0ffd58z"));
}
#[test]
fn looks_like_uuid_rejects_empty_string() {
assert!(!looks_like_uuid(""));
}
#[test]
fn looks_like_uuid_rejects_string_with_no_dashes() {
assert!(!looks_like_uuid("806306804da645f9bba8b888e0ffd58c"));
}
#[test]
fn looks_like_uuid_rejects_bare_dashes() {
assert!(!looks_like_uuid("----"));
}
const UUID: &str = "80630680-4da6-45f9-bba8-b888e0ffd58c";
fn argv(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn fallback_rewrites_bare_uuid_to_get() {
let cli = parse_with_uuid_fallback(argv(&["socket-patch", UUID])).unwrap();
match cli.command {
Commands::Get(args) => assert_eq!(args.identifier, UUID),
_ => panic!("expected Commands::Get"),
}
}
#[test]
fn fallback_preserves_trailing_flags() {
let cli = parse_with_uuid_fallback(argv(&["socket-patch", UUID, "--json"])).unwrap();
match cli.command {
Commands::Get(args) => {
assert_eq!(args.identifier, UUID);
assert!(args.common.json, "--json should be forwarded to get");
}
_ => panic!("expected Commands::Get"),
}
}
#[test]
fn fallback_returns_original_error_when_first_arg_is_not_uuid() {
let err = match parse_with_uuid_fallback(argv(&["socket-patch", "not-a-uuid"])) {
Ok(_) => panic!("expected parse to fail"),
Err(e) => e,
};
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
#[test]
fn fallback_is_skipped_when_normal_parse_succeeds() {
let cli = parse_with_uuid_fallback(argv(&["socket-patch", "list"])).unwrap();
assert!(matches!(cli.command, Commands::List(_)));
}
#[test]
fn fallback_does_not_double_rewrite_explicit_get() {
let cli = parse_with_uuid_fallback(argv(&["socket-patch", "get", UUID])).unwrap();
match cli.command {
Commands::Get(args) => assert_eq!(args.identifier, UUID),
_ => panic!("expected Commands::Get"),
}
}
#[test]
fn fallback_surfaces_original_error_when_rewrite_also_fails() {
let err = match parse_with_uuid_fallback(argv(&[
"socket-patch",
UUID,
"--invalid-flag-that-get-does-not-accept",
])) {
Ok(_) => panic!("expected parse to fail"),
Err(e) => e,
};
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
}