From 58e0e76feb039486b57843d6da9e0b6b6d0638bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:50:43 +0000 Subject: [PATCH 1/6] Initial plan From de915869b5ef1bfc435085215513b82baf963372 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:58:43 +0000 Subject: [PATCH 2/6] Fix URI encoding to preserve unreserved characters (-, _, ., ~) Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com> --- src/aliyun/signature.rs | 94 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index 4b230cb..27d5630 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -1,10 +1,44 @@ use anyhow::{Context, Result}; use chrono::Utc; -use percent_encoding::{NON_ALPHANUMERIC, percent_encode}; +use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; use rand::RngCore; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; +/// RFC 3986 unreserved characters that should NOT be percent-encoded. +/// Unreserved = ALPHA / DIGIT / "-" / "_" / "." / "~" +/// This set includes all characters EXCEPT alphanumerics and the four unreserved punctuation marks. +const FRAGMENT: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'<') + .add(b'>') + .add(b'`') + .add(b'#') + .add(b'?') + .add(b'{') + .add(b'}') + .add(b'%') + .add(b'/') + .add(b':') + .add(b';') + .add(b'=') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'|') + .add(b'&') + .add(b'+') + .add(b',') + .add(b'$') + .add(b'!') + .add(b'\'') + .add(b'(') + .add(b')') + .add(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 +92,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(), FRAGMENT), + percent_encode(v.as_bytes(), FRAGMENT) ) }) .collect::>() @@ -84,7 +118,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(), FRAGMENT).to_string()) .collect::>() .join("/"), ); @@ -336,4 +370,56 @@ 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] + 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"); + } } From 0ff7ddf73c6d9498b2f48eec29c948249f4ecf6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:00:32 +0000 Subject: [PATCH 3/6] Improve documentation comment for FRAGMENT encoding set Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com> --- src/aliyun/signature.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index 27d5630..157e910 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -5,9 +5,9 @@ use rand::RngCore; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; -/// RFC 3986 unreserved characters that should NOT be percent-encoded. -/// Unreserved = ALPHA / DIGIT / "-" / "_" / "." / "~" -/// This set includes all characters EXCEPT alphanumerics and the four unreserved punctuation marks. +/// Custom encoding set for RFC 3986 compliance. +/// This set defines characters that SHOULD be percent-encoded. +/// It excludes alphanumerics and RFC 3986 unreserved characters: "-" / "_" / "." / "~" const FRAGMENT: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') From 19a21d288eebdad9eb8b4eb123f67c79d74c8883 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:02:05 +0000 Subject: [PATCH 4/6] Add test case for path separator preservation in URI canonicalization Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com> --- src/aliyun/signature.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index 157e910..c933ac2 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -396,6 +396,19 @@ mod tests { 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 that / within a segment would be handled (though this is unusual) + // Note: In practice, / characters within segments should already be encoded before canonicalization + assert_eq!( + AliyunSigner::canonicalize_uri("/"), + "/" + ); } #[test] From fd991002fe3ee63cb024d9dee38c1b6a170aa0ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:03:27 +0000 Subject: [PATCH 5/6] Enhance documentation for FRAGMENT encoding set with detailed character explanations Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com> --- src/aliyun/signature.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index c933ac2..01559e0 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -8,6 +8,15 @@ use std::collections::BTreeMap; /// Custom encoding set for RFC 3986 compliance. /// This set defines characters that SHOULD be percent-encoded. /// It excludes alphanumerics and RFC 3986 unreserved characters: "-" / "_" / "." / "~" +/// +/// The set includes: +/// - Control characters (via CONTROLS base set) +/// - Special characters that have meaning in URIs/URLs (e.g., ?, #, /, :, @, etc.) +/// - Characters that need escaping for safety (e.g., space, ", <, >, etc.) +/// - Reserved characters per RFC 3986 (e.g., &, =, +, $, etc.) +/// +/// Note: "/" is included but handled specially in canonicalize_uri where paths are +/// split by "/" before encoding segments, preserving path structure. const FRAGMENT: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') From 8a14c918324959fe0924aea9e191f8b2509bb0df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:58:49 +0000 Subject: [PATCH 6/6] Use NON_ALPHANUMERIC.remove() to exclude unreserved characters Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com> --- src/aliyun/signature.rs | 64 +++++++---------------------------------- 1 file changed, 11 insertions(+), 53 deletions(-) diff --git a/src/aliyun/signature.rs b/src/aliyun/signature.rs index 01559e0..cbe33a6 100644 --- a/src/aliyun/signature.rs +++ b/src/aliyun/signature.rs @@ -1,52 +1,17 @@ use anyhow::{Context, Result}; use chrono::Utc; -use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_encode}; use rand::RngCore; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; -/// Custom encoding set for RFC 3986 compliance. -/// This set defines characters that SHOULD be percent-encoded. -/// It excludes alphanumerics and RFC 3986 unreserved characters: "-" / "_" / "." / "~" -/// -/// The set includes: -/// - Control characters (via CONTROLS base set) -/// - Special characters that have meaning in URIs/URLs (e.g., ?, #, /, :, @, etc.) -/// - Characters that need escaping for safety (e.g., space, ", <, >, etc.) -/// - Reserved characters per RFC 3986 (e.g., &, =, +, $, etc.) -/// -/// Note: "/" is included but handled specially in canonicalize_uri where paths are -/// split by "/" before encoding segments, preserving path structure. -const FRAGMENT: &AsciiSet = &CONTROLS - .add(b' ') - .add(b'"') - .add(b'<') - .add(b'>') - .add(b'`') - .add(b'#') - .add(b'?') - .add(b'{') - .add(b'}') - .add(b'%') - .add(b'/') - .add(b':') - .add(b';') - .add(b'=') - .add(b'@') - .add(b'[') - .add(b'\\') - .add(b']') - .add(b'^') - .add(b'|') - .add(b'&') - .add(b'+') - .add(b',') - .add(b'$') - .add(b'!') - .add(b'\'') - .add(b'(') - .add(b')') - .add(b'*'); +/// 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) /// @@ -101,8 +66,8 @@ impl AliyunSigner { .map(|(k, v)| { format!( "{}={}", - percent_encode(k.as_bytes(), FRAGMENT), - percent_encode(v.as_bytes(), FRAGMENT) + percent_encode(k.as_bytes(), UNRESERVED), + percent_encode(v.as_bytes(), UNRESERVED) ) }) .collect::>() @@ -127,7 +92,7 @@ impl AliyunSigner { out.push_str( &trimmed .split('/') - .map(|segment| percent_encode(segment.as_bytes(), FRAGMENT).to_string()) + .map(|segment| percent_encode(segment.as_bytes(), UNRESERVED).to_string()) .collect::>() .join("/"), ); @@ -411,13 +376,6 @@ mod tests { AliyunSigner::canonicalize_uri("/path/to/resource"), "/path/to/resource" ); - - // Test that / within a segment would be handled (though this is unusual) - // Note: In practice, / characters within segments should already be encoded before canonicalization - assert_eq!( - AliyunSigner::canonicalize_uri("/"), - "/" - ); } #[test]