From f4a5d928988ab540337886975ce59410d9f9097e Mon Sep 17 00:00:00 2001 From: ZIFeIYUuuuuuu Date: Wed, 9 Sep 2026 13:03:19 +0800 Subject: [PATCH] fix(script): preserve unreserved script ID characters --- .changeset/fix-script-id-path-encoding.md | 5 +++ crates/google-workspace-cli/src/executor.rs | 37 +++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .changeset/fix-script-id-path-encoding.md diff --git a/.changeset/fix-script-id-path-encoding.md b/.changeset/fix-script-id-path-encoding.md new file mode 100644 index 000000000..5e386b2d7 --- /dev/null +++ b/.changeset/fix-script-id-path-encoding.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Preserve RFC 3986 unreserved characters in Apps Script IDs used in URL paths. diff --git a/crates/google-workspace-cli/src/executor.rs b/crates/google-workspace-cli/src/executor.rs index 46f31ac4b..ef3d2efe0 100644 --- a/crates/google-workspace-cli/src/executor.rs +++ b/crates/google-workspace-cli/src/executor.rs @@ -714,6 +714,10 @@ fn render_path_template( let encoded = if is_plus { let validated = crate::validate::validate_resource_name(&val_str)?; crate::validate::encode_path_preserving_slashes(validated) + } else if key == "scriptId" { + // Apps Script's :run endpoint rejects percent-encoded RFC 3986 + // unreserved characters (`-`, `_`, `.`, `~`) in script IDs. + encode_script_id_path_segment(&val_str) } else { crate::validate::encode_path_segment(&val_str) }; @@ -729,6 +733,14 @@ fn render_path_template( Ok(rendered) } +fn encode_script_id_path_segment(value: &str) -> String { + crate::validate::encode_path_segment(value) + .replace("%2D", "-") + .replace("%5F", "_") + .replace("%2E", ".") + .replace("%7E", "~") +} + /// Attempts to extract a GCP console enable URL from a Google API `accessNotConfigured` /// error message. /// @@ -1882,6 +1894,31 @@ mod tests { ); } + #[test] + fn test_build_url_preserves_unreserved_script_id_characters() { + let doc = RestDescription { + base_url: Some("https://script.googleapis.com/".to_string()), + ..Default::default() + }; + let method = RestMethod { + path: "v1/scripts/{scriptId}:run".to_string(), + flat_path: Some("v1/scripts/{scriptId}:run".to_string()), + ..Default::default() + }; + let mut params = Map::new(); + params.insert( + "scriptId".to_string(), + json!("1-ukYIb6_NbryM5UJXBwAMeY5GKtSBA05csXty3iY7DhO-P"), + ); + + let (url, _) = build_url(&doc, &method, ¶ms, false).unwrap(); + + assert_eq!( + url, + "https://script.googleapis.com/v1/scripts/1-ukYIb6_NbryM5UJXBwAMeY5GKtSBA05csXty3iY7DhO-P:run" + ); + } + #[test] fn test_build_url_errors_for_path_param_not_in_template() { let doc = RestDescription {