Files
bun.sh/src/js/node/querystring.ts
Meghan Denny 6286824e28 node: some builtins cleanup (#22200)
Co-authored-by: Meghan Denny <meghan@bun.com>
2025-08-27 20:34:37 -07:00

519 lines
17 KiB
TypeScript

// Hardcoded module "node:querystring"
const ArrayIsArray = Array.isArray;
const MathAbs = Math.abs;
const NumberIsFinite = Number.isFinite;
const ObjectKeys = Object.keys;
const StringPrototypeCharCodeAt = String.prototype.charCodeAt;
const StringPrototypeSlice = String.prototype.slice;
const StringPrototypeToUpperCase = String.prototype.toUpperCase;
const NumberPrototypeToString = Number.prototype.toString;
var __commonJS =
(cb, mod: typeof module | undefined = undefined) =>
() => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var require_src = __commonJS((exports, module) => {
/**
* @param {string} str
* @param {Int8Array} noEscapeTable
* @param {string[]} hexTable
* @returns {string}
*/
function encodeStr(str, noEscapeTable, hexTable) {
const len = str.length;
if (len === 0) return "";
let out = "";
let lastPos = 0;
let i = 0;
outer: for (; i < len; i++) {
let c = StringPrototypeCharCodeAt.$call(str, i);
// ASCII
while (c < 0x80) {
if (noEscapeTable[c] !== 1) {
if (lastPos < i) out += StringPrototypeSlice.$call(str, lastPos, i);
lastPos = i + 1;
out += hexTable[c];
}
if (++i === len) break outer;
c = StringPrototypeCharCodeAt.$call(str, i);
}
if (lastPos < i) out += StringPrototypeSlice.$call(str, lastPos, i);
// Multi-byte characters ...
if (c < 0x800) {
lastPos = i + 1;
out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)];
continue;
}
if (c < 0xd800 || c >= 0xe000) {
lastPos = i + 1;
out += hexTable[0xe0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)];
continue;
}
// Surrogate pair
++i;
// This branch should never happen because all URLSearchParams entries
// should already be converted to USVString. But, included for
// completion's sake anyway.
if (i >= len) throw $ERR_INVALID_URI();
const c2 = StringPrototypeCharCodeAt.$call(str, i) & 0x3ff;
lastPos = i + 1;
c = 0x10000 + (((c & 0x3ff) << 10) | c2);
out +=
hexTable[0xf0 | (c >> 18)] +
hexTable[0x80 | ((c >> 12) & 0x3f)] +
hexTable[0x80 | ((c >> 6) & 0x3f)] +
hexTable[0x80 | (c & 0x3f)];
}
if (lastPos === 0) return str;
if (lastPos < len) return out + StringPrototypeSlice.$call(str, lastPos);
return out;
}
const hexTable = new Array(256);
for (let i = 0; i < 256; ++i)
hexTable[i] = "%" + StringPrototypeToUpperCase.$call((i < 16 ? "0" : "") + NumberPrototypeToString.$call(i, 16));
// prettier-ignore
const isHexTable = new Int8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ...
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ... 256
]);
const QueryString = (module.exports = {
unescapeBuffer,
// `unescape()` is a JS global, so we need to use a different local name
unescape: qsUnescape,
// `escape()` is a JS global, so we need to use a different local name
escape: qsEscape,
stringify,
encode: stringify,
parse,
decode: parse,
});
// prettier-ignore
const unhexTable = new Int8Array([
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
+0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255
]);
/**
* A safe fast alternative to decodeURIComponent
* @param {string} s
* @param {boolean} decodeSpaces
* @returns {string}
*/
function unescapeBuffer(s, decodeSpaces) {
const out = Buffer.allocUnsafe(s.length);
let index = 0;
let outIndex = 0;
let currentChar;
let nextChar;
let hexHigh;
let hexLow;
const maxLength = s.length - 2;
// Flag to know if some hex chars have been decoded
let hasHex = false;
while (index < s.length) {
currentChar = StringPrototypeCharCodeAt.$call(s, index);
if (currentChar === 43 /* '+' */ && decodeSpaces) {
out[outIndex++] = 32; // ' '
index++;
continue;
}
if (currentChar === 37 /* '%' */ && index < maxLength) {
currentChar = StringPrototypeCharCodeAt.$call(s, ++index);
hexHigh = unhexTable[currentChar];
if (!(hexHigh >= 0)) {
out[outIndex++] = 37; // '%'
continue;
} else {
nextChar = StringPrototypeCharCodeAt.$call(s, ++index);
hexLow = unhexTable[nextChar];
if (!(hexLow >= 0)) {
out[outIndex++] = 37; // '%'
index--;
} else {
hasHex = true;
currentChar = hexHigh * 16 + hexLow;
}
}
}
out[outIndex++] = currentChar;
index++;
}
return hasHex ? out.slice(0, outIndex) : out;
}
/**
* @param {string} s
* @param {boolean} decodeSpaces
* @returns {string}
*/
function qsUnescape(s, decodeSpaces) {
try {
return decodeURIComponent(s);
} catch {
return QueryString.unescapeBuffer(s, decodeSpaces).toString();
}
}
// These characters do not need escaping when generating query strings:
// ! - . _ ~
// ' ( ) *
// digits
// alpha (uppercase)
// alpha (lowercase)
// prettier-ignore
const noEscape = new Int8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127
]);
/**
* QueryString.escape() replaces encodeURIComponent()
* @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4
* @param {any} str
* @returns {string}
*/
function qsEscape(str) {
if (typeof str !== "string") {
if (typeof str === "object") str = String(str);
else str += "";
}
return encodeStr(str, noEscape, hexTable);
}
/**
* @param {string | number | bigint | boolean | symbol | undefined | null} v
* @returns {string}
*/
function stringifyPrimitive(v) {
if (typeof v === "string") return v;
if (typeof v === "number" && NumberIsFinite(v)) return "" + v;
if (typeof v === "bigint") return "" + v;
if (typeof v === "boolean") return v ? "true" : "false";
return "";
}
/**
* @param {string | number | bigint | boolean} v
* @param {(v: string) => string} encode
* @returns {string}
*/
function encodeStringified(v, encode) {
if (typeof v === "string") return v.length ? encode(v) : "";
if (typeof v === "number" && NumberIsFinite(v)) {
// Values >= 1e21 automatically switch to scientific notation which requires
// escaping due to the inclusion of a '+' in the output
return MathAbs(v) < 1e21 ? "" + v : encode("" + v);
}
if (typeof v === "bigint") return "" + v;
if (typeof v === "boolean") return v ? "true" : "false";
return "";
}
/**
* @param {string | number | boolean | null} v
* @param {(v: string) => string} encode
* @returns {string}
*/
function encodeStringifiedCustom(v, encode) {
return encode(stringifyPrimitive(v));
}
/**
* @param {Record<string, string | number | boolean
* | ReadonlyArray<string | number | boolean> | null>} obj
* @param {string} [sep]
* @param {string} [eq]
* @param {{ encodeURIComponent?: (v: string) => string }} [options]
* @returns {string}
*/
function stringify(obj, sep, eq, options) {
sep ||= "&";
eq ||= "=";
let encode = QueryString.escape;
if (options && typeof options.encodeURIComponent === "function") {
encode = options.encodeURIComponent;
}
const convert = encode === qsEscape ? encodeStringified : encodeStringifiedCustom;
if (obj !== null && typeof obj === "object") {
const keys = ObjectKeys(obj);
const len = keys.length;
let fields = "";
for (let i = 0; i < len; ++i) {
const k = keys[i];
const v = obj[k];
let ks = convert(k, encode);
ks += eq;
if (ArrayIsArray(v)) {
const vlen = v.length;
if (vlen === 0) continue;
if (fields) fields += sep;
for (let j = 0; j < vlen; ++j) {
if (j) fields += sep;
fields += ks;
fields += convert(v[j], encode);
}
} else {
if (fields) fields += sep;
fields += ks;
fields += convert(v, encode);
}
}
return fields;
}
return "";
}
/**
* @param {string} str
* @returns {number[]}
*/
function charCodes(str) {
if (str.length === 0) return [];
if (str.length === 1) return [StringPrototypeCharCodeAt.$call(str, 0)];
const ret = new Array(str.length);
for (let i = 0; i < str.length; ++i) ret[i] = StringPrototypeCharCodeAt.$call(str, i);
return ret;
}
const defSepCodes = [38]; // &
const defEqCodes = [61]; // =
function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
if (key.length > 0 && keyEncoded) key = decodeStr(key, decode);
if (value.length > 0 && valEncoded) value = decodeStr(value, decode);
if (obj[key] === undefined) {
obj[key] = value;
} else {
const curValue = obj[key];
// A simple Array-specific property check is enough here to
// distinguish from a string value and is faster and still safe
// since we are generating all of the values being assigned.
if (curValue.pop) curValue[curValue.length] = value;
else obj[key] = [curValue, value];
}
}
/**
* Parse a key/val string.
* @param {string} qs
* @param {string} sep
* @param {string} eq
* @param {{
* maxKeys?: number;
* decodeURIComponent?(v: string): string;
* }} [options]
* @returns {Record<string, string | string[]>}
*/
function parse(qs, sep, eq, options) {
const obj = Object.create(null);
if (typeof qs !== "string" || qs.length === 0) {
return obj;
}
const sepCodes = !sep ? defSepCodes : charCodes(String(sep));
const eqCodes = !eq ? defEqCodes : charCodes(String(eq));
const sepLen = sepCodes.length;
const eqLen = eqCodes.length;
let pairs = 1000;
if (options && typeof options.maxKeys === "number") {
// -1 is used in place of a value like Infinity for meaning
// "unlimited pairs" because of additional checks V8 (at least as of v5.4)
// has to do when using variables that contain values like Infinity. Since
// `pairs` is always decremented and checked explicitly for 0, -1 works
// effectively the same as Infinity, while providing a significant
// performance boost.
pairs = options.maxKeys > 0 ? options.maxKeys : -1;
}
let decode = QueryString.unescape;
if (options && typeof options.decodeURIComponent === "function") {
decode = options.decodeURIComponent;
}
const customDecode = decode !== qsUnescape;
let lastPos = 0;
let sepIdx = 0;
let eqIdx = 0;
let key = "";
let value = "";
let keyEncoded = customDecode;
let valEncoded = customDecode;
const plusChar = customDecode ? "%20" : " ";
let encodeCheck = 0;
for (let i = 0; i < qs.length; ++i) {
const code = StringPrototypeCharCodeAt.$call(qs, i);
// Try matching key/value pair separator (e.g. '&')
if (code === sepCodes[sepIdx]) {
if (++sepIdx === sepLen) {
// Key/value pair separator match!
const end = i - sepIdx + 1;
if (eqIdx < eqLen) {
// We didn't find the (entire) key/value separator
if (lastPos < end) {
// Treat the substring as part of the key instead of the value
key += StringPrototypeSlice.$call(qs, lastPos, end);
} else if (key.length === 0) {
// We saw an empty substring between separators
if (--pairs === 0) return obj;
lastPos = i + 1;
sepIdx = eqIdx = 0;
continue;
}
} else if (lastPos < end) {
value += StringPrototypeSlice.$call(qs, lastPos, end);
}
addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
if (--pairs === 0) return obj;
keyEncoded = valEncoded = customDecode;
key = value = "";
encodeCheck = 0;
lastPos = i + 1;
sepIdx = eqIdx = 0;
}
} else {
sepIdx = 0;
// Try matching key/value separator (e.g. '=') if we haven't already
if (eqIdx < eqLen) {
if (code === eqCodes[eqIdx]) {
if (++eqIdx === eqLen) {
// Key/value separator match!
const end = i - eqIdx + 1;
if (lastPos < end) key += StringPrototypeSlice.$call(qs, lastPos, end);
encodeCheck = 0;
lastPos = i + 1;
}
continue;
} else {
eqIdx = 0;
if (!keyEncoded) {
// Try to match an (valid) encoded byte once to minimize unnecessary
// calls to string decoding functions
if (code === 37 /* % */) {
encodeCheck = 1;
continue;
} else if (encodeCheck > 0) {
if (isHexTable[code] === 1) {
if (++encodeCheck === 3) keyEncoded = true;
continue;
} else {
encodeCheck = 0;
}
}
}
}
if (code === 43 /* + */) {
if (lastPos < i) key += StringPrototypeSlice.$call(qs, lastPos, i);
key += plusChar;
lastPos = i + 1;
continue;
}
}
if (code === 43 /* + */) {
if (lastPos < i) value += StringPrototypeSlice.$call(qs, lastPos, i);
value += plusChar;
lastPos = i + 1;
} else if (!valEncoded) {
// Try to match an (valid) encoded byte (once) to minimize unnecessary
// calls to string decoding functions
if (code === 37 /* % */) {
encodeCheck = 1;
} else if (encodeCheck > 0) {
if (isHexTable[code] === 1) {
if (++encodeCheck === 3) valEncoded = true;
} else {
encodeCheck = 0;
}
}
}
}
}
// Deal with any leftover key or value data
if (lastPos < qs.length) {
if (eqIdx < eqLen) key += StringPrototypeSlice.$call(qs, lastPos);
else if (sepIdx < sepLen) value += StringPrototypeSlice.$call(qs, lastPos);
} else if (eqIdx === 0 && key.length === 0) {
// We ended on an empty substring
return obj;
}
addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);
return obj;
}
/**
* V8 does not optimize functions with try-catch blocks, so we isolate them here
* to minimize the damage (Note: no longer true as of V8 5.4 -- but still will
* not be inlined).
* @param {string} s
* @param {(v: string) => string} decoder
* @returns {string}
*/
function decodeStr(s, decoder) {
try {
return decoder(s);
} catch {
return QueryString.unescape(s, true);
}
}
});
export default require_src();