diff --git a/packages/app/src/TransformRequest/MultipartFormDataTransform.ts b/packages/app/src/TransformRequest/MultipartFormDataTransform.ts index 6e68587e..2267590b 100644 --- a/packages/app/src/TransformRequest/MultipartFormDataTransform.ts +++ b/packages/app/src/TransformRequest/MultipartFormDataTransform.ts @@ -17,10 +17,9 @@ export const MultipartFormDataTransform = async (request: IRequest) const rs = await request.createReadableStream(); const defer = new Defer(); - const result: Record = {}; const pending: Defer[] = []; - const {parseFieldName, inject} = QueryString; + const qs = new QueryString(); bb .on("file", (name, file, info) => { @@ -37,7 +36,7 @@ export const MultipartFormDataTransform = async (request: IRequest) tmpname, }; - inject(parseFieldName(name), value, result); + qs.push(name, value); const def = new Defer(); pending.push(def); @@ -48,7 +47,7 @@ export const MultipartFormDataTransform = async (request: IRequest) }) .on("field", (name, value) => { try { - inject(parseFieldName(name), JSON.parse(value), result); + qs.push(name, JSON.parse(value)); } catch { // skip } @@ -60,5 +59,5 @@ export const MultipartFormDataTransform = async (request: IRequest) await defer; await Promise.all(pending); - return result as any; + return qs.toObject(); }; diff --git a/packages/util/src/qs.ts b/packages/util/src/qs.ts index 12420f6b..14715bd2 100644 --- a/packages/util/src/qs.ts +++ b/packages/util/src/qs.ts @@ -1,15 +1,34 @@ +import {Rec} from "@bunt/type"; + const isNumeric = (key: string): boolean => !isNaN(+key); export class QueryString { - public static parseFieldName = (name: string): string[] => { + readonly #value: Rec; + + constructor(entries: [field: string, value: unknown][] = []) { + this.#value = Object.create(null); + for (const [field, value] of entries) { + this.push(field, value); + } + } + + public parseField(name: string): string[] { const base = name.replace(/\[.+/, ""); return [base, ...[...name.matchAll(/\[([^\]]*)\]/ig)].map(([, key]) => key)]; - }; + } + + public push(name: string, value: unknown): Rec { + return this.#inject(this.parseField(name), value, this.#value); + } + + public toObject(): Rec { + return this.#value; + } - public static inject = ([key, ...paths]: string[], value: unknown, fields: any = {}): any => { + #inject = ([key, ...paths]: string[], value: unknown, fields: Rec = Object.create(null)): Rec => { if (paths.length > 0) { - fields[key] = this.inject(paths, value, fields[key]); + fields[key] = this.#inject(paths, value, fields[key]); } else { fields[key] = value; } diff --git a/packages/util/test/src/qs/QueryStirng.test.ts b/packages/util/test/src/qs/QueryStirng.test.ts index 7e35f7e4..4fc660a9 100644 --- a/packages/util/test/src/qs/QueryStirng.test.ts +++ b/packages/util/test/src/qs/QueryStirng.test.ts @@ -1,34 +1,45 @@ import {QueryString} from "../../../src"; describe("QueryString", () => { + test("Prevent pollution", () => { + const injectTest = {}; + const injectKey = "__proto__[polluted]"; + + const qs = new QueryString(); + qs.push(injectKey, true); + + expect(Reflect.has(Reflect.get(injectTest, "__proto__"), "polluted")).toBeFalsy(); + }); + test("Base", () => { + const qs = new QueryString(); const field = "foo[bar][baz][0]"; - const parsed = QueryString.parseFieldName(field); + const parsed = qs.parseField(field); expect(parsed) .toEqual(["foo", "bar", "baz", "0"]); - expect(QueryString.inject(parsed, 1)) + expect(qs.push(field, 1)) .toEqual({foo: {bar: {baz: [1]}}}); }); test("Array", () => { + const qs = new QueryString(); const map: [string, any][] = [ ["foo[0]", 1], ["foo[1]", 2], ["foo[2]", 3], ]; - const result = {}; for (const [field, value] of map) { - const paths = QueryString.parseFieldName(field); - QueryString.inject(paths, value, result); + qs.push(field, value); } - expect(result).toEqual({foo: [1, 2, 3]}); + expect(qs.toObject()).toEqual({foo: [1, 2, 3]}); }); test("Nested array", () => { + const qs = new QueryString(); const map: [string, any][] = [ ["foo[0][a]", 1], ["foo[0][b]", 2], @@ -36,13 +47,11 @@ describe("QueryString", () => { ["foo[1][d]", 4], ]; - const result = {}; for (const [field, value] of map) { - const paths = QueryString.parseFieldName(field); - QueryString.inject(paths, value, result); + qs.push(field, value); } - expect(result).toEqual({foo: [{a: 1, b: 2}, {c: 3, d: 4}]}); + expect(qs.toObject()).toEqual({foo: [{a: 1, b: 2}, {c: 3, d: 4}]}); }); test.each([ @@ -50,8 +59,8 @@ describe("QueryString", () => { ["foo[bar]", 1, {foo: {bar: 1}}], ["foo[bar][0]", 1, {foo: {bar: [1]}}], ["foo[0][bar]", 1, {foo: [{bar: 1}]}], - ])("Variants", (field, value, res) => ( - expect(QueryString.inject(QueryString.parseFieldName(field), value)) - .toEqual(res) - )); + ])("Variants", (field, value, res) => { + const qs = new QueryString([[field, value]]); + expect(qs.toObject()).toEqual(res); + }); });