From 8a2fbeec3545713555f1e6e0e8ce225ce01b0982 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 01:26:35 -0500 Subject: [PATCH] Give querystring.parse results a null prototype querystring.parse and querystring.decode build their result with a null prototype, as Node does: util.inspect prints the [Object: null prototype] base, Object.getPrototypeOf answers null, ToPrimitive throws a TypeError, and Object.prototype methods are not reachable through the result. --- .../src/backend/llvm/lib-filesystem.ts | 2 ++ .../5842-qs-parse-null-prototype.ts.json | 6 ++++ .../5843-qs-parse-null-prototype-js.mjs.json | 6 ++++ tests/corpus/5842-qs-parse-null-prototype.ts | 34 +++++++++++++++++++ .../5843-qs-parse-null-prototype-js.mjs | 12 +++++++ tests/harness/corpus-costs.json | 4 +++ 6 files changed, 64 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json create mode 100644 tests/corpus/5842-qs-parse-null-prototype.ts create mode 100644 tests/corpus/5843-qs-parse-null-prototype-js.mjs diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 1083c69d5..b1d8869fd 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -746,6 +746,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const dict = B.tmp(); B.line(`${dict} = call ptr @${mangleRecordNew(e.type.shapeId)}()`); const out = host.own({ name: dict, type: e.type }); + // Node builds the result with a null prototype. + B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(dict, e.type.shapeId)}`); const ovf = host.recordOvfPtr(dict, e.type.shapeId); B.line( `call void @scr_qs_parse_into(ptr ${ovf}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, double ${args[3]!.name}, i32 ${strTag}, i32 ${arrTag})`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json new file mode 100644 index 000000000..a5b4a3e21 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5842-qs-parse-null-prototype.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5842-qs-parse-null-prototype.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json new file mode 100644 index 000000000..228f0a4c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5843-qs-parse-null-prototype-js.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5843-qs-parse-null-prototype-js.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/5842-qs-parse-null-prototype.ts b/tests/corpus/5842-qs-parse-null-prototype.ts new file mode 100644 index 000000000..02d5cc134 --- /dev/null +++ b/tests/corpus/5842-qs-parse-null-prototype.ts @@ -0,0 +1,34 @@ +// querystring.parse and querystring.decode return a null-prototype +// object: inspection prints the "[Object: null prototype]" base, +// getPrototypeOf answers null, and ToPrimitive and Object.prototype +// methods throw TypeErrors. Spread copies keep Object.prototype. +import * as qs from "node:querystring"; +import { inspect } from "node:util"; + +const parsed = qs.parse("a=1&b=2&b=3&c"); +console.log(parsed); +console.log(inspect(parsed), inspect({ nested: parsed }), inspect([qs.parse("")])); +console.log(qs.decode("x=1;y=2", ";")); +console.log(Object.getPrototypeOf(parsed) === null, Object.keys(parsed), JSON.stringify(parsed)); + +const attempt = (label: string, f: () => unknown): void => { + try { + console.log(label, f()); + } catch (e) { + console.log(label, (e as Error).name, (e as Error).message); + } +}; +attempt("String", () => String(parsed)); +attempt("template", () => `${parsed}`); +attempt("concat", () => "q=" + parsed); +attempt("hasOwnProperty", () => parsed.hasOwnProperty("a")); +attempt("toString", () => parsed.toString()); +console.log(Object.prototype.toString.call(parsed), Object.hasOwn(parsed, "b")); +console.log(Object.prototype.hasOwnProperty.call(parsed, "c")); + +const copy = { ...parsed }; +console.log(copy, Object.getPrototypeOf(copy) === Object.prototype, String(copy)); + +parsed.added = "yes"; +delete parsed.a; +console.log(parsed, parsed.b, parsed.added); diff --git a/tests/corpus/5843-qs-parse-null-prototype-js.mjs b/tests/corpus/5843-qs-parse-null-prototype-js.mjs new file mode 100644 index 000000000..676d188c4 --- /dev/null +++ b/tests/corpus/5843-qs-parse-null-prototype-js.mjs @@ -0,0 +1,12 @@ +// querystring.parse results keep their null prototype when they cross into +// checked-dynamic values in a JavaScript module: inspection, getPrototypeOf, +// ToPrimitive and the `in` operator. +import qs from "node:querystring"; +const parsed = qs.parse("a=1&b=2&b=3"); +const holder = { parsed, list: [parsed] }; +console.log(holder); +const dyn = JSON.parse("{}"); +dyn.p = parsed; +console.log(dyn, Object.getPrototypeOf(dyn.p) === null); +try { console.log(String(dyn.p)); } catch (e) { console.log(e.name, e.message); } +console.log("toString" in parsed, "a" in parsed); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a0be8c095..221659356 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1849,6 +1849,8 @@ "5836-url-search-params-tuple-pairs.ts": 2118, "5837-util-format-json-circular.ts": 3117, "5838-qs-stringify-bigint.ts": 2207, + "5842-qs-parse-null-prototype.ts": 1774, + "5843-qs-parse-null-prototype-js.mjs": 1559, "5858-disposable-stack.ts": 3050, "5859-disposable-stack-js.js": 3100, "5881-inspect-weak-collections.ts": 2615, @@ -4506,6 +4508,8 @@ "5836-url-search-params-tuple-pairs.ts": 3193, "5837-util-format-json-circular.ts": 3408, "5838-qs-stringify-bigint.ts": 3519, + "5842-qs-parse-null-prototype.ts": 7077, + "5843-qs-parse-null-prototype-js.mjs": 5395, "5858-disposable-stack.ts": 3750, "5859-disposable-stack-js.js": 3600, "5881-inspect-weak-collections.ts": 2915,