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 mut map: IndexMap<String, Value> = IndexMap::new();
69 if !s.is_empty() {
70 for pair in s.split(&sep) {
71 if pair.is_empty() {
72 continue;
73 }
74 let (k, v) = match pair.split_once(&eq) {
75 Some((k, v)) => (unescape(k), unescape(v)),
76 None => (unescape(pair), String::new()),
77 };
78 let val = with_host(|h| h.new_str(v));
79 match map.get(&k).cloned() {
81 Some(existing) => {
82 let is_arr = with_host(|h| matches!(h.get(&existing), Some(JsObj::Array(_))));
83 if is_arr {
84 with_host(|h| {
85 if let Some(JsObj::Array(items)) = h.get_mut(&existing) {
86 items.push(val);
87 }
88 });
89 } else {
90 let arr = with_host(|h| h.new_array(vec![existing, val]));
91 map.insert(k, arr);
92 }
93 }
94 None => {
95 map.insert(k, val);
96 }
97 }
98 }
99 }
100 with_host(|h| h.new_object(map))
101}
102
103fn stringify(args: &[Value]) -> Value {
105 let obj = args.first().cloned().unwrap_or(Value::Undef);
106 let sep = args
107 .get(1)
108 .filter(|v| !matches!(v, Value::Undef))
109 .map(|_| super::arg_str(args, 1))
110 .filter(|s| !s.is_empty())
111 .unwrap_or_else(|| "&".into());
112 let eq = args
113 .get(2)
114 .filter(|v| !matches!(v, Value::Undef))
115 .map(|_| super::arg_str(args, 2))
116 .filter(|s| !s.is_empty())
117 .unwrap_or_else(|| "=".into());
118 let entries = with_host(|h| match h.get(&obj) {
119 Some(JsObj::Object(p)) => p
120 .iter()
121 .filter(|(k, _)| !k.starts_with("@@"))
122 .map(|(k, v)| (k.clone(), v.clone()))
123 .collect::<Vec<_>>(),
124 _ => Vec::new(),
125 });
126 let mut parts: Vec<String> = Vec::new();
127 for (k, v) in entries {
128 let ek = escape(&k);
129 let elems = with_host(|h| match h.get(&v) {
131 Some(JsObj::Array(items)) => {
132 Some(items.iter().map(|x| h.str_of(x)).collect::<Vec<_>>())
133 }
134 _ => None,
135 });
136 match elems {
137 Some(list) => {
138 for e in list {
139 parts.push(format!("{ek}{eq}{}", escape(&e)));
140 }
141 }
142 None => {
143 let ev = with_host(|h| h.str_of(&v));
144 parts.push(format!("{ek}{eq}{}", escape(&ev)));
145 }
146 }
147 }
148 with_host(|h| h.new_str(parts.join(&sep)))
149}
150
151fn unescape_buffer(s: &str, decode_spaces: bool) -> Vec<u8> {
154 let b = s.as_bytes();
155 let mut out: Vec<u8> = Vec::with_capacity(b.len());
156 let mut i = 0;
157 while i < b.len() {
158 match b[i] {
159 b'+' if decode_spaces => {
160 out.push(b' ');
161 i += 1;
162 }
163 b'%' if i + 2 < b.len() => {
164 let hi = (b[i + 1] as char).to_digit(16);
165 let lo = (b[i + 2] as char).to_digit(16);
166 match (hi, lo) {
167 (Some(h), Some(l)) => {
168 out.push((h * 16 + l) as u8);
169 i += 3;
170 }
171 _ => {
172 out.push(b'%');
173 i += 1;
174 }
175 }
176 }
177 c => {
178 out.push(c);
179 i += 1;
180 }
181 }
182 }
183 out
184}
185
186fn escape(s: &str) -> String {
188 const UNRESERVED: &[u8] =
189 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
190 let mut out = String::with_capacity(s.len());
191 for &b in s.as_bytes() {
192 if UNRESERVED.contains(&b) {
193 out.push(b as char);
194 } else {
195 out.push('%');
196 out.push(
197 char::from_digit((b >> 4) as u32, 16)
198 .unwrap()
199 .to_ascii_uppercase(),
200 );
201 out.push(
202 char::from_digit((b & 0xf) as u32, 16)
203 .unwrap()
204 .to_ascii_uppercase(),
205 );
206 }
207 }
208 out
209}
210
211fn unescape(s: &str) -> String {
214 let bytes = s.as_bytes();
215 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
216 let mut i = 0;
217 while i < bytes.len() {
218 match bytes[i] {
219 b'+' => {
220 out.push(b' ');
221 i += 1;
222 }
223 b'%' if i + 2 < bytes.len() => {
224 let hi = (bytes[i + 1] as char).to_digit(16);
225 let lo = (bytes[i + 2] as char).to_digit(16);
226 match (hi, lo) {
227 (Some(h), Some(l)) => {
228 out.push((h * 16 + l) as u8);
229 i += 3;
230 }
231 _ => {
232 out.push(b'%');
233 i += 1;
234 }
235 }
236 }
237 b => {
238 out.push(b);
239 i += 1;
240 }
241 }
242 }
243 String::from_utf8_lossy(&out).into_owned()
244}