From 6f3f37e4c852d71dc8c3cf4939d877a1149d6da3 Mon Sep 17 00:00:00 2001 From: Mohamed Akram Date: Mon, 3 Oct 2022 23:23:16 +0400 Subject: [PATCH 01/10] Improve types --- lib/binary_parser.ts | 485 +++++++++++++++++++++++++++++++-------- package-lock.json | 13 ++ package.json | 1 + test/composite_parser.ts | 26 +-- test/types.ts | 308 +++++++++++++++++++++++++ 5 files changed, 720 insertions(+), 113 deletions(-) create mode 100644 test/types.ts diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index f8a9229f..d2da2fd5 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -107,29 +107,34 @@ class Context { } } -const aliasRegistry = new Map(); +const aliasRegistry = new Map>(); const FUNCTION_PREFIX = "___parser_"; -interface ParserOptions { - length?: number | string | ((item: any) => number); - assert?: number | string | ((item: number | string) => boolean); - lengthInBytes?: number | string | ((item: any) => number); - type?: string | Parser; - formatter?: (item: any) => any; +interface ParserOptions> { + length?: number | string | ((this: O, item: any) => number); + assert?: number | string | ((this: O, item: T) => boolean); + lengthInBytes?: number | string | ((this: O, item: any) => number); + type?: string | Parser; + formatter?: (this: O, item: T) => F; encoding?: string; - readUntil?: "eof" | ((item: any, buffer: Buffer) => boolean); + readUntil?: "eof" | ((this: O, item: number, buffer: Buffer) => boolean); greedy?: boolean; - choices?: { [key: number]: string | Parser }; - defaultChoice?: string | Parser; + choices?: Record; + defaultChoice?: string | Parser; zeroTerminated?: boolean; clone?: boolean; stripNull?: boolean; key?: string; - tag?: string | ((item: any) => number); - offset?: number | string | ((item: any) => number); + tag?: string | ((this: O, item: any) => number); + offset?: number | string | ((this: O, item: any) => number); wrapper?: (buffer: Buffer) => Buffer; } +interface ArrayParserOptions + extends Omit, "type"> { + type?: string | Parser; +} + type Types = PrimitiveTypes | ComplexTypes; type ComplexTypes = @@ -147,7 +152,7 @@ type ComplexTypes = type Endianness = "be" | "le"; -type PrimitiveTypes = +export type PrimitiveTypes = | "uint8" | "uint16le" | "uint16be" @@ -274,7 +279,17 @@ const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveTypes]: boolean } = { doublebe: false, }; -export class Parser { +type Next = N extends string + ? Parser + : Parser; + +type ChoiceType

= P extends Parser + ? O + : P extends PrimitiveTypes + ? number + : any; + +export class Parser { varName = ""; type: Types = ""; options: ParserOptions = {}; @@ -303,11 +318,11 @@ export class Parser { ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type]};`); } - private primitiveN( + private primitiveN( type: PrimitiveTypes, - varName: string, - options: ParserOptions - ): this { + varName: N | undefined, + options: ParserOptions + ): Next { return this.setNextParser(type as Types, varName, options); } @@ -315,59 +330,131 @@ export class Parser { return (type + this.endian.toLowerCase()) as PrimitiveTypes; } - uint8(varName: string, options: ParserOptions = {}): this { + uint8(): Next; + uint8( + varName: N, + options?: ParserOptions + ): Next; + uint8( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("uint8", varName, options); } - uint16(varName: string, options: ParserOptions = {}): this { + uint16(): Next; + uint16( + varName: N, + options?: ParserOptions + ): Next; + uint16( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN(this.useThisEndian("uint16"), varName, options); } - uint16le(varName: string, options: ParserOptions = {}): this { + uint16le( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("uint16le", varName, options); } - uint16be(varName: string, options: ParserOptions = {}): this { + uint16be( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("uint16be", varName, options); } - uint32(varName: string, options: ParserOptions = {}): this { + uint32(): Next; + uint32( + varName: N, + options?: ParserOptions + ): Next; + uint32( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN(this.useThisEndian("uint32"), varName, options); } - uint32le(varName: string, options: ParserOptions = {}): this { + uint32le( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("uint32le", varName, options); } - uint32be(varName: string, options: ParserOptions = {}): this { + uint32be( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("uint32be", varName, options); } - int8(varName: string, options: ParserOptions = {}): this { + int8(): Next; + int8( + varName: N, + options?: ParserOptions + ): Next; + int8( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("int8", varName, options); } - int16(varName: string, options: ParserOptions = {}): this { + int16(): Next; + int16( + varName: N, + options?: ParserOptions + ): Next; + int16( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN(this.useThisEndian("int16"), varName, options); } - int16le(varName: string, options: ParserOptions = {}): this { + int16le( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("int16le", varName, options); } - int16be(varName: string, options: ParserOptions = {}): this { + int16be( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("int16be", varName, options); } - int32(varName: string, options: ParserOptions = {}): this { + int32(): Next; + int32( + varName: N, + options?: ParserOptions + ): Next; + int32( + varName?: N, + options: ParserOptions = {} + ): Next { return this.primitiveN(this.useThisEndian("int32"), varName, options); } - int32le(varName: string, options: ParserOptions = {}): this { + int32le( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("int32le", varName, options); } - int32be(varName: string, options: ParserOptions = {}): this { + int32be( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("int32be", varName, options); } @@ -376,182 +463,322 @@ export class Parser { throw new Error("BigInt64 is unsupported on this runtime"); } - int64(varName: string, options: ParserOptions = {}): this { + int64(): Next; + int64( + varName: N, + options?: ParserOptions + ): Next; + int64( + varName?: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("int64"), varName, options); } - int64be(varName: string, options: ParserOptions = {}): this { + int64be( + varName: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN("int64be", varName, options); } - int64le(varName: string, options: ParserOptions = {}): this { + int64le( + varName: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN("int64le", varName, options); } - uint64(varName: string, options: ParserOptions = {}): this { + uint64(): Next; + uint64( + varName: N, + options?: ParserOptions + ): Next; + uint64( + varName?: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("uint64"), varName, options); } - uint64be(varName: string, options: ParserOptions = {}): this { + uint64be( + varName: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN("uint64be", varName, options); } - uint64le(varName: string, options: ParserOptions = {}): this { + uint64le( + varName: N, + options: ParserOptions = {} + ): Next { this.bigIntVersionCheck(); return this.primitiveN("uint64le", varName, options); } - floatle(varName: string, options: ParserOptions = {}): this { + floatle( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("floatle", varName, options); } - floatbe(varName: string, options: ParserOptions = {}): this { + floatbe( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("floatbe", varName, options); } - doublele(varName: string, options: ParserOptions = {}): this { + doublele( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("doublele", varName, options); } - doublebe(varName: string, options: ParserOptions = {}): this { + doublebe( + varName: N, + options: ParserOptions = {} + ): Next { return this.primitiveN("doublebe", varName, options); } - private bitN(size: BitSizes, varName: string, options: ParserOptions): this { + private bitN( + size: BitSizes, + varName: N, + options: ParserOptions + ): Next { options.length = size; return this.setNextParser("bit", varName, options); } - bit1(varName: string, options: ParserOptions = {}): this { + bit1( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(1, varName, options); } - bit2(varName: string, options: ParserOptions = {}): this { + bit2( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(2, varName, options); } - bit3(varName: string, options: ParserOptions = {}): this { + bit3( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(3, varName, options); } - bit4(varName: string, options: ParserOptions = {}): this { + bit4( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(4, varName, options); } - bit5(varName: string, options: ParserOptions = {}): this { + bit5( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(5, varName, options); } - bit6(varName: string, options: ParserOptions = {}): this { + bit6( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(6, varName, options); } - bit7(varName: string, options: ParserOptions = {}): this { + bit7( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(7, varName, options); } - bit8(varName: string, options: ParserOptions = {}): this { + bit8( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(8, varName, options); } - bit9(varName: string, options: ParserOptions = {}): this { + bit9( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(9, varName, options); } - bit10(varName: string, options: ParserOptions = {}): this { + bit10( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(10, varName, options); } - bit11(varName: string, options: ParserOptions = {}): this { + bit11( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(11, varName, options); } - bit12(varName: string, options: ParserOptions = {}): this { + bit12( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(12, varName, options); } - bit13(varName: string, options: ParserOptions = {}): this { + bit13( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(13, varName, options); } - bit14(varName: string, options: ParserOptions = {}): this { + bit14( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(14, varName, options); } - bit15(varName: string, options: ParserOptions = {}): this { + bit15( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(15, varName, options); } - bit16(varName: string, options: ParserOptions = {}): this { + bit16( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(16, varName, options); } - bit17(varName: string, options: ParserOptions = {}): this { + bit17( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(17, varName, options); } - bit18(varName: string, options: ParserOptions = {}): this { + bit18( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(18, varName, options); } - bit19(varName: string, options: ParserOptions = {}): this { + bit19( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(19, varName, options); } - bit20(varName: string, options: ParserOptions = {}): this { + bit20( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(20, varName, options); } - bit21(varName: string, options: ParserOptions = {}): this { + bit21( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(21, varName, options); } - bit22(varName: string, options: ParserOptions = {}): this { + bit22( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(22, varName, options); } - bit23(varName: string, options: ParserOptions = {}): this { + bit23( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(23, varName, options); } - bit24(varName: string, options: ParserOptions = {}): this { + bit24( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(24, varName, options); } - bit25(varName: string, options: ParserOptions = {}): this { + bit25( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(25, varName, options); } - bit26(varName: string, options: ParserOptions = {}): this { + bit26( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(26, varName, options); } - bit27(varName: string, options: ParserOptions = {}): this { + bit27( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(27, varName, options); } - bit28(varName: string, options: ParserOptions = {}): this { + bit28( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(28, varName, options); } - bit29(varName: string, options: ParserOptions = {}): this { + bit29( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(29, varName, options); } - bit30(varName: string, options: ParserOptions = {}): this { + bit30( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(30, varName, options); } - bit31(varName: string, options: ParserOptions = {}): this { + bit31( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(31, varName, options); } - bit32(varName: string, options: ParserOptions = {}): this { + bit32( + varName: N, + options: ParserOptions = {} + ): Next { return this.bitN(32, varName, options); } @@ -570,10 +797,15 @@ export class Parser { throw new Error("assert option on seek is not allowed."); } - return this.setNextParser("seek", "", { length: relOffset }); + return this.setNextParser("seek", "", { + length: relOffset, + } as ParserOptions) as unknown as this; } - string(varName: string, options: ParserOptions): this { + string( + varName: N, + options: ParserOptions + ): Next { if (!options.zeroTerminated && !options.length && !options.greedy) { throw new Error( "One of length, zeroTerminated, or greedy must be defined for string." @@ -597,7 +829,10 @@ export class Parser { return this.setNextParser("string", varName, options); } - buffer(varName: string, options: ParserOptions): this { + buffer( + varName: N, + options: ParserOptions + ): Next { if (!options.length && !options.readUntil) { throw new Error("length or readUntil must be defined for buffer."); } @@ -605,7 +840,15 @@ export class Parser { return this.setNextParser("buffer", varName, options); } - wrapped(varName: string | ParserOptions, options?: ParserOptions): this { + wrapped(options: ParserOptions): Next; + wrapped( + varName: N, + options: ParserOptions + ): Next; + wrapped( + varName: N | ParserOptions, + options?: ParserOptions + ): Next { if (typeof options !== "object" && typeof varName === "object") { options = varName; varName = ""; @@ -619,10 +862,23 @@ export class Parser { throw new Error("length or readUntil must be defined for wrapped."); } - return this.setNextParser("wrapper", varName as string, options); + return this.setNextParser("wrapper", varName as N, options); } - array(varName: string, options: ParserOptions): this { + array( + varName: N, + options: Omit, "type"> & { + type: PrimitiveTypes; + } + ): Next; + array( + varName: N, + options: ArrayParserOptions + ): Next; + array( + varName: N, + options: ArrayParserOptions + ): Next { if (!options.readUntil && !options.length && !options.lengthInBytes) { throw new Error( "One of readUntil, length and lengthInBytes must be defined for array." @@ -641,10 +897,29 @@ export class Parser { throw new Error(`Array element type "${options.type}" is unkown.`); } - return this.setNextParser("array", varName, options); + return this.setNextParser( + "array", + varName, + options as ParserOptions + ); } - choice(varName: string | ParserOptions, options?: ParserOptions): this { + choice>, F = ChoiceType>( + options: ParserOptions, F, C> + ): Next; + choice< + N extends string, + C extends string | Parser>, + F = ChoiceType + >(varName: N, options: ParserOptions, F, C>): Next; + choice< + N extends string, + C extends string | Parser>, + F = ChoiceType + >( + varName: N | ParserOptions, F, C>, + options?: ParserOptions, F, C> + ): Next { if (typeof options !== "object" && typeof varName === "object") { options = varName; varName = ""; @@ -679,10 +954,18 @@ export class Parser { } } - return this.setNextParser("choice", varName as string, options); + return this.setNextParser("choice", varName as N, options); } - nest(varName: string | ParserOptions, options?: ParserOptions): this { + nest(options: ParserOptions): Next; + nest( + varName: N, + options: ParserOptions + ): Next; + nest( + varName: N | ParserOptions, + options?: ParserOptions + ): Next { if (typeof options !== "object" && typeof varName === "object") { options = varName; varName = ""; @@ -702,10 +985,13 @@ export class Parser { ); } - return this.setNextParser("nest", varName as string, options); + return this.setNextParser("nest", varName as N, options); } - pointer(varName: string, options: ParserOptions): this { + pointer( + varName: N, + options: ParserOptions + ): Next { if (!options.offset) { throw new Error("offset is required for pointer."); } @@ -725,7 +1011,10 @@ export class Parser { return this.setNextParser("pointer", varName, options); } - saveOffset(varName: string, options: ParserOptions = {}): this { + saveOffset( + varName: N, + options: ParserOptions = {} + ): Next { return this.setNextParser("saveOffset", varName, options); } @@ -898,7 +1187,7 @@ export class Parser { } // Follow the parser chain till the root and start parsing from there - parse(buffer: Buffer | Uint8Array) { + parse(buffer: Buffer | Uint8Array): O { if (!this.compiled) { this.compile(); } @@ -906,16 +1195,16 @@ export class Parser { return this.compiled!(buffer, this.constructorFn); } - private setNextParser( + private setNextParser( type: Types, - varName: string, - options: ParserOptions - ): this { + varName: N | undefined, + options: ParserOptions + ): Next { const parser = new Parser(); parser.type = type; - parser.varName = varName; - parser.options = options; + parser.varName = varName as string; + parser.options = options as ParserOptions; parser.endian = this.endian; if (this.head) { @@ -925,7 +1214,7 @@ export class Parser { } this.head = parser; - return this; + return this as unknown as Next; } // Call code generator for this parser @@ -1323,10 +1612,10 @@ export class Parser { } } - private generateChoiceCase( + private generateChoiceCase( ctx: Context, varName: string, - type: string | Parser + type: string | Parser ) { if (typeof type === "string") { const varName = ctx.generateVariable(this.varName); diff --git a/package-lock.json b/package-lock.json index bf35ee30..96bd5481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@types/mocha": "^9.1.1", "@types/node": "^18.7.18", + "expect-type": "^0.14.2", "karma": "^6.4.1", "karma-chrome-launcher": "^3.1.1", "karma-mocha": "^2.0.1", @@ -1645,6 +1646,12 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect-type": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-0.14.2.tgz", + "integrity": "sha512-ed3+tr5ujbIYXZ8Pl/VgIphwJQ0q5tBLGGdn7Zvwt1WyPBRX83xjT5pT77P/GkuQbctx0K2ZNSSan7eruJqTCQ==", + "dev": true + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5482,6 +5489,12 @@ "safe-buffer": "^5.1.1" } }, + "expect-type": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-0.14.2.tgz", + "integrity": "sha512-ed3+tr5ujbIYXZ8Pl/VgIphwJQ0q5tBLGGdn7Zvwt1WyPBRX83xjT5pT77P/GkuQbctx0K2ZNSSan7eruJqTCQ==", + "dev": true + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", diff --git a/package.json b/package.json index fa9c5074..45804aa9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "devDependencies": { "@types/mocha": "^9.1.1", "@types/node": "^18.7.18", + "expect-type": "^0.14.2", "karma": "^6.4.1", "karma-chrome-launcher": "^3.1.1", "karma-mocha": "^2.0.1", diff --git a/test/composite_parser.ts b/test/composite_parser.ts index 333c7684..d047b55b 100644 --- a/test/composite_parser.ts +++ b/test/composite_parser.ts @@ -154,7 +154,7 @@ function compositeParserTests( const parser = Parser.start() .uint16le("length") .array("message", { - lengthInBytes: function (this: any) { + lengthInBytes: function () { return this.length; }, type: elementParser, @@ -229,7 +229,7 @@ function compositeParserTests( }); it("should parse until function returns true when readUntil is function", () => { const parser = Parser.start().array("data", { - readUntil: (item: number, _: Buffer) => item === 0, + readUntil: (item) => item === 0, type: "uint8", }); @@ -242,7 +242,7 @@ function compositeParserTests( }); it("should parse until function returns true when readUntil is function (using read-ahead)", () => { const parser = Parser.start().array("data", { - readUntil: (_: number, buf: Buffer) => buf.length > 0 && buf[0] === 0, + readUntil: (_, buf) => buf.length > 0 && buf[0] === 0, type: "uint8", }); @@ -290,7 +290,7 @@ function compositeParserTests( const parser = Parser.start().array("data", { type: "uint8", length: 4, - formatter: (arr: number[]) => arr.join("."), + formatter: (arr) => arr.join("."), }); const buffer = factory([0x0a, 0x0a, 0x01, 0x6e]); @@ -408,7 +408,7 @@ function compositeParserTests( it("should be able to access to index context variable when using length", () => { const elementParser = new Parser() .uint8("key", { - formatter: function (this: any, item: number) { + formatter: function (this: any, item) { return this.$index % 2 === 0 ? item : String.fromCharCode(item); }, }) @@ -444,7 +444,7 @@ function compositeParserTests( // @ts-ignore const elementParser = new Parser() .uint8("key", { - formatter: function (this: any, item: number) { + formatter: function (this: any, item) { return this.$index % 2 === 0 ? item : String.fromCharCode(item); }, }) @@ -808,7 +808,7 @@ function compositeParserTests( const parser = Parser.start() .string("selector", { length: 4 }) .choice({ - tag: function (this: { selector: string }) { + tag: function () { return parseInt(this.selector, 2); // string base 2 to integer decimal }, choices: { @@ -921,8 +921,7 @@ function compositeParserTests( }); const personParser = new Parser().nest("name", { type: nameParser, - formatter: (name: { firstName: string; lastName: string }) => - name.firstName + " " + name.lastName, + formatter: (name) => name.firstName + " " + name.lastName, }); const buffer = factory(new TextEncoder().encode("John\0Doe\0")); @@ -1139,10 +1138,7 @@ function compositeParserTests( .int16le("a") .int16le("b") .int16le("c", { - assert: function ( - this: { a: number; b: number }, - x: number | string - ) { + assert: function (x) { return this.a + this.b === x; }, }); @@ -1161,7 +1157,7 @@ function compositeParserTests( .int16le("a") .int16le("b") .int16le("c", { - assert(this: { a: number; b: number }, x: number | string) { + assert(x) { return this.a + this.b === x; }, }); @@ -1259,7 +1255,7 @@ function compositeParserTests( .uint32le("length") .wrapped("compressedData", { length: "length", - wrapper: (x: Uint8Array) => inflateRawSync(x), + wrapper: (x) => inflateRawSync(x), type: bufferParser, }) .uint8("answer"); diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 00000000..16722b82 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,308 @@ +import { expectTypeOf } from "expect-type"; + +import { Parser as Parser_, PrimitiveTypes } from "../lib/binary_parser"; + +type Endianness = "be" | "le"; +type N = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; +type BitSizes = Exclude | `1${N}` | `2${N}` | `3${0 | 1 | 2}`; +type Int = + | `int${T}` + | `uint${T}` + | `int${T}${Endianness}` + | `uint${T}${Endianness}`; +type NumericType = + | "int8" + | "uint8" + | Int<16> + | Int<32> + | Int<64> + | `float${Endianness}` + | `double${Endianness}` + | `bit${BitSizes}`; + +const buf = Buffer.from([]); + +// We only care about the types, don't actually parse +class Parser extends Parser_ { + static start() { + return new Parser(); + } + parse() { + return {}; + } + // @ts-expect-error + private setNextParser() { + return this; + } +} + +describe("Parser", () => { + it("should have correct numeric types", () => { + const numericType = "uint8" as NumericType; + expectTypeOf( + Parser.start() + [numericType]("x", { + assert: (x) => { + expectTypeOf(x).toEqualTypeOf(); + return x == 1; + }, + formatter: (x) => { + expectTypeOf(x).toEqualTypeOf(); + return x.toString(); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ + x: string; + }>(); + }); + it("should have correct string types", () => { + expectTypeOf( + Parser.start().string("s", { length: 1 }).parse(buf) + ).toEqualTypeOf<{ s: string }>(); + expectTypeOf( + Parser.start() + .string("s", { + length: 1, + assert: (s) => { + expectTypeOf(s).toEqualTypeOf(); + return s.length == 1; + }, + formatter: (s) => { + expectTypeOf(s).toEqualTypeOf(); + return Number(s); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ s: number }>(); + }); + it("should have correct buffer types", () => { + expectTypeOf( + Parser.start().buffer("b", { length: 1 }).parse(buf) + ).toEqualTypeOf<{ b: Buffer }>(); + expectTypeOf( + Parser.start() + .buffer("b", { + length: 1, + assert: (b) => { + expectTypeOf(b).toEqualTypeOf(); + return b.length == 1; + }, + formatter: (b) => { + expectTypeOf(b).toEqualTypeOf(); + return Number(b[0]); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ b: number }>(); + }); + it("should have correct wrapped types", () => { + expectTypeOf( + Parser.start() + .wrapped("w", { + type: Parser.start().string("s", { length: 1 }), + wrapper: (b) => b, + length: 1, + }) + .parse(buf) + ).toEqualTypeOf<{ w: { s: string } }>(); + expectTypeOf( + Parser.start() + .wrapped({ + type: Parser.start().string("s", { length: 1 }), + wrapper: (b) => b, + length: 1, + }) + .parse(buf) + ).toEqualTypeOf<{ s: string }>(); + expectTypeOf( + Parser.start() + .wrapped("w", { + type: Parser.start().string("s", { length: 1 }), + length: 1, + wrapper(b) { + expectTypeOf(b).toEqualTypeOf(); + return b; + }, + assert: (w) => { + expectTypeOf(w).toEqualTypeOf<{ s: string }>(); + return w.s.length == 1; + }, + formatter: (w) => { + expectTypeOf(w).toEqualTypeOf<{ s: string }>(); + return Number(w.s); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ w: number }>(); + }); + it("should have correct array types", () => { + const arr = Parser.start() + .array("a", { + type: "uint8" as PrimitiveTypes, + length: 1, + }) + .parse(buf); + expectTypeOf(arr).toEqualTypeOf<{ a: number[] }>(); + expectTypeOf(arr.a?.[0]).not.toBeAny(); + + const arr2 = Parser.start() + .array("a", { + type: Parser.start().string("s", { length: 10 }), + length: 1, + }) + .parse(buf); + expectTypeOf(arr2).toEqualTypeOf<{ a: { s: string }[] }>(); + expectTypeOf(arr2.a?.[0]).not.toBeAny(); + + expectTypeOf( + Parser.start() + .array("a", { + type: Parser.start().string("s", { length: 1 }), + length: 1, + assert(a) { + expectTypeOf(a).toEqualTypeOf<{ s: string }[]>(); + return a[0].s.length == 1; + }, + formatter(a) { + expectTypeOf(a).toEqualTypeOf<{ s: string }[]>(); + return Number(a[0].s); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ a: number }>(); + }); + it("should have correct choice types", () => { + expectTypeOf( + Parser.start() + .uint8("tag") + .choice("val", { + tag: "tag", + choices: { + 1: Parser.start().uint8("num"), + 2: Parser.start().string("str", { zeroTerminated: true }), + }, + }) + .parse(buf) + ).toEqualTypeOf<{ tag: number; val: { num: number } | { str: string } }>(); + expectTypeOf( + Parser.start() + .uint8("tag") + .choice({ + tag: "tag", + choices: { + 1: Parser.start().uint8("num"), + 2: Parser.start().string("str", { zeroTerminated: true }), + }, + }) + .parse(buf) + ).toEqualTypeOf<{ tag: number } & ({ num: number } | { str: string })>(); + }); + it("should have correct nest types", () => { + expectTypeOf( + Parser.start() + .nest("n", { + type: Parser.start().string("s", { length: 1 }), + }) + .parse(buf) + ).toEqualTypeOf<{ n: { s: string } }>(); + expectTypeOf( + Parser.start() + .nest({ + type: Parser.start().string("s", { length: 1 }), + length: 1, + }) + .parse(buf) + ).toEqualTypeOf<{ s: string }>(); + expectTypeOf( + Parser.start() + .nest("n", { + type: Parser.start().string("s", { length: 1 }), + length: 1, + assert: (n) => { + expectTypeOf(n).toEqualTypeOf<{ s: string }>(); + return n.s.length == 1; + }, + formatter: (n) => { + expectTypeOf(n).toEqualTypeOf<{ s: string }>(); + return Number(n.s); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ n: number }>(); + }); + it("should have correct pointer types", () => { + expectTypeOf( + Parser.start() + .pointer("p", { + type: Parser.start().string("s", { length: 1 }), + offset: 1, + }) + .parse(buf) + ).toEqualTypeOf<{ p: { s: string } }>(); + expectTypeOf( + Parser.start() + .pointer("p", { + type: Parser.start().string("s", { length: 1 }), + offset: 1, + assert: (p) => { + expectTypeOf(p).toEqualTypeOf<{ s: string }>(); + return p.s.length == 1; + }, + formatter: (p) => { + expectTypeOf(p).toEqualTypeOf<{ s: string }>(); + return Number(p.s); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ p: number }>(); + }); + it("should have correct offset types", () => { + expectTypeOf(Parser.start().saveOffset("p").parse(buf)).toEqualTypeOf<{ + p: number; + }>(); + expectTypeOf( + Parser.start() + .saveOffset("p", { + assert: (p) => { + expectTypeOf(p).toEqualTypeOf(); + return p == 1; + }, + formatter: (p) => { + expectTypeOf(p).toEqualTypeOf(); + return p.toString(); + }, + }) + .parse(buf) + ).toEqualTypeOf<{ p: string }>(); + }); + it("should have assert context type", () => { + expectTypeOf( + Parser.start() + .uint8("a") + .uint8("b", { + assert() { + expectTypeOf(this).toEqualTypeOf<{ a: number }>(); + return this.a == 1; + }, + }) + .parse(buf) + ).toEqualTypeOf<{ a: number; b: number }>(); + }); + it("should have tag context type", () => { + expectTypeOf( + Parser.start() + .uint8("a") + .choice({ + tag() { + expectTypeOf(this).toEqualTypeOf<{ a: number }>(); + return this.a; + }, + choices: { + 1: Parser.start().uint8("num"), + }, + }) + .parse(buf) + ).toEqualTypeOf<{ a: number; num: number }>(); + }); +}); From 41dec502953188567f8dd6ab0c8cd57644f01dc2 Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Fri, 16 Dec 2022 23:00:55 +0900 Subject: [PATCH 02/10] refactor: enable some `compilerOptions` for safety --- lib/binary_parser.ts | 19 ++++++------------- test/types.ts | 4 ++-- tsconfig.json | 7 ++++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index d2da2fd5..c0679f35 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -15,11 +15,8 @@ class Context { } generateVariable(name?: string): string { - const scopes = [...this.scopes[this.scopes.length - 1]]; - if (name) { - scopes.push(name); - } - + const scopes = [...(this.scopes[this.scopes.length - 1] ?? [])]; + if (name) scopes.push(name); return scopes.join("."); } @@ -47,15 +44,11 @@ class Context { } pushPath(name: string) { - if (name) { - this.scopes[this.scopes.length - 1].push(name); - } + if (name) this.scopes[this.scopes.length - 1]?.push(name); } popPath(name: string) { - if (name) { - this.scopes[this.scopes.length - 1].pop(); - } + if (name) this.scopes[this.scopes.length - 1]?.pop(); } pushScope(name: string) { @@ -1347,7 +1340,7 @@ export class Parser { const getMaxBits = (from = 0) => { let sum = 0; for (let i = from; i < ctx.bitFields.length; i++) { - const length = ctx.bitFields[i].options.length as number; + const length = ctx.bitFields[i]!.options.length as number; if (sum + length > 32) break; sum += length; } @@ -1662,7 +1655,7 @@ export class Parser { ctx.pushCode(`switch(${tag}) {`); for (const tagString in this.options.choices) { const tag = parseInt(tagString, 10); - const type = this.options.choices[tag]; + const type = this.options.choices[tag]!; ctx.pushCode(`case ${tag}:`); this.generateChoiceCase(ctx, this.varName, type); diff --git a/test/types.ts b/test/types.ts index 16722b82..3ff9039c 100644 --- a/test/types.ts +++ b/test/types.ts @@ -162,11 +162,11 @@ describe("Parser", () => { length: 1, assert(a) { expectTypeOf(a).toEqualTypeOf<{ s: string }[]>(); - return a[0].s.length == 1; + return a[0]?.s.length === 1; }, formatter(a) { expectTypeOf(a).toEqualTypeOf<{ s: string }[]>(); - return Number(a[0].s); + return Number(a[0]?.s); }, }) .parse(buf) diff --git a/tsconfig.json b/tsconfig.json index 938d0b40..938f4861 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,10 +9,11 @@ "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitAny": true, + "exactOptionalPropertyTypes": true, "types": ["node", "mocha"], "esModuleInterop": true }, - "files": [ - "lib/binary_parser.ts" - ] + "files": ["lib/binary_parser.ts"] } From e4966316e25042d07f7b19be9e3c0f4d4f3e66bd Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:11:50 +0900 Subject: [PATCH 03/10] refactor: merge properties of inferred type for better IntelliSense readability --- lib/binary_parser.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index c0679f35..ae2fb437 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -273,8 +273,10 @@ const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveTypes]: boolean } = { }; type Next = N extends string - ? Parser - : Parser; + ? Parser< + O & { [name in N]: T } extends infer O ? { [K in keyof O]: O[K] } : never + > + : Parser; type ChoiceType

= P extends Parser ? O From 90abb11d7a09147b984e250b99118b0c40b0518e Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:37:01 +0900 Subject: [PATCH 04/10] fix: remove invalid signature of methods --- lib/binary_parser.ts | 46 +++----------------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index ae2fb437..846b4af1 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -315,7 +315,7 @@ export class Parser { private primitiveN( type: PrimitiveTypes, - varName: N | undefined, + varName: N, options: ParserOptions ): Next { return this.setNextParser(type as Types, varName, options); @@ -325,25 +325,15 @@ export class Parser { return (type + this.endian.toLowerCase()) as PrimitiveTypes; } - uint8(): Next; uint8( varName: N, - options?: ParserOptions - ): Next; - uint8( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN("uint8", varName, options); } - uint16(): Next; uint16( varName: N, - options?: ParserOptions - ): Next; - uint16( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN(this.useThisEndian("uint16"), varName, options); @@ -363,13 +353,8 @@ export class Parser { return this.primitiveN("uint16be", varName, options); } - uint32(): Next; uint32( varName: N, - options?: ParserOptions - ): Next; - uint32( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN(this.useThisEndian("uint32"), varName, options); @@ -389,25 +374,15 @@ export class Parser { return this.primitiveN("uint32be", varName, options); } - int8(): Next; int8( varName: N, - options?: ParserOptions - ): Next; - int8( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN("int8", varName, options); } - int16(): Next; int16( varName: N, - options?: ParserOptions - ): Next; - int16( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN(this.useThisEndian("int16"), varName, options); @@ -427,13 +402,8 @@ export class Parser { return this.primitiveN("int16be", varName, options); } - int32(): Next; int32( varName: N, - options?: ParserOptions - ): Next; - int32( - varName?: N, options: ParserOptions = {} ): Next { return this.primitiveN(this.useThisEndian("int32"), varName, options); @@ -458,13 +428,8 @@ export class Parser { throw new Error("BigInt64 is unsupported on this runtime"); } - int64(): Next; int64( varName: N, - options?: ParserOptions - ): Next; - int64( - varName?: N, options: ParserOptions = {} ): Next { this.bigIntVersionCheck(); @@ -487,13 +452,8 @@ export class Parser { return this.primitiveN("int64le", varName, options); } - uint64(): Next; uint64( varName: N, - options?: ParserOptions - ): Next; - uint64( - varName?: N, options: ParserOptions = {} ): Next { this.bigIntVersionCheck(); @@ -1192,13 +1152,13 @@ export class Parser { private setNextParser( type: Types, - varName: N | undefined, + varName: N, options: ParserOptions ): Next { const parser = new Parser(); parser.type = type; - parser.varName = varName as string; + parser.varName = varName; parser.options = options as ParserOptions; parser.endian = this.endian; From 9dfc35e1ceab3203139477aa3b0a7778a41fd788 Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:39:06 +0900 Subject: [PATCH 05/10] refactor: use singular name for union types --- lib/binary_parser.ts | 58 ++++++++++++++++++++++---------------------- test/types.ts | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index 846b4af1..c24da8ae 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -128,9 +128,9 @@ interface ArrayParserOptions type?: string | Parser; } -type Types = PrimitiveTypes | ComplexTypes; +type Type = PrimitiveType | ComplexType; -type ComplexTypes = +type ComplexType = | "bit" | "string" | "buffer" @@ -145,7 +145,7 @@ type ComplexTypes = type Endianness = "be" | "le"; -export type PrimitiveTypes = +export type PrimitiveType = | "uint8" | "uint16le" | "uint16be" @@ -165,7 +165,7 @@ export type PrimitiveTypes = | "doublele" | "doublebe"; -type PrimitiveTypesWithoutEndian = +type PrimitiveTypeWithoutEndian = | "uint8" | "uint16" | "uint32" @@ -175,7 +175,7 @@ type PrimitiveTypesWithoutEndian = | "int64" | "uint64"; -type BitSizes = +type BitSize = | 1 | 2 | 3 @@ -209,7 +209,7 @@ type BitSizes = | 31 | 32; -const PRIMITIVE_SIZES: { [key in PrimitiveTypes]: number } = { +const PRIMITIVE_SIZES: { [key in PrimitiveType]: number } = { uint8: 1, uint16le: 2, uint16be: 2, @@ -230,7 +230,7 @@ const PRIMITIVE_SIZES: { [key in PrimitiveTypes]: number } = { doublebe: 8, }; -const PRIMITIVE_NAMES: { [key in PrimitiveTypes]: string } = { +const PRIMITIVE_NAMES: { [key in PrimitiveType]: string } = { uint8: "Uint8", uint16le: "Uint16", uint16be: "Uint16", @@ -251,7 +251,7 @@ const PRIMITIVE_NAMES: { [key in PrimitiveTypes]: string } = { doublebe: "Float64", }; -const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveTypes]: boolean } = { +const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveType]: boolean } = { uint8: false, uint16le: true, uint16be: false, @@ -280,13 +280,13 @@ type Next = N extends string type ChoiceType

= P extends Parser ? O - : P extends PrimitiveTypes + : P extends PrimitiveType ? number : any; export class Parser { varName = ""; - type: Types = ""; + type: Type = ""; options: ParserOptions = {}; next?: Parser; head?: Parser; @@ -302,7 +302,7 @@ export class Parser { return new Parser(); } - private primitiveGenerateN(type: PrimitiveTypes, ctx: Context) { + private primitiveGenerateN(type: PrimitiveType, ctx: Context) { const typeName = PRIMITIVE_NAMES[type]; const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type]; ctx.pushCode( @@ -314,15 +314,15 @@ export class Parser { } private primitiveN( - type: PrimitiveTypes, + type: PrimitiveType, varName: N, options: ParserOptions ): Next { - return this.setNextParser(type as Types, varName, options); + return this.setNextParser(type as Type, varName, options); } - private useThisEndian(type: PrimitiveTypesWithoutEndian): PrimitiveTypes { - return (type + this.endian.toLowerCase()) as PrimitiveTypes; + private useThisEndian(type: PrimitiveTypeWithoutEndian): PrimitiveType { + return (type + this.endian.toLowerCase()) as PrimitiveType; } uint8( @@ -505,7 +505,7 @@ export class Parser { } private bitN( - size: BitSizes, + size: BitSize, varName: N, options: ParserOptions ): Next { @@ -823,7 +823,7 @@ export class Parser { array( varName: N, options: Omit, "type"> & { - type: PrimitiveTypes; + type: PrimitiveType; } ): Next; array( @@ -1094,7 +1094,7 @@ export class Parser { let size = NaN; if (Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0) { - size = PRIMITIVE_SIZES[this.type as PrimitiveTypes]; + size = PRIMITIVE_SIZES[this.type as PrimitiveType]; // if this is a fixed length string } else if ( @@ -1117,7 +1117,7 @@ export class Parser { ) { let elementSize = NaN; if (typeof this.options.type === "string") { - elementSize = PRIMITIVE_SIZES[this.options.type as PrimitiveTypes]; + elementSize = PRIMITIVE_SIZES[this.options.type as PrimitiveType]; } else if (this.options.type instanceof Parser) { elementSize = this.options.type.sizeOf(); } @@ -1151,7 +1151,7 @@ export class Parser { } private setNextParser( - type: Types, + type: Type, varName: N, options: ParserOptions ): Next { @@ -1504,12 +1504,12 @@ export class Parser { if (typeof type === "string") { if (!aliasRegistry.get(type)) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveTypes]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveTypes]; + const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; ctx.pushCode( `var ${item} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]};`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]};`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); @@ -1575,12 +1575,12 @@ export class Parser { if (typeof type === "string") { const varName = ctx.generateVariable(this.varName); if (!aliasRegistry.has(type)) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveTypes]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveTypes]; + const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; ctx.pushCode( `${varName} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]}`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]}`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); @@ -1809,12 +1809,12 @@ export class Parser { ctx.addReference(this.options.type!); } } else if (Object.keys(PRIMITIVE_SIZES).indexOf(this.options.type!) >= 0) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveTypes]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveTypes]; + const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; ctx.pushCode( `${nestVar} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]};`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]};`); } // Restore offset diff --git a/test/types.ts b/test/types.ts index 3ff9039c..3fd45d2a 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,6 +1,6 @@ import { expectTypeOf } from "expect-type"; -import { Parser as Parser_, PrimitiveTypes } from "../lib/binary_parser"; +import { Parser as Parser_, PrimitiveType } from "../lib/binary_parser"; type Endianness = "be" | "le"; type N = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; @@ -139,7 +139,7 @@ describe("Parser", () => { it("should have correct array types", () => { const arr = Parser.start() .array("a", { - type: "uint8" as PrimitiveTypes, + type: "uint8" as PrimitiveType, length: 1, }) .parse(buf); From bf75d53794c38395e7d6738b7b579494cfc63c16 Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:43:31 +0900 Subject: [PATCH 06/10] fix: remove invalid variant of `ComplexType` --- lib/binary_parser.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index c24da8ae..d76dd782 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -140,8 +140,7 @@ type ComplexType = | "seek" | "pointer" | "saveOffset" - | "wrapper" - | ""; + | "wrapper"; type Endianness = "be" | "le"; @@ -286,7 +285,7 @@ type ChoiceType

