diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index 4b230cb..cbe33a6 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -1,10 +1,18 @@ use anyhow::{Context, Result}; use chrono::Utc; -use percent_encoding::{NON_ALPHANUMERIC, percent_encode}; +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_encode}; use rand::RngCore; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; +/// RFC 3986 unreserved characters: ALPHA / DIGIT / "-" / "_" / "." / "~" +/// These characters should NOT be percent-encoded. +const UNRESERVED: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~'); + /// Aliyun OpenAPI V3 signature generator (ACS3-HMAC-SHA256) /// /// Docs: https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature @@ -58,8 +66,8 @@ impl AliyunSigner { .map(|(k, v)| { format!( "{}={}", - percent_encode(k.as_bytes(), NON_ALPHANUMERIC), - percent_encode(v.as_bytes(), NON_ALPHANUMERIC) + percent_encode(k.as_bytes(), UNRESERVED), + percent_encode(v.as_bytes(), UNRESERVED) ) }) .collect::>() @@ -84,7 +92,7 @@ impl AliyunSigner { out.push_str( &trimmed .split('/') - .map(|segment| percent_encode(segment.as_bytes(), NON_ALPHANUMERIC).to_string()) + .map(|segment| percent_encode(segment.as_bytes(), UNRESERVED).to_string()) .collect::>() .join("/"), ); @@ -336,4 +344,62 @@ mod tests { "06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0" ); } + + #[test] + fn test_canonicalize_uri_with_unreserved_chars() { + // Test that unreserved characters (-, _, ., ~) are NOT percent-encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path-with_dots.and~tilde"), + "/path-with_dots.and~tilde" + ); + + // Test with multiple segments + assert_eq!( + AliyunSigner::canonicalize_uri("/api/v1.0/user_name-123~test"), + "/api/v1.0/user_name-123~test" + ); + + // Test that special characters ARE encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path with spaces"), + "/path%20with%20spaces" + ); + + // Test mixed case + assert_eq!( + AliyunSigner::canonicalize_uri("/valid-_.~/but spaces"), + "/valid-_.~/but%20spaces" + ); + + // Test that path separators (/) are preserved and not encoded + assert_eq!( + AliyunSigner::canonicalize_uri("/path/to/resource"), + "/path/to/resource" + ); + } + + #[test] + fn test_build_canonical_query_string_with_unreserved_chars() { + // Test that unreserved characters (-, _, ., ~) are NOT percent-encoded + let mut params = BTreeMap::new(); + params.insert("key-1".to_string(), "value_1".to_string()); + params.insert("key.2".to_string(), "value.2".to_string()); + params.insert("key~3".to_string(), "value~3".to_string()); + + let result = AliyunSigner::build_canonical_query_string(¶ms); + // BTreeMap orders keys alphabetically + assert_eq!(result, "key-1=value_1&key.2=value.2&key~3=value~3"); + + // Test that special characters ARE encoded + let mut params2 = BTreeMap::new(); + params2.insert("key with space".to_string(), "value with space".to_string()); + let result2 = AliyunSigner::build_canonical_query_string(¶ms2); + assert_eq!(result2, "key%20with%20space=value%20with%20space"); + + // Test mixed case + let mut params3 = BTreeMap::new(); + params3.insert("valid-_~.key".to_string(), "needs encoding!".to_string()); + let result3 = AliyunSigner::build_canonical_query_string(¶ms3); + assert_eq!(result3, "valid-_~.key=needs%20encoding%21"); + } }