Skip to main content

nodejs/stdlib/
querystring.rs

1//! Node `querystring` module: `parse`/`stringify` (with the `escape`/`unescape`
2//! aliases `encode`/`decode`). Values are percent-decoded/encoded with `+`
3//! standing for a space, the legacy `application/x-www-form-urlencoded` rules
4//! Node's `querystring` uses (distinct from the `qs` package express also ships).
5
6use 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            // arg_str borrows the host; compute it BEFORE the new_str with_host.
26            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        // `querystring.unescapeBuffer(str[, decodeSpaces])` → a Buffer of the raw
34        // decoded bytes. `+` is decoded to a space only when `decodeSpaces` is true
35        // (Node's default is false).
36        "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
48/// `querystring.parse(str[, sep[, eq]])` → an object of decoded key/value pairs.
49/// A repeated key collects its values into an array, matching Node.
50///
51/// An explicitly-passed `undefined` separator means "use the default", not the
52/// STRING `"undefined"` — `body-parser` calls
53/// `parse(body, undefined, undefined, { maxKeys })`, and coercing those to text
54/// made the whole body one key.
55fn 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            // A repeated key promotes to (and then extends) an array.
80            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
103/// `querystring.stringify(obj[, sep[, eq]])`.
104fn 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        // An array value emits one `key=elem` pair per element.
130        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
151/// `querystring.unescapeBuffer` core — decode `%XX` to raw bytes (and `+` to a
152/// space when `decode_spaces`), leaving malformed escapes literal.
153fn 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
186/// `querystring.escape` — percent-encode (space → `%20`, like Node; NOT `+`).
187fn 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
211/// Reverse `escape` (`+` → space, `%XX` → byte). Malformed escapes pass through
212/// literally, as Node's `querystring.unescape` does (it never throws).
213fn 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}