diff --git a/node/querystring.ts b/node/querystring.ts index b56ccc5d87bb..86cc4dedba49 100644 --- a/node/querystring.ts +++ b/node/querystring.ts @@ -2,7 +2,106 @@ import { Buffer } from "./buffer.ts"; import { ERR_INVALID_URI } from "./_errors.ts"; -export interface ParsedUrlQuery { +/** + * Alias of querystring.parse() + * @legacy + */ +export const decode = parse; + +/** + * Alias of querystring.stringify() + * @legacy + */ +export const encode = stringify; + +const hexTable = new Array(256); +for (let i = 0; i < 256; ++i) { + hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(); +} + +function encodeStr( + str: string, + noEscapeTable: Int8Array, + hexTable: string[], +): string { + const len = str.length; + if (len === 0) return ""; + + let out = ""; + let lastPos = 0; + + for (let i = 0; i < len; i++) { + let c = str.charCodeAt(i); + // ASCII + if (c < 0x80) { + if (noEscapeTable[c] === 1) continue; + if (lastPos < i) out += str.slice(lastPos, i); + lastPos = i + 1; + out += hexTable[c]; + continue; + } + + if (lastPos < i) out += str.slice(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 new ERR_INVALID_URI(); + + const c2 = str.charCodeAt(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 + str.slice(lastPos); + return out; +} + +/** + * replaces encodeURIComponent() + * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 + */ +function qsEscape(str: unknown): string { + if (typeof str !== "string") { + if (typeof str === "object") { + str = String(str); + } else { + str += ""; + } + } + return encodeStr(str as string, noEscape, hexTable); +} + +/** + * Performs URL percent-encoding on the given `str` in a manner that is optimized for the specific requirements of URL query strings. + * Used by `querystring.stringify()` and is generally not expected to be used directly. + * It is exported primarily to allow application code to provide a replacement percent-encoding implementation if necessary by assigning `querystring.escape` to an alternative function. + * @legacy + * @see Tested in `test-querystring-escape.js` + */ +export const escape = qsEscape; + +interface ParsedUrlQuery { [key: string]: string | string[] | undefined; } @@ -13,10 +112,25 @@ interface ParseOptions { maxKeys?: number; } -export const hexTable = new Array(256); -for (let i = 0; i < 256; ++i) { - hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(); -} +// deno-fmt-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 +]); function charCodes(str: string): number[] { const ret = new Array(str.length); @@ -56,32 +170,16 @@ function addKeyVal( } } -// deno-fmt-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 -]); - /** * Parses a URL query string into a collection of key and value pairs. * @param str The URL query string to parse * @param sep The substring used to delimit key and value pairs in the query string. Default: '&'. * @param eq The substring used to delimit keys and values in the query string. Default: '='. * @param options The parse options + * @param options.decodeURIComponent The function to use when decoding percent-encoded characters in the query string. Default: `querystring.unescape()`. + * @param options.maxKeys Specifies the maximum number of keys to parse. Specify `0` to remove key counting limitations. Default: `1000`. + * @legacy + * @see Tested in test-querystring.js */ export function parse( str: string, @@ -272,79 +370,6 @@ const noEscape = new Int8Array([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 ]); -/** - * replaces encodeURIComponent() - * @see https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4 - */ -function qsEscape(str: unknown): string { - if (typeof str !== "string") { - if (typeof str === "object") { - str = String(str); - } else { - str += ""; - } - } - return encodeStr(str as string, noEscape, hexTable); -} - -function encodeStr( - str: string, - noEscapeTable: Int8Array, - hexTable: string[], -): string { - const len = str.length; - if (len === 0) return ""; - - let out = ""; - let lastPos = 0; - - for (let i = 0; i < len; i++) { - let c = str.charCodeAt(i); - // ASCII - if (c < 0x80) { - if (noEscapeTable[c] === 1) continue; - if (lastPos < i) out += str.slice(lastPos, i); - lastPos = i + 1; - out += hexTable[c]; - continue; - } - - if (lastPos < i) out += str.slice(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 new ERR_INVALID_URI(); - - const c2 = str.charCodeAt(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 + str.slice(lastPos); - return out; -} - // deno-lint-ignore no-explicit-any function stringifyPrimitive(v: any): string { if (typeof v === "string") { @@ -395,6 +420,9 @@ function encodeStringified(v: any, encode: (string: string) => string): string { * @param sep The substring used to delimit key and value pairs in the query string. Default: '&'. * @param eq The substring used to delimit keys and values in the query string. Default: '='. * @param options The stringify options + * @param options.encodeURIComponent The function to use when converting URL-unsafe characters to percent-encoding in the query string. Default: `querystring.escape()`. + * @legacy + * @see Tested in `test-querystring.js` */ export function stringify( // deno-lint-ignore no-explicit-any @@ -464,14 +492,6 @@ const unhexTable = new Int8Array([ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255 ]); -function qsUnescape(s: string): string { - try { - return decodeURIComponent(s); - } catch { - return unescapeBuffer(s).toString(); - } -} - /** * A safe fast alternative to decodeURIComponent */ @@ -517,17 +537,26 @@ export function unescapeBuffer(s: string, decodeSpaces = false): Buffer { return hasHex ? out.slice(0, outIndex) : out; } -/** Alias of querystring.parse() */ -export const decode = parse; -/** Alias of querystring.stringify() */ -export const encode = stringify; +function qsUnescape(s: string): string { + try { + return decodeURIComponent(s); + } catch { + return unescapeBuffer(s).toString(); + } +} + +/** + * Performs decoding of URL percent-encoded characters on the given `str`. + * Used by `querystring.parse()` and is generally not expected to be used directly. + * It is exported primarily to allow application code to provide a replacement decoding implementation if necessary by assigning `querystring.unescape` to an alternative function. + * @legacy + * @see Tested in `test-querystring-escape.js` + */ export const unescape = qsUnescape; -export const escape = qsEscape; export default { parse, stringify, - hexTable, decode, encode, unescape, diff --git a/node/querystring_test.ts b/node/querystring_test.ts index 70b102b6de9f..d05a75eabd55 100644 --- a/node/querystring_test.ts +++ b/node/querystring_test.ts @@ -17,19 +17,6 @@ Deno.test({ }, }); -Deno.test({ - name: "stringify with escape", - fn() { - assertEquals( - stringify({ - a: "hello!", - b: "こんにちは", - }), - "a=hello%EF%BC%81&b=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF", - ); - }, -}); - Deno.test({ name: "parse", fn() { @@ -41,16 +28,3 @@ Deno.test({ }); }, }); - -Deno.test({ - name: "parse escaped string", - fn() { - assertEquals( - parse("a=hello%EF%BC%81&b=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF"), - { - a: "hello!", - b: "こんにちは", - }, - ); - }, -});