nodejs/stdlib/
querystring.rs1use crate::host::{with_host, JsObj};
7use fusevm::Value;
8use indexmap::IndexMap;
9
10pub const METHODS: &[&str] = &[
11 "parse",
12 "stringify",
13 "escape",
14 "unescape",
15 "encode",
16 "decode",
17 "unescapeBuffer",
18];
19
20pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
21 Some(match method {
22 "parse" | "decode" => Ok(parse(&super::arg_str(args, 0), args)),
23 "stringify" | "encode" => Ok(stringify(args)),
24 "escape" => {
25 let s = super::arg_str(args, 0);
27 Ok(with_host(|h| h.new_str(escape(&s))))
28 }
29 "unescape" => {
30 let s = super::arg_str(args, 0);
31 Ok(with_host(|h| h.new_str(unescape(&s))))
32 }
33 "unescapeBuffer" => {
37 let s = super::arg_str(args, 0);
38 let decode_spaces = matches!(args.get(1), Some(Value::Bool(true)));
39 Ok(super::buffer::from_bytes(&unescape_buffer(
40 &s,
41 decode_spaces,
42 )))
43 }
44 _ => return None,
45 })
46}
47
48fn parse(s: &str, args: &[Value]) -> Value {
56 let sep = args
57 .get(1)
58 .filter(|v| !matches!(v, Value::Undef))
59 .map(|_| super::arg_str(args, 1))
60 .filter(|s| !s.is_empty())
61 .unwrap_or_else(|| "&".into());
62 let eq = args
63 .get(2)
64 .filter(|v| !matches!(v, Value::Undef))
65 .map(|_| super::arg_str(args, 2))
66 .filter(|s| !s.is_empty())
67 .unwrap_or_else(|| "=".into());
68 let max_keys = args
72 .get(3)
73 .filter(|v| !matches!(v, Value::Undef))
74 .and_then(|o| crate::builtins::get_property(o, "maxKeys").ok())
75 .filter(|v| !matches!(v, Value::Undef))
76 .map(|v| with_host(|h| h.to_number(&v)))
77 .filter(|n| n.is_finite() && *n >= 0.0)
78 .map(|n| n as usize)
79 .unwrap_or(1000);
80 let mut map: IndexMap<String, Value> = IndexMap::new();
81 if !s.is_empty() {
82 for pair in s.split(&sep) {
83 if pair.is_empty() {
84 continue;
85 }
86 if max_keys != 0 && map.len() >= max_keys {
87 break;
88 }
89 let (k, v) = match pair.split_once(&eq) {
90 Some((k, v)) => (unescape_form(k), unescape_form(v)),
91 None => (unescape_form(pair), String::new()),
92 };
93 let val = with_host(|h| h.new_str(v));
94 match map.get(&k).cloned() {
96 Some(existing) => {
97 let is_arr = with_host(|h| matches!(h.get(&existing), Some(JsObj::Array(_))));
98 if is_arr {
99 with_host(|h| {
100 if let Some(JsObj::Array(items)) = h.get_mut(&existing) {
101 items.push(val);
102 }
103 });
104 } else {
105 let arr = with_host(|h| h.new_array(vec![existing, val]));
106 map.insert(k, arr);
107 }
108 }
109 None => {
110 map.insert(k, val);
111 }
112 }
113 }
114 }
115 with_host(|h| {
119 let obj = h.new_object(map);
120 let null = h.null();
121 h.set_proto(&obj, null);
122 obj
123 })
124}
125
126fn stringify_value(v: &Value) -> String {
134 with_host(|h| match v {
135 Value::Bool(_) | Value::Int(_) | Value::Float(_) => h.str_of(v),
136 Value::Str(_) => h.str_of(v),
137 Value::Obj(_) => match h.get(v) {
138 Some(JsObj::Str(_)) | Some(JsObj::BigInt(_)) => h.str_of(v),
139 _ => String::new(),
140 },
141 _ => String::new(),
142 })
143}
144
145fn stringify(args: &[Value]) -> Value {
147 let obj = args.first().cloned().unwrap_or(Value::Undef);
148 let sep = args
149 .get(1)
150 .filter(|v| !matches!(v, Value::Undef))
151 .map(|_| super::arg_str(args, 1))
152 .filter(|s| !s.is_empty())
153 .unwrap_or_else(|| "&".into());
154 let eq = args
155 .get(2)
156 .filter(|v| !matches!(v, Value::Undef))
157 .map(|_| super::arg_str(args, 2))
158 .filter(|s| !s.is_empty())
159 .unwrap_or_else(|| "=".into());
160 let entries = with_host(|h| match h.get(&obj) {
161 Some(JsObj::Object(p)) => p
162 .iter()
163 .filter(|(k, _)| !k.starts_with("@@"))
164 .map(|(k, v)| (k.clone(), v.clone()))
165 .collect::<Vec<_>>(),
166 _ => Vec::new(),
167 });
168 let mut parts: Vec<String> = Vec::new();
169 for (k, v) in entries {
170 let ek = escape(&k);
171 let elems = with_host(|h| match h.get(&v) {
173 Some(JsObj::Array(items)) => Some(items.clone()),
174 _ => None,
175 });
176 match elems {
177 Some(list) => {
178 for e in list {
181 parts.push(format!("{ek}{eq}{}", escape(&stringify_value(&e))));
182 }
183 }
184 None => {
185 parts.push(format!("{ek}{eq}{}", escape(&stringify_value(&v))));
186 }
187 }
188 }
189 with_host(|h| h.new_str(parts.join(&sep)))
190}
191
192fn unescape_buffer(s: &str, decode_spaces: bool) -> Vec<u8> {
195 let b = s.as_bytes();
196 let mut out: Vec<u8> = Vec::with_capacity(b.len());
197 let mut i = 0;
198 while i < b.len() {
199 match b[i] {
200 b'+' if decode_spaces => {
201 out.push(b' ');
202 i += 1;
203 }
204 b'%' if i + 2 < b.len() => {
205 let hi = (b[i + 1] as char).to_digit(16);
206 let lo = (b[i + 2] as char).to_digit(16);
207 match (hi, lo) {
208 (Some(h), Some(l)) => {
209 out.push((h * 16 + l) as u8);
210 i += 3;
211 }
212 _ => {
213 out.push(b'%');
214 i += 1;
215 }
216 }
217 }
218 c => {
219 out.push(c);
220 i += 1;
221 }
222 }
223 }
224 out
225}
226
227fn escape(s: &str) -> String {
229 const UNRESERVED: &[u8] =
230 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
231 let mut out = String::with_capacity(s.len());
232 for &b in s.as_bytes() {
233 if UNRESERVED.contains(&b) {
234 out.push(b as char);
235 } else {
236 out.push('%');
237 out.push(
238 char::from_digit((b >> 4) as u32, 16)
239 .unwrap()
240 .to_ascii_uppercase(),
241 );
242 out.push(
243 char::from_digit((b & 0xf) as u32, 16)
244 .unwrap()
245 .to_ascii_uppercase(),
246 );
247 }
248 }
249 out
250}
251
252fn unescape(s: &str) -> String {
260 unescape_inner(s, false)
261}
262
263fn unescape_form(s: &str) -> String {
265 unescape_inner(s, true)
266}
267
268fn unescape_inner(s: &str, plus_is_space: bool) -> String {
269 let bytes = s.as_bytes();
270 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
271 let mut i = 0;
272 while i < bytes.len() {
273 match bytes[i] {
274 b'+' if plus_is_space => {
275 out.push(b' ');
276 i += 1;
277 }
278 b'%' if i + 2 < bytes.len() => {
279 let hi = (bytes[i + 1] as char).to_digit(16);
280 let lo = (bytes[i + 2] as char).to_digit(16);
281 match (hi, lo) {
282 (Some(h), Some(l)) => {
283 out.push((h * 16 + l) as u8);
284 i += 3;
285 }
286 _ => {
287 out.push(b'%');
288 i += 1;
289 }
290 }
291 }
292 b => {
293 out.push(b);
294 i += 1;
295 }
296 }
297 }
298 String::from_utf8_lossy(&out).into_owned()
299}