Skip to content

Commit e32b4f1

Browse files
Powerbyte7Powerbyte7
authored andcommitted
Avoid percent-encoding for "(" and ")" characters.
The VRChat API does not accept percent-encoded variants of InstanceID, specifically the characters "(" and ")". This causes an error when attempting the following API call: ``` https://api.vrchat.cloud/api/1/instances/wrld_28aab3e4-953f-4c85-9223-ef29a0873a6e:31124%7Egroup%28grp_b2a19685-c53e-4c5a-9503-744a5373bf2c%29%7EgroupAccessType%28plus%29%7Eregion%28use%29/shortName ``` ```json {"error":{"message":"\"malformed url\"","status_code":400,"waf_code":26497}} ``` When using the raw string without encoding, the API yields a `200` response as expected. ``` https://vrchat.com/api/1/instances/wrld_28aab3e4-953f-4c85-9223-ef29a0873a6e:31124~group(grp_b2a19685-c53e-4c5a-9503-744a5373bf2c)~groupAccessType(plus)~region(use)/shortName ```
1 parent af1fae7 commit e32b4f1

3 files changed

Lines changed: 111 additions & 3 deletions

File tree

src/apis/mod.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,44 @@ impl<T> From<std::io::Error> for Error<T> {
6666
}
6767
}
6868

69+
fn hex_digit(b: u8) -> Option<u8> {
70+
match b {
71+
b'0'..=b'9' => Some(b - b'0'),
72+
b'a'..=b'f' => Some(b - b'a' + 10),
73+
b'A'..=b'F' => Some(b - b'A' + 10),
74+
_ => None,
75+
}
76+
}
77+
78+
fn parse_percent_encoded(bytes: &[u8; 3]) -> Option<u8> {
79+
if bytes[0] != b'%' { return None; }
80+
let hi = hex_digit(bytes[1])?;
81+
let lo = hex_digit(bytes[2])?;
82+
Some((hi << 4) | lo)
83+
}
84+
6985
pub fn urlencode<T: AsRef<str>>(s: T) -> String {
70-
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect()
86+
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes())
87+
.map(|string| {
88+
debug_assert!(
89+
!string.starts_with('%') || string.len() == 3,
90+
"the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings"
91+
);
92+
93+
let parsed = match string.as_bytes().try_into() {
94+
Ok(bytes) => parse_percent_encoded(bytes),
95+
Err(_) => None,
96+
};
97+
98+
// The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID.
99+
// The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed.
100+
match parsed {
101+
Some(b'(') => "(",
102+
Some(b')') => ")",
103+
_ => string
104+
}
105+
})
106+
.collect()
71107
}
72108

73109
pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> {

templates/reqwest-trait/api_mod.mustache

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,44 @@ impl <T> From<std::io::Error> for Error<T> {
9595
}
9696
}
9797

98+
fn hex_digit(b: u8) -> Option<u8> {
99+
match b {
100+
b'0'..=b'9' => Some(b - b'0'),
101+
b'a'..=b'f' => Some(b - b'a' + 10),
102+
b'A'..=b'F' => Some(b - b'A' + 10),
103+
_ => None,
104+
}
105+
}
106+
107+
fn parse_percent_encoded(bytes: &[u8; 3]) -> Option<u8> {
108+
if bytes[0] != b'%' { return None; }
109+
let hi = hex_digit(bytes[1])?;
110+
let lo = hex_digit(bytes[2])?;
111+
Some((hi << 4) | lo)
112+
}
113+
98114
pub fn urlencode<T: AsRef<str>>(s: T) -> String {
99-
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect()
115+
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes())
116+
.map(|string| {
117+
debug_assert!(
118+
!string.starts_with('%') || string.len() == 3,
119+
"the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings"
120+
);
121+
122+
let parsed = match string.as_bytes().try_into() {
123+
Ok(bytes) => parse_percent_encoded(bytes),
124+
Err(_) => None,
125+
};
126+
127+
// The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID.
128+
// The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed.
129+
match parsed {
130+
Some(b'(') => "(",
131+
Some(b')') => ")",
132+
_ => string
133+
}
134+
})
135+
.collect()
100136
}
101137

102138
pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> {

templates/reqwest/api_mod.mustache

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,44 @@ impl <T> From<std::io::Error> for Error<T> {
118118
}
119119
}
120120

121+
fn hex_digit(b: u8) -> Option<u8> {
122+
match b {
123+
b'0'..=b'9' => Some(b - b'0'),
124+
b'a'..=b'f' => Some(b - b'a' + 10),
125+
b'A'..=b'F' => Some(b - b'A' + 10),
126+
_ => None,
127+
}
128+
}
129+
130+
fn parse_percent_encoded(bytes: &[u8; 3]) -> Option<u8> {
131+
if bytes[0] != b'%' { return None; }
132+
let hi = hex_digit(bytes[1])?;
133+
let lo = hex_digit(bytes[2])?;
134+
Some((hi << 4) | lo)
135+
}
136+
121137
pub fn urlencode<T: AsRef<str>>(s: T) -> String {
122-
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect()
138+
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes())
139+
.map(|string| {
140+
debug_assert!(
141+
!string.starts_with('%') || string.len() == 3,
142+
"the iterator should yield percent-encoded strings of exactly 3 bytes, or unescaped strings"
143+
);
144+
145+
let parsed = match string.as_bytes().try_into() {
146+
Ok(bytes) => parse_percent_encoded(bytes),
147+
Err(_) => None,
148+
};
149+
150+
// The VRChat API deviates from the application/x-www-form-urlencoded percent-encode set, for values like the InstanceID.
151+
// The characters bellow should remain unchanged for URI parameters, or requests will be rejected as malformed.
152+
match parsed {
153+
Some(b'(') => "(",
154+
Some(b')') => ")",
155+
_ => string
156+
}
157+
})
158+
.collect()
123159
}
124160

125161
pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> {

0 commit comments

Comments
 (0)