diff --git a/src/apis/mod.rs b/src/apis/mod.rs index 4b02c74..08532a1 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -66,8 +66,44 @@ impl From for Error { } } +fn hex_digit(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } +} + +fn parse_percent_encoded(bytes: &[u8; 3]) -> Option { + if bytes[0] != b'%' { return None; } + let hi = hex_digit(bytes[1])?; + let lo = hex_digit(bytes[2])?; + Some((hi << 4) | lo) +} + pub fn urlencode>(s: T) -> String { - ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()) + .map(|string| { + debug_assert!( + !string.starts_with('%') || string.len() == 3, + "the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings" + ); + + let parsed = match string.as_bytes().try_into() { + Ok(bytes) => parse_percent_encoded(bytes), + Err(_) => None, + }; + + // The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID. + // The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed. + match parsed { + Some(b'(') => "(", + Some(b')') => ")", + _ => string + } + }) + .collect() } pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> { diff --git a/templates/reqwest-trait/api_mod.mustache b/templates/reqwest-trait/api_mod.mustache index 41a4ba0..60af285 100644 --- a/templates/reqwest-trait/api_mod.mustache +++ b/templates/reqwest-trait/api_mod.mustache @@ -95,8 +95,44 @@ impl From for Error { } } +fn hex_digit(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } +} + +fn parse_percent_encoded(bytes: &[u8; 3]) -> Option { + if bytes[0] != b'%' { return None; } + let hi = hex_digit(bytes[1])?; + let lo = hex_digit(bytes[2])?; + Some((hi << 4) | lo) +} + pub fn urlencode>(s: T) -> String { - ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()) + .map(|string| { + debug_assert!( + !string.starts_with('%') || string.len() == 3, + "the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings" + ); + + let parsed = match string.as_bytes().try_into() { + Ok(bytes) => parse_percent_encoded(bytes), + Err(_) => None, + }; + + // The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID. + // The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed. + match parsed { + Some(b'(') => "(", + Some(b')') => ")", + _ => string + } + }) + .collect() } pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> { diff --git a/templates/reqwest/api_mod.mustache b/templates/reqwest/api_mod.mustache index ada95b7..389abf0 100644 --- a/templates/reqwest/api_mod.mustache +++ b/templates/reqwest/api_mod.mustache @@ -118,8 +118,44 @@ impl From for Error { } } +fn hex_digit(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } +} + +fn parse_percent_encoded(bytes: &[u8; 3]) -> Option { + if bytes[0] != b'%' { return None; } + let hi = hex_digit(bytes[1])?; + let lo = hex_digit(bytes[2])?; + Some((hi << 4) | lo) +} + pub fn urlencode>(s: T) -> String { - ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()) + .map(|string| { + debug_assert!( + !string.starts_with('%') || string.len() == 3, + "the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings" + ); + + let parsed = match string.as_bytes().try_into() { + Ok(bytes) => parse_percent_encoded(bytes), + Err(_) => None, + }; + + // The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID. + // The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed. + match parsed { + Some(b'(') => "(", + Some(b')') => ")", + _ => string + } + }) + .collect() } pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> {