diff --git a/.changeset/fix-api-version-syntax.md b/.changeset/fix-api-version-syntax.md new file mode 100644 index 00000000..58e7df6c --- /dev/null +++ b/.changeset/fix-api-version-syntax.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix `:` syntax so unlisted Discovery APIs can be called directly diff --git a/crates/google-workspace-cli/src/main.rs b/crates/google-workspace-cli/src/main.rs index 41dcc1e1..77588f40 100644 --- a/crates/google-workspace-cli/src/main.rs +++ b/crates/google-workspace-cli/src/main.rs @@ -325,10 +325,12 @@ pub fn parse_service_and_version( let mut service_arg = first_arg; let mut version_override: Option = None; - // Check for --api-version flag anywhere in args + // Check for --api-version flag anywhere in args (space-separated or = form) for i in 0..args.len() { if args[i] == "--api-version" && i + 1 < args.len() { version_override = Some(args[i + 1].clone()); + } else if let Some(ver) = args[i].strip_prefix("--api-version=") { + version_override = Some(ver.to_string()); } } @@ -340,8 +342,27 @@ pub fn parse_service_and_version( } } - let (api_name, default_version) = services::resolve_service(service_arg)?; - let version = version_override.unwrap_or(default_version); + let (api_name, version) = match services::resolve_service(service_arg) { + Ok((name, default_ver)) => (name, version_override.unwrap_or(default_ver)), + Err(e) => { + if let Some(ver) = version_override { + let is_valid = |s: &str| { + !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') + }; + if is_valid(service_arg) && is_valid(&ver) { + (service_arg.to_string(), ver) + } else { + return Err(GwsError::Validation( + "Invalid service name or version: only alphanumeric characters, underscores, hyphens, and dots are allowed.".to_string() + )); + } + } else { + return Err(e); + } + } + }; Ok((api_name, version)) } @@ -735,4 +756,59 @@ mod tests { let scopes: Vec = vec![]; assert_eq!(select_scope(&scopes), None); } + + #[test] + fn test_parse_service_and_version_unlisted_with_colon() { + // admob is not in the known services list, but admob:v1 should work + let args = vec![ + "gws".to_string(), + "admob:v1".to_string(), + "accounts".to_string(), + "list".to_string(), + ]; + let result = parse_service_and_version(&args, "admob:v1"); + assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); + } + + #[test] + fn test_parse_service_and_version_unlisted_with_equals_flag() { + // admob is not in the known services list; --api-version=v1 (equals form) should work + let args = vec![ + "gws".to_string(), + "admob".to_string(), + "--api-version=v1".to_string(), + "accounts".to_string(), + "list".to_string(), + ]; + let result = parse_service_and_version(&args, "admob"); + assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); + } + + #[test] + fn test_parse_service_and_version_rejects_path_traversal_in_service() { + let args = vec![ + "gws".to_string(), + "../evil:v1".to_string(), + ]; + let result = parse_service_and_version(&args, "../evil:v1"); + assert!(result.is_err(), "path traversal in service name must be rejected"); + } + + #[test] + fn test_parse_service_and_version_rejects_path_traversal_in_version() { + let args = vec![ + "gws".to_string(), + "admob".to_string(), + "--api-version=../evil".to_string(), + ]; + let result = parse_service_and_version(&args, "admob"); + assert!(result.is_err(), "path traversal in version must be rejected"); + } + + #[test] + fn test_parse_service_and_version_rejects_special_chars_in_service() { + let args = vec!["gws".to_string(), "svc?foo:v1".to_string()]; + let result = parse_service_and_version(&args, "svc?foo:v1"); + assert!(result.is_err(), "special chars in service name must be rejected"); + } }