Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 153 additions & 112 deletions node/querystring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,114 @@
import { Buffer } from "./buffer.ts";
import { ERR_INVALID_URI } from "./_errors.ts";

export interface ParsedUrlQuery {
/******************** querystring.decode() ********************/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid this kind of comments are just confusing to other contributors because we don't have rule to group functions & variable by the position in the source code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
* Alias of querystring.parse()
* @deprecated
Copy link
Member

@kt3k kt3k Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

querystring APIs are tagged as Legacy but it seems that legacy is different from deprecated in Node.js documentation (ref: nodejs/node#37784 for example )

So maybe we should tag this as @legacy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 349b5f1

*/
export const decode = parse;

/******************** querystring.encode() ********************/

/**
* Alias of querystring.stringify()
* @deprecated
*/
export const encode = stringify;

/******************** querystring.escape(str) ********************/

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.
* @deprecated
* @see Tested in `test-querystring-escape.js`
*/
export const escape = qsEscape;

/******************** querystring.parse(str[, sep[, eq[, options]]]) ********************/

interface ParsedUrlQuery {
[key: string]: string | string[] | undefined;
}

Expand All @@ -13,10 +120,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);
Expand Down Expand Up @@ -56,32 +178,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`.
* @deprecated
* @see Tested in test-querystring.js
*/
export function parse(
str: string,
Expand Down Expand Up @@ -247,6 +353,8 @@ export function parse(
return obj;
}

/******************** querystring.stringify(obj[, sep[, eq[, options]]]) ********************/

interface StringifyOptions {
/** The function to use when converting URL-unsafe characters to percent-encoding in the query string. */
encodeURIComponent: (string: string) => string;
Expand All @@ -272,79 +380,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") {
Expand Down Expand Up @@ -395,6 +430,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()`.
* @deprecated
* @see Tested in `test-querystring.js`
*/
export function stringify(
// deno-lint-ignore no-explicit-any
Expand Down Expand Up @@ -444,6 +482,8 @@ export function stringify(
return "";
}

/******************** querystring.unescape(str) ********************/

// deno-fmt-ignore
const unhexTable = new Int8Array([
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
Expand All @@ -464,14 +504,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
*/
Expand Down Expand Up @@ -517,17 +549,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.
* @deprecated
* @see Tested in `test-querystring-escape.js`
*/
export const unescape = qsUnescape;
export const escape = qsEscape;

export default {
parse,
stringify,
hexTable,
decode,
encode,
unescape,
Expand Down
26 changes: 0 additions & 26 deletions node/querystring_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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: "こんにちは",
},
);
},
});