= P extends Parser export class Parser { varName = ""; - type: Type = ""; + type: Type | undefined; options: ParserOptions = {}; next?: Parser; head?: Parser; @@ -1093,7 +1092,10 @@ export class Parser { sizeOf(): number { let size = NaN; - if (Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0) { + if ( + this.type !== undefined && + Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0 + ) { size = PRIMITIVE_SIZES[this.type as PrimitiveType]; // if this is a fixed length string @@ -1293,7 +1295,9 @@ export class Parser { if ( !this.next || - (this.next && ["bit", "nest"].indexOf(this.next.type) < 0) + (this.next && + this.next.type !== undefined && + ["bit", "nest"].indexOf(this.next.type) < 0) ) { const val = ctx.generateTmpVariable(); From 3b7ff37ba322294fd6739ce5e72f8d93bb541443 Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:52:00 +0900 Subject: [PATCH 07/10] refactor: remove unneccessary type coercion --- lib/binary_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index d76dd782..68bf6a94 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -317,7 +317,7 @@ export class Parser { varName: N, options: ParserOptions ): Next { - return this.setNextParser(type as Type, varName, options); + return this.setNextParser(type, varName, options); } private useThisEndian(type: PrimitiveTypeWithoutEndian): PrimitiveType { From 698b974995c2c46ad0994268956d71ca12812d1a Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 00:59:40 +0900 Subject: [PATCH 08/10] fix: add missing default value for `options` parameter --- lib/binary_parser.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index 68bf6a94..51c169bd 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -758,7 +758,7 @@ export class Parser { string( varName: N, - options: ParserOptions + options: ParserOptions = {} ): Next { if (!options.zeroTerminated && !options.length && !options.greedy) { throw new Error( @@ -785,7 +785,7 @@ export class Parser { buffer( varName: N, - options: ParserOptions + options: ParserOptions = {} ): Next { if (!options.length && !options.readUntil) { throw new Error("length or readUntil must be defined for buffer."); @@ -944,7 +944,7 @@ export class Parser { pointer( varName: N, - options: ParserOptions + options: ParserOptions = {} ): Next { if (!options.offset) { throw new Error("offset is required for pointer."); From 28aa1a7008584ddf1ee906302585b6dd7051d9cd Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 01:06:35 +0900 Subject: [PATCH 09/10] refactor: enhance types --- lib/binary_parser.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index 51c169bd..8cdaa819 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -208,7 +208,12 @@ type BitSize = | 31 | 32; -const PRIMITIVE_SIZES: { [key in PrimitiveType]: number } = { +const conforms = + () => + (value: U): U => + value; + +const PRIMITIVE_SIZES = conforms<{ [key in PrimitiveType]: 1 | 2 | 4 | 8 }>()({ uint8: 1, uint16le: 2, uint16be: 2, @@ -227,9 +232,14 @@ const PRIMITIVE_SIZES: { [key in PrimitiveType]: number } = { floatbe: 4, doublele: 8, doublebe: 8, -}; - -const PRIMITIVE_NAMES: { [key in PrimitiveType]: string } = { +} as const); + +const PRIMITIVE_NAMES = conforms<{ + [key in PrimitiveType]: + | `${"Int" | "Uint"}${8 | 16 | 32}` + | `Big${"Int" | "Uint"}64` + | `Float${32 | 64}`; +}>()({ uint8: "Uint8", uint16le: "Uint16", uint16be: "Uint16", @@ -248,9 +258,11 @@ const PRIMITIVE_NAMES: { [key in PrimitiveType]: string } = { floatbe: "Float32", doublele: "Float64", doublebe: "Float64", -}; +} as const); -const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveType]: boolean } = { +const PRIMITIVE_LITTLE_ENDIANS = conforms<{ + [key in PrimitiveType]: boolean; +}>()({ uint8: false, uint16le: true, uint16be: false, @@ -269,7 +281,7 @@ const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveType]: boolean } = { floatbe: false, doublele: true, doublebe: false, -}; +} as const); type Next = N extends string ? Parser< From 0cf63cb89ab671098930cf6e422c2ae62eb087d4 Mon Sep 17 00:00:00 2001 From: Yu Shimura Date: Sat, 17 Dec 2022 04:27:42 +0900 Subject: [PATCH 10/10] fix: enhance types --- lib/binary_parser.ts | 541 ++++++++++++++++++++------------------- test/composite_parser.ts | 21 +- test/types.ts | 22 +- 3 files changed, 306 insertions(+), 278 deletions(-) diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index 8cdaa819..9e2f47d0 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -100,80 +100,163 @@ class Context { } } -const aliasRegistry = new Map>(); +const aliasRegistry = new Map>(); const FUNCTION_PREFIX = "___parser_"; -interface ParserOptions> { - length?: number | string | ((this: O, item: any) => number); - assert?: number | string | ((this: O, item: T) => boolean); - lengthInBytes?: number | string | ((this: O, item: any) => number); - type?: string | Parser; - formatter?: (this: O, item: T) => F; - encoding?: string; - readUntil?: "eof" | ((this: O, item: number, buffer: Buffer) => boolean); - greedy?: boolean; - choices?: Record; - defaultChoice?: string | Parser; - zeroTerminated?: boolean; - clone?: boolean; - stripNull?: boolean; - key?: string; - tag?: string | ((this: O, item: any) => number); - offset?: number | string | ((this: O, item: any) => number); - wrapper?: (buffer: Buffer) => Buffer; +namespace ParserOptions { + export type Common = { + formatter?: ((this: O, item: T) => F) | undefined; + assert?: number | string | ((this: O, item: T) => boolean) | undefined; + }; + + export type Numeric = Common; + export type String = Common & { + encoding?: TextDecoder["encoding"] | undefined; + length?: number | string | ((this: O, item: any) => number) | undefined; + zeroTerminated?: boolean | undefined; + greedy?: boolean | undefined; + stripNull?: boolean | undefined; + }; + export type Buffer = Common & { + clone?: boolean | undefined; + } & ( + | { length: number | string | ((this: O, item: any) => number) } + | { + readUntil: + | "eof" + | ((this: O, item: number, buffer: globalThis.Buffer) => boolean); + } + ); + export type Array< + O, + T extends Type.Numeric.Integer | Parser, + F + > = Common< + O, + T extends Type.Numeric.Integer + ? number[] + : T extends Parser + ? T[] + : never, + F + > & { + type: T; + } & ( + | { length: number | string | ((this: O, item: any) => number) } + | { lengthInBytes: number | string | ((this: O, item: any) => number) } + | { + readUntil: + | "eof" + | ((this: O, item: number, buffer: globalThis.Buffer) => boolean); + } + ); + export type Choice< + O, + C extends Record>, + D, + T, + F = C extends Record ? ChoiceType | D : never + > = Common & { + tag: string | ((this: O, item: any) => number); + choices: C; + defaultChoice?: string | Parser | undefined; + }; + export type Nest = Common & { + type: Parser; + }; + export type Pointer = Common & { + type: Type.Numeric.Integer | Type.Numeric.BigInteger | Parser; + offset: number | string | ((this: O, item: any) => number); + }; + export type SaveOffset = Common; + export type Wrapped = Common & { + wrapper: ( + x: globalThis.Buffer | Uint8Array + ) => globalThis.Buffer | Uint8Array; + type: Parser; + } & ( + | { length: number | string | ((this: O, item: any) => number) } + | { + readUntil: + | "eof" + | ((this: O, item: number, buffer: globalThis.Buffer) => boolean); + } + ); } -interface ArrayParserOptions - extends Omit, "type"> { - type?: string | Parser; +type ParserOptions = { + length?: number | string | ((this: O, item: any) => number) | undefined; + assert?: number | string | ((this: O, item: T) => boolean) | undefined; + lengthInBytes?: + | number + | string + | ((this: O, item: any) => number) + | undefined; + type?: string | Parser | undefined; + formatter?: ((this: O, item: T) => F) | undefined; + encoding?: string | undefined; + readUntil?: + | "eof" + | ((this: O, item: number, buffer: Buffer) => boolean) + | undefined; + greedy?: boolean | undefined; + choices?: Record> | undefined; + defaultChoice?: string | Parser | undefined; + zeroTerminated?: boolean | undefined; + clone?: boolean | undefined; + stripNull?: boolean | undefined; + key?: string | undefined; + tag?: string | ((this: O, item: any) => number) | undefined; + offset?: number | string | ((this: O, item: any) => number) | undefined; + wrapper?: ((buffer: Buffer | Uint8Array) => Buffer | Uint8Array) | undefined; +}; + +namespace Type { + export namespace Numeric { + export type Integer = + | "uint8" + | "uint16le" + | "uint16be" + | "uint32le" + | "uint32be" + | "int8" + | "int16le" + | "int16be" + | "int32le" + | "int32be"; + export type BigInteger = "uint64le" | "uint64be" | "int64le" | "int64be"; + export type Float = "floatle" | "floatbe" | "doublele" | "doublebe"; + + export type IntegerWithoutEndian = + | "uint8" + | "uint16" + | "uint32" + | "int8" + | "int16" + | "int32" + | "int64" + | "uint64"; + } + + export type Numeric = Numeric.Integer | Numeric.BigInteger | Numeric.Float; + + export type Complex = + | "bit" + | "string" + | "buffer" + | "array" + | "choice" + | "nest" + | "seek" + | "pointer" + | "saveOffset" + | "wrapper"; } -type Type = PrimitiveType | ComplexType; - -type ComplexType = - | "bit" - | "string" - | "buffer" - | "array" - | "choice" - | "nest" - | "seek" - | "pointer" - | "saveOffset" - | "wrapper"; +type Type = Type.Numeric | Type.Complex; type Endianness = "be" | "le"; -export type PrimitiveType = - | "uint8" - | "uint16le" - | "uint16be" - | "uint32le" - | "uint32be" - | "uint64le" - | "uint64be" - | "int8" - | "int16le" - | "int16be" - | "int32le" - | "int32be" - | "int64le" - | "int64be" - | "floatle" - | "floatbe" - | "doublele" - | "doublebe"; - -type PrimitiveTypeWithoutEndian = - | "uint8" - | "uint16" - | "uint32" - | "int8" - | "int16" - | "int32" - | "int64" - | "uint64"; - type BitSize = | 1 | 2 @@ -213,7 +296,7 @@ const conforms = (value: U): U => value; -const PRIMITIVE_SIZES = conforms<{ [key in PrimitiveType]: 1 | 2 | 4 | 8 }>()({ +const PRIMITIVE_SIZES = conforms<{ [key in Type.Numeric]: 1 | 2 | 4 | 8 }>()({ uint8: 1, uint16le: 2, uint16be: 2, @@ -235,7 +318,7 @@ const PRIMITIVE_SIZES = conforms<{ [key in PrimitiveType]: 1 | 2 | 4 | 8 }>()({ } as const); const PRIMITIVE_NAMES = conforms<{ - [key in PrimitiveType]: + [key in Type.Numeric]: | `${"Int" | "Uint"}${8 | 16 | 32}` | `Big${"Int" | "Uint"}64` | `Float${32 | 64}`; @@ -261,7 +344,7 @@ const PRIMITIVE_NAMES = conforms<{ } as const); const PRIMITIVE_LITTLE_ENDIANS = conforms<{ - [key in PrimitiveType]: boolean; + [key in Type.Numeric]: boolean; }>()({ uint8: false, uint16le: true, @@ -289,16 +372,16 @@ type Next = N extends string > : Parser; -type ChoiceType

= P extends Parser - ? O - : P extends PrimitiveType +type ChoiceType

= P extends Type.Numeric ? number - : any; + : P extends Parser + ? O + : unknown; export class Parser { varName = ""; type: Type | undefined; - options: ParserOptions = {}; + options: ParserOptions = {}; next?: Parser; head?: Parser; compiled?: Function; @@ -313,7 +396,7 @@ export class Parser { return new Parser(); } - private primitiveGenerateN(type: PrimitiveType, ctx: Context) { + private primitiveGenerateN(type: Type.Numeric, ctx: Context) { const typeName = PRIMITIVE_NAMES[type]; const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type]; ctx.pushCode( @@ -325,111 +408,111 @@ export class Parser { } private primitiveN( - type: PrimitiveType, + type: Type.Numeric, varName: N, options: ParserOptions ): Next { return this.setNextParser(type, varName, options); } - private useThisEndian(type: PrimitiveTypeWithoutEndian): PrimitiveType { - return (type + this.endian.toLowerCase()) as PrimitiveType; + private useThisEndian(type: Type.Numeric.IntegerWithoutEndian): Type.Numeric { + return (type + this.endian.toLowerCase()) as Type.Numeric; } uint8( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("uint8", varName, options); } uint16( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN(this.useThisEndian("uint16"), varName, options); } uint16le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("uint16le", varName, options); } uint16be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("uint16be", varName, options); } uint32( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN(this.useThisEndian("uint32"), varName, options); } uint32le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("uint32le", varName, options); } uint32be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("uint32be", varName, options); } int8( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("int8", varName, options); } int16( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN(this.useThisEndian("int16"), varName, options); } int16le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("int16le", varName, options); } int16be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("int16be", varName, options); } int32( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN(this.useThisEndian("int32"), varName, options); } int32le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("int32le", varName, options); } int32be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("int32be", varName, options); } @@ -441,7 +524,7 @@ export class Parser { int64( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("int64"), varName, options); @@ -449,7 +532,7 @@ export class Parser { int64be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN("int64be", varName, options); @@ -457,7 +540,7 @@ export class Parser { int64le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN("int64le", varName, options); @@ -465,7 +548,7 @@ export class Parser { uint64( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("uint64"), varName, options); @@ -473,7 +556,7 @@ export class Parser { uint64be( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN("uint64be", varName, options); @@ -481,7 +564,7 @@ export class Parser { uint64le( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { this.bigIntVersionCheck(); return this.primitiveN("uint64le", varName, options); @@ -489,28 +572,28 @@ export class Parser { floatle( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("floatle", varName, options); } floatbe( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("floatbe", varName, options); } doublele( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("doublele", varName, options); } doublebe( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.primitiveN("doublebe", varName, options); } @@ -526,224 +609,224 @@ export class Parser { bit1( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(1, varName, options); } bit2( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(2, varName, options); } bit3( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(3, varName, options); } bit4( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(4, varName, options); } bit5( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(5, varName, options); } bit6( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(6, varName, options); } bit7( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(7, varName, options); } bit8( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(8, varName, options); } bit9( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(9, varName, options); } bit10( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(10, varName, options); } bit11( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(11, varName, options); } bit12( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(12, varName, options); } bit13( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(13, varName, options); } bit14( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(14, varName, options); } bit15( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(15, varName, options); } bit16( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(16, varName, options); } bit17( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(17, varName, options); } bit18( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(18, varName, options); } bit19( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(19, varName, options); } bit20( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(20, varName, options); } bit21( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(21, varName, options); } bit22( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(22, varName, options); } bit23( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(23, varName, options); } bit24( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(24, varName, options); } bit25( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(25, varName, options); } bit26( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(26, varName, options); } bit27( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(27, varName, options); } bit28( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(28, varName, options); } bit29( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(29, varName, options); } bit30( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(30, varName, options); } bit31( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(31, varName, options); } bit32( varName: N, - options: ParserOptions = {} + options: ParserOptions.Numeric = {} ): Next { return this.bitN(32, varName, options); } @@ -754,23 +837,19 @@ export class Parser { return this; } - skip(length: ParserOptions["length"], options: ParserOptions = {}): this { - return this.seek(length, options); + skip(length: ParserOptions["length"]): this { + return this.seek(length); } - seek(relOffset: ParserOptions["length"], options: ParserOptions = {}): this { - if (options.assert) { - throw new Error("assert option on seek is not allowed."); - } - + seek(relOffset: ParserOptions["length"]): this { return this.setNextParser("seek", "", { length: relOffset, - } as ParserOptions) as unknown as this; + }) as unknown as this; } string( varName: N, - options: ParserOptions = {} + options: ParserOptions.String = {} ): Next { if (!options.zeroTerminated && !options.length && !options.greedy) { throw new Error( @@ -797,111 +876,66 @@ export class Parser { buffer( varName: N, - options: ParserOptions = {} + options: ParserOptions.Buffer ): Next { - if (!options.length && !options.readUntil) { - throw new Error("length or readUntil must be defined for buffer."); - } - return this.setNextParser("buffer", varName, options); } - wrapped(options: ParserOptions): Next; + wrapped( + options: ParserOptions.Wrapped + ): Next; wrapped( varName: N, - options: ParserOptions + options: ParserOptions.Wrapped ): Next; wrapped( - varName: N | ParserOptions, - options?: ParserOptions + ...args: + | [ParserOptions.Wrapped] + | [N, ParserOptions.Wrapped] ): Next { - if (typeof options !== "object" && typeof varName === "object") { - options = varName; - varName = ""; - } - - if (!options || !options.wrapper || !options.type) { - throw new Error("Both wrapper and type must be defined for wrapped."); - } - - if (!options.length && !options.readUntil) { - throw new Error("length or readUntil must be defined for wrapped."); - } - + const [varName, options] = + args.length === 2 ? args : ["" as const, ...args]; return this.setNextParser("wrapper", varName as N, options); } - array( - varName: N, - options: Omit, "type"> & { - type: PrimitiveType; - } - ): Next; - array( - varName: N, - options: ArrayParserOptions - ): Next; - array( - varName: N, - options: ArrayParserOptions - ): Next { - if (!options.readUntil && !options.length && !options.lengthInBytes) { - throw new Error( - "One of readUntil, length and lengthInBytes must be defined for array." - ); - } - - if (!options.type) { - throw new Error("type is required for array."); - } - - if ( - typeof options.type === "string" && - !aliasRegistry.has(options.type) && - !(options.type in PRIMITIVE_SIZES) - ) { - throw new Error(`Array element type "${options.type}" is unkown.`); - } - - return this.setNextParser( - "array", - varName, - options as ParserOptions - ); + array< + N extends string, + T extends Type.Numeric.Integer | Parser, + F = T extends Type.Numeric.Integer + ? number[] + : T extends Parser + ? T[] + : never + >(varName: N, options: ParserOptions.Array): Next { + return this.setNextParser("array", varName, options); } - choice>, F = ChoiceType>( - options: ParserOptions, F, C> - ): Next; + choice< + C extends Record>, + D = never, + T = C extends Record ? ChoiceType | D : never, + F = T + >(options: ParserOptions.Choice): Next; choice< N extends string, - C extends string | Parser>, - F = ChoiceType - >(varName: N, options: ParserOptions, F, C>): Next; + C extends Record>, + D = never, + T = C extends Record ? ChoiceType | D : never, + F = T + >(varName: N, options: ParserOptions.Choice): Next; choice< N extends string, - C extends string | Parser>, - F = ChoiceType + C extends Record>, + D = never, + T = C extends Record ? ChoiceType | D : never, + F = T >( - varName: N | ParserOptions, F, C>, - options?: ParserOptions, F, C> + ...args: + | [ParserOptions.Choice] + | [N, ParserOptions.Choice] ): Next { - if (typeof options !== "object" && typeof varName === "object") { - options = varName; - varName = ""; - } - - if (!options) { - throw new Error("tag and choices are are required for choice."); - } - - if (!options.tag) { - throw new Error("tag is requird for choice."); - } - - if (!options.choices) { - throw new Error("choices is required for choice."); - } + const [varName, options] = + args.length === 2 ? args : ["" as const, ...args]; for (const keyString in options.choices) { const key = parseInt(keyString, 10); @@ -923,23 +957,16 @@ export class Parser { return this.setNextParser("choice", varName as N, options); } - nest(options: ParserOptions): Next; + nest(options: ParserOptions.Nest): Next; nest( varName: N, - options: ParserOptions + options: ParserOptions.Nest ): Next; nest( - varName: N | ParserOptions, - options?: ParserOptions + ...args: [ParserOptions.Nest] | [N, ParserOptions.Nest] ): Next { - if (typeof options !== "object" && typeof varName === "object") { - options = varName; - varName = ""; - } - - if (!options || !options.type) { - throw new Error("type is required for nest."); - } + const [varName, options] = + args.length === 2 ? args : ["" as const, ...args]; if (!(options.type instanceof Parser) && !aliasRegistry.has(options.type)) { throw new Error("type must be a known parser name or a Parser object."); @@ -956,16 +983,8 @@ export class Parser { pointer( varName: N, - options: ParserOptions = {} + options: ParserOptions.Pointer ): Next { - if (!options.offset) { - throw new Error("offset is required for pointer."); - } - - if (!options.type) { - throw new Error("type is required for pointer."); - } - if ( typeof options.type === "string" && !(options.type in PRIMITIVE_SIZES) && @@ -979,7 +998,7 @@ export class Parser { saveOffset( varName: N, - options: ParserOptions = {} + options: ParserOptions.SaveOffset = {} ): Next { return this.setNextParser("saveOffset", varName, options); } @@ -1108,7 +1127,7 @@ export class Parser { this.type !== undefined && Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0 ) { - size = PRIMITIVE_SIZES[this.type as PrimitiveType]; + size = PRIMITIVE_SIZES[this.type as Type.Numeric]; // if this is a fixed length string } else if ( @@ -1131,7 +1150,7 @@ export class Parser { ) { let elementSize = NaN; if (typeof this.options.type === "string") { - elementSize = PRIMITIVE_SIZES[this.options.type as PrimitiveType]; + elementSize = PRIMITIVE_SIZES[this.options.type as Type.Numeric]; } else if (this.options.type instanceof Parser) { elementSize = this.options.type.sizeOf(); } @@ -1173,7 +1192,7 @@ export class Parser { parser.type = type; parser.varName = varName; - parser.options = options as ParserOptions; + parser.options = options as ParserOptions<{}, unknown, unknown>; parser.endian = this.endian; if (this.head) { @@ -1520,12 +1539,12 @@ export class Parser { if (typeof type === "string") { if (!aliasRegistry.get(type)) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; + const typeName = PRIMITIVE_NAMES[type as Type.Numeric]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as Type.Numeric]; ctx.pushCode( `var ${item} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]};`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as Type.Numeric]};`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); @@ -1591,12 +1610,12 @@ export class Parser { if (typeof type === "string") { const varName = ctx.generateVariable(this.varName); if (!aliasRegistry.has(type)) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; + const typeName = PRIMITIVE_NAMES[type as Type.Numeric]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as Type.Numeric]; ctx.pushCode( `${varName} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]}`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as Type.Numeric]}`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); @@ -1825,12 +1844,12 @@ export class Parser { ctx.addReference(this.options.type!); } } else if (Object.keys(PRIMITIVE_SIZES).indexOf(this.options.type!) >= 0) { - const typeName = PRIMITIVE_NAMES[type as PrimitiveType]; - const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveType]; + const typeName = PRIMITIVE_NAMES[type as Type.Numeric]; + const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as Type.Numeric]; ctx.pushCode( `${nestVar} = dataView.get${typeName}(offset, ${littleEndian});` ); - ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveType]};`); + ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as Type.Numeric]};`); } // Restore offset diff --git a/test/composite_parser.ts b/test/composite_parser.ts index d047b55b..2228357d 100644 --- a/test/composite_parser.ts +++ b/test/composite_parser.ts @@ -262,7 +262,6 @@ function compositeParserTests( .int32le("size") .string("name", { length: 8, encoding: "utf8" }), length: "numlumps", - key: "name", }); const buffer = factory([ @@ -272,18 +271,18 @@ function compositeParserTests( ]); deepStrictEqual(parser.parse(buffer), { numlumps: 2, - lumps: { - AAAAAAAA: { + lumps: [ + { filepos: 1234, size: 5678, name: "AAAAAAAA", }, - bbbbbbbb: { + { filepos: 5678, size: 1234, name: "bbbbbbbb", }, - }, + ], }); }); it("should use formatter to transform parsed array", () => { @@ -303,6 +302,7 @@ function compositeParserTests( .namely("self") .uint8("length") .array("data", { + // @ts-expect-error: undocumented behavior type: "self", length: "length", }); @@ -328,6 +328,7 @@ function compositeParserTests( .namely("self") .uint8("length") .array("data", { + // @ts-expect-error: undocumented behavior type: "self", length: "length", }); @@ -462,6 +463,7 @@ function compositeParserTests( .uint16le("valueLength") .array("message", { length: "length", + // @ts-expect-error: undocumented behavior type: "ArrayLengthIndexTest", }); @@ -598,6 +600,7 @@ function compositeParserTests( 0: stop, 1: "self", 2: Parser.start() + // @ts-expect-error: undocumented behavior .nest("left", { type: "self" }) .nest("right", { type: stop }), }, @@ -624,7 +627,9 @@ function compositeParserTests( // @ts-ignore const twoCells = Parser.start() .namely("twoCells") + // @ts-expect-error: undocumented behavior .nest("left", { type: "self" }) + // @ts-expect-error: undocumented behavior .nest("right", { type: "stop" }); parser.uint8("type").choice("data", { @@ -654,6 +659,7 @@ function compositeParserTests( const stop = Parser.start(); const twoCells = Parser.start() + // @ts-expect-error: undocumented behavior .nest("left", { type: "self" }) .nest("right", { type: stop }); @@ -690,11 +696,16 @@ function compositeParserTests( 0: stop, 1: "self", 2: Parser.start() + // @ts-expect-error: undocumented behavior .nest("left", { type: "self" }) + // @ts-expect-error: undocumented behavior .nest("right", { type: "self" }), 3: Parser.start() + // @ts-expect-error: undocumented behavior .nest("one", { type: "self" }) + // @ts-expect-error: undocumented behavior .nest("two", { type: "self" }) + // @ts-expect-error: undocumented behavior .nest("three", { type: "self" }), }, }); diff --git a/test/types.ts b/test/types.ts index 3fd45d2a..62029c39 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,6 +1,6 @@ import { expectTypeOf } from "expect-type"; -import { Parser as Parser_, PrimitiveType } from "../lib/binary_parser"; +import { Parser as Parser_ } from "../lib/binary_parser"; type Endianness = "be" | "le"; type N = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; @@ -121,7 +121,7 @@ describe("Parser", () => { type: Parser.start().string("s", { length: 1 }), length: 1, wrapper(b) { - expectTypeOf(b).toEqualTypeOf(); + expectTypeOf(b).toEqualTypeOf(); return b; }, assert: (w) => { @@ -139,7 +139,7 @@ describe("Parser", () => { it("should have correct array types", () => { const arr = Parser.start() .array("a", { - type: "uint8" as PrimitiveType, + type: "uint8" as const, length: 1, }) .parse(buf); @@ -182,9 +182,13 @@ describe("Parser", () => { 1: Parser.start().uint8("num"), 2: Parser.start().string("str", { zeroTerminated: true }), }, + defaultChoice: Parser.start().uint8("def"), }) .parse(buf) - ).toEqualTypeOf<{ tag: number; val: { num: number } | { str: string } }>(); + ).toEqualTypeOf<{ + tag: number; + val: { num: number } | { str: string } | { def: number }; + }>(); expectTypeOf( Parser.start() .uint8("tag") @@ -201,24 +205,18 @@ describe("Parser", () => { it("should have correct nest types", () => { expectTypeOf( Parser.start() - .nest("n", { - type: Parser.start().string("s", { length: 1 }), - }) + .nest("n", { type: Parser.start().string("s", { length: 1 }) }) .parse(buf) ).toEqualTypeOf<{ n: { s: string } }>(); expectTypeOf( Parser.start() - .nest({ - type: Parser.start().string("s", { length: 1 }), - length: 1, - }) + .nest({ type: Parser.start().string("s", { length: 1 }) }) .parse(buf) ).toEqualTypeOf<{ s: string }>(); expectTypeOf( Parser.start() .nest("n", { type: Parser.start().string("s", { length: 1 }), - length: 1, assert: (n) => { expectTypeOf(n).toEqualTypeOf<{ s: string }>(); return n.s.length == 1;