UNPKG

binary-parser

Version:

Blazing-fast binary parser builder

1,585 lines (1,350 loc) 44.8 kB
class Context { code = ""; scopes = [["vars"]]; bitFields: Parser[] = []; tmpVariableCount = 0; references = new Map<string, { resolved: boolean; requested: boolean }>(); importPath: string; imports: any[] = []; reverseImports = new Map<any, number>(); useContextVariables = false; constructor(importPath: string, useContextVariables: boolean) { this.importPath = importPath; this.useContextVariables = useContextVariables; } generateVariable(name?: string): string { const scopes = [...this.scopes[this.scopes.length - 1]]; if (name) { scopes.push(name); } return scopes.join("."); } generateOption(val: number | string | Function): string { switch (typeof val) { case "number": return val.toString(); case "string": return this.generateVariable(val); case "function": return `${this.addImport(val)}.call(${this.generateVariable()}, vars)`; } } generateError(err: string) { this.pushCode(`throw new Error(${err});`); } generateTmpVariable(): string { return "$tmp" + this.tmpVariableCount++; } pushCode(code: string) { this.code += code + "\n"; } pushPath(name: string) { if (name) { this.scopes[this.scopes.length - 1].push(name); } } popPath(name: string) { if (name) { this.scopes[this.scopes.length - 1].pop(); } } pushScope(name: string) { this.scopes.push([name]); } popScope() { this.scopes.pop(); } addImport(im: any): string { if (!this.importPath) return `(${im})`; let id = this.reverseImports.get(im); if (!id) { id = this.imports.push(im) - 1; this.reverseImports.set(im, id); } return `${this.importPath}[${id}]`; } addReference(alias: string) { if (!this.references.has(alias)) { this.references.set(alias, { resolved: false, requested: false }); } } markResolved(alias: string) { const reference = this.references.get(alias); if (reference) { reference.resolved = true; } } markRequested(aliasList: string[]) { aliasList.forEach((alias) => { const reference = this.references.get(alias); if (reference) { reference.requested = true; } }); } getUnresolvedReferences(): string[] { return Array.from(this.references) .filter(([_, reference]) => !reference.resolved && !reference.requested) .map(([alias, _]) => alias); } } const aliasRegistry = new Map<string, Parser>(); 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; encoding?: string; readUntil?: "eof" | ((item: any, buffer: Buffer) => boolean); greedy?: boolean; choices?: { [key: number]: string | Parser }; defaultChoice?: string | Parser; zeroTerminated?: boolean; clone?: boolean; stripNull?: boolean; key?: string; tag?: string | ((item: any) => number); offset?: number | string | ((item: any) => number); wrapper?: (buffer: Buffer) => Buffer; } type Types = PrimitiveTypes | ComplexTypes; type ComplexTypes = | "bit" | "string" | "buffer" | "array" | "choice" | "nest" | "seek" | "pointer" | "saveOffset" | "wrapper" | ""; type Endianness = "be" | "le"; type PrimitiveTypes = | "uint8" | "uint16le" | "uint16be" | "uint32le" | "uint32be" | "uint64le" | "uint64be" | "int8" | "int16le" | "int16be" | "int32le" | "int32be" | "int64le" | "int64be" | "floatle" | "floatbe" | "doublele" | "doublebe"; type PrimitiveTypesWithoutEndian = | "uint8" | "uint16" | "uint32" | "int8" | "int16" | "int32" | "int64" | "uint64"; type BitSizes = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32; const PRIMITIVE_SIZES: { [key in PrimitiveTypes]: number } = { uint8: 1, uint16le: 2, uint16be: 2, uint32le: 4, uint32be: 4, int8: 1, int16le: 2, int16be: 2, int32le: 4, int32be: 4, int64be: 8, int64le: 8, uint64be: 8, uint64le: 8, floatle: 4, floatbe: 4, doublele: 8, doublebe: 8, }; const PRIMITIVE_NAMES: { [key in PrimitiveTypes]: string } = { uint8: "Uint8", uint16le: "Uint16", uint16be: "Uint16", uint32le: "Uint32", uint32be: "Uint32", int8: "Int8", int16le: "Int16", int16be: "Int16", int32le: "Int32", int32be: "Int32", int64be: "BigInt64", int64le: "BigInt64", uint64be: "BigUint64", uint64le: "BigUint64", floatle: "Float32", floatbe: "Float32", doublele: "Float64", doublebe: "Float64", }; const PRIMITIVE_LITTLE_ENDIANS: { [key in PrimitiveTypes]: boolean } = { uint8: false, uint16le: true, uint16be: false, uint32le: true, uint32be: false, int8: false, int16le: true, int16be: false, int32le: true, int32be: false, int64be: false, int64le: true, uint64be: false, uint64le: true, floatle: true, floatbe: false, doublele: true, doublebe: false, }; export class Parser { varName = ""; type: Types = ""; options: ParserOptions = {}; next?: Parser; head?: Parser; compiled?: Function; endian: Endianness = "be"; constructorFn?: Function; alias?: string; useContextVariables = false; constructor() {} static start() { return new Parser(); } private primitiveGenerateN(type: PrimitiveTypes, ctx: Context) { const typeName = PRIMITIVE_NAMES[type]; const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type]; ctx.pushCode( `${ctx.generateVariable( this.varName )} = dataView.get${typeName}(offset, ${littleEndian});` ); ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type]};`); } private primitiveN( type: PrimitiveTypes, varName: string, options: ParserOptions ): this { return this.setNextParser(type as Types, varName, options); } private useThisEndian(type: PrimitiveTypesWithoutEndian): PrimitiveTypes { return (type + this.endian.toLowerCase()) as PrimitiveTypes; } uint8(varName: string, options: ParserOptions = {}): this { return this.primitiveN("uint8", varName, options); } uint16(varName: string, options: ParserOptions = {}): this { return this.primitiveN(this.useThisEndian("uint16"), varName, options); } uint16le(varName: string, options: ParserOptions = {}): this { return this.primitiveN("uint16le", varName, options); } uint16be(varName: string, options: ParserOptions = {}): this { return this.primitiveN("uint16be", varName, options); } uint32(varName: string, options: ParserOptions = {}): this { return this.primitiveN(this.useThisEndian("uint32"), varName, options); } uint32le(varName: string, options: ParserOptions = {}): this { return this.primitiveN("uint32le", varName, options); } uint32be(varName: string, options: ParserOptions = {}): this { return this.primitiveN("uint32be", varName, options); } int8(varName: string, options: ParserOptions = {}): this { return this.primitiveN("int8", varName, options); } int16(varName: string, options: ParserOptions = {}): this { return this.primitiveN(this.useThisEndian("int16"), varName, options); } int16le(varName: string, options: ParserOptions = {}): this { return this.primitiveN("int16le", varName, options); } int16be(varName: string, options: ParserOptions = {}): this { return this.primitiveN("int16be", varName, options); } int32(varName: string, options: ParserOptions = {}): this { return this.primitiveN(this.useThisEndian("int32"), varName, options); } int32le(varName: string, options: ParserOptions = {}): this { return this.primitiveN("int32le", varName, options); } int32be(varName: string, options: ParserOptions = {}): this { return this.primitiveN("int32be", varName, options); } private bigIntVersionCheck() { if (!DataView.prototype.getBigInt64) throw new Error("BigInt64 is unsupported on this runtime"); } int64(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("int64"), varName, options); } int64be(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN("int64be", varName, options); } int64le(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN("int64le", varName, options); } uint64(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN(this.useThisEndian("uint64"), varName, options); } uint64be(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN("uint64be", varName, options); } uint64le(varName: string, options: ParserOptions = {}): this { this.bigIntVersionCheck(); return this.primitiveN("uint64le", varName, options); } floatle(varName: string, options: ParserOptions = {}): this { return this.primitiveN("floatle", varName, options); } floatbe(varName: string, options: ParserOptions = {}): this { return this.primitiveN("floatbe", varName, options); } doublele(varName: string, options: ParserOptions = {}): this { return this.primitiveN("doublele", varName, options); } doublebe(varName: string, options: ParserOptions = {}): this { return this.primitiveN("doublebe", varName, options); } private bitN(size: BitSizes, varName: string, options: ParserOptions): this { options.length = size; return this.setNextParser("bit", varName, options); } bit1(varName: string, options: ParserOptions = {}): this { return this.bitN(1, varName, options); } bit2(varName: string, options: ParserOptions = {}): this { return this.bitN(2, varName, options); } bit3(varName: string, options: ParserOptions = {}): this { return this.bitN(3, varName, options); } bit4(varName: string, options: ParserOptions = {}): this { return this.bitN(4, varName, options); } bit5(varName: string, options: ParserOptions = {}): this { return this.bitN(5, varName, options); } bit6(varName: string, options: ParserOptions = {}): this { return this.bitN(6, varName, options); } bit7(varName: string, options: ParserOptions = {}): this { return this.bitN(7, varName, options); } bit8(varName: string, options: ParserOptions = {}): this { return this.bitN(8, varName, options); } bit9(varName: string, options: ParserOptions = {}): this { return this.bitN(9, varName, options); } bit10(varName: string, options: ParserOptions = {}): this { return this.bitN(10, varName, options); } bit11(varName: string, options: ParserOptions = {}): this { return this.bitN(11, varName, options); } bit12(varName: string, options: ParserOptions = {}): this { return this.bitN(12, varName, options); } bit13(varName: string, options: ParserOptions = {}): this { return this.bitN(13, varName, options); } bit14(varName: string, options: ParserOptions = {}): this { return this.bitN(14, varName, options); } bit15(varName: string, options: ParserOptions = {}): this { return this.bitN(15, varName, options); } bit16(varName: string, options: ParserOptions = {}): this { return this.bitN(16, varName, options); } bit17(varName: string, options: ParserOptions = {}): this { return this.bitN(17, varName, options); } bit18(varName: string, options: ParserOptions = {}): this { return this.bitN(18, varName, options); } bit19(varName: string, options: ParserOptions = {}): this { return this.bitN(19, varName, options); } bit20(varName: string, options: ParserOptions = {}): this { return this.bitN(20, varName, options); } bit21(varName: string, options: ParserOptions = {}): this { return this.bitN(21, varName, options); } bit22(varName: string, options: ParserOptions = {}): this { return this.bitN(22, varName, options); } bit23(varName: string, options: ParserOptions = {}): this { return this.bitN(23, varName, options); } bit24(varName: string, options: ParserOptions = {}): this { return this.bitN(24, varName, options); } bit25(varName: string, options: ParserOptions = {}): this { return this.bitN(25, varName, options); } bit26(varName: string, options: ParserOptions = {}): this { return this.bitN(26, varName, options); } bit27(varName: string, options: ParserOptions = {}): this { return this.bitN(27, varName, options); } bit28(varName: string, options: ParserOptions = {}): this { return this.bitN(28, varName, options); } bit29(varName: string, options: ParserOptions = {}): this { return this.bitN(29, varName, options); } bit30(varName: string, options: ParserOptions = {}): this { return this.bitN(30, varName, options); } bit31(varName: string, options: ParserOptions = {}): this { return this.bitN(31, varName, options); } bit32(varName: string, options: ParserOptions = {}): this { return this.bitN(32, varName, options); } namely(alias: string): this { aliasRegistry.set(alias, this); this.alias = alias; return this; } skip(length: ParserOptions["length"], options: ParserOptions = {}): this { return this.seek(length, options); } seek(relOffset: ParserOptions["length"], options: ParserOptions = {}): this { if (options.assert) { throw new Error("assert option on seek is not allowed."); } return this.setNextParser("seek", "", { length: relOffset }); } string(varName: string, options: ParserOptions): this { if (!options.zeroTerminated && !options.length && !options.greedy) { throw new Error( "One of length, zeroTerminated, or greedy must be defined for string." ); } if ((options.zeroTerminated || options.length) && options.greedy) { throw new Error( "greedy is mutually exclusive with length and zeroTerminated for string." ); } if (options.stripNull && !(options.length || options.greedy)) { throw new Error( "length or greedy must be defined if stripNull is enabled." ); } options.encoding = options.encoding || "utf8"; return this.setNextParser("string", varName, options); } buffer(varName: string, options: ParserOptions): this { if (!options.length && !options.readUntil) { throw new Error("length or readUntil must be defined for buffer."); } return this.setNextParser("buffer", varName, options); } wrapped(varName: string | ParserOptions, options?: ParserOptions): this { 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."); } return this.setNextParser("wrapper", varName as string, options); } array(varName: string, options: ParserOptions): this { 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); } choice(varName: string | ParserOptions, options?: ParserOptions): this { 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."); } for (const keyString in options.choices) { const key = parseInt(keyString, 10); const value = options.choices[key]; if (isNaN(key)) { throw new Error(`Choice key "${keyString}" is not a number.`); } if ( typeof value === "string" && !aliasRegistry.has(value) && !((value as string) in PRIMITIVE_SIZES) ) { throw new Error(`Choice type "${value}" is unkown.`); } } return this.setNextParser("choice", varName as string, options); } nest(varName: string | ParserOptions, options?: ParserOptions): this { if (typeof options !== "object" && typeof varName === "object") { options = varName; varName = ""; } if (!options || !options.type) { throw new Error("type is required for nest."); } if (!(options.type instanceof Parser) && !aliasRegistry.has(options.type)) { throw new Error("type must be a known parser name or a Parser object."); } if (!(options.type instanceof Parser) && !varName) { throw new Error( "type must be a Parser object if the variable name is omitted." ); } return this.setNextParser("nest", varName as string, options); } pointer(varName: string, options: ParserOptions): this { 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) && !aliasRegistry.has(options.type) ) { throw new Error(`Pointer type "${options.type}" is unkown.`); } return this.setNextParser("pointer", varName, options); } saveOffset(varName: string, options: ParserOptions = {}): this { return this.setNextParser("saveOffset", varName, options); } endianness(endianness: "little" | "big"): this { switch (endianness.toLowerCase()) { case "little": this.endian = "le"; break; case "big": this.endian = "be"; break; default: throw new Error('endianness must be one of "little" or "big"'); } return this; } endianess(endianess: "little" | "big"): this { return this.endianness(endianess); } useContextVars(useContextVariables = true): this { this.useContextVariables = useContextVariables; return this; } create(constructorFn: Function): this { if (!(constructorFn instanceof Function)) { throw new Error("Constructor must be a Function object."); } this.constructorFn = constructorFn; return this; } private getContext(importPath: string): Context { const ctx = new Context(importPath, this.useContextVariables); ctx.pushCode( "var dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);" ); if (!this.alias) { this.addRawCode(ctx); } else { this.addAliasedCode(ctx); ctx.pushCode(`return ${FUNCTION_PREFIX + this.alias}(0).result;`); } return ctx; } getCode(): string { const importPath = "imports"; return this.getContext(importPath).code; } private addRawCode(ctx: Context) { ctx.pushCode("var offset = 0;"); ctx.pushCode( `var vars = ${this.constructorFn ? "new constructorFn()" : "{}"};` ); ctx.pushCode("vars.$parent = null;"); ctx.pushCode("vars.$root = vars;"); this.generate(ctx); this.resolveReferences(ctx); ctx.pushCode("delete vars.$parent;"); ctx.pushCode("delete vars.$root;"); ctx.pushCode("return vars;"); } private addAliasedCode(ctx: Context) { ctx.pushCode(`function ${FUNCTION_PREFIX + this.alias}(offset, context) {`); ctx.pushCode( `var vars = ${this.constructorFn ? "new constructorFn()" : "{}"};` ); ctx.pushCode( "var ctx = Object.assign({$parent: null, $root: vars}, context || {});" ); ctx.pushCode(`vars = Object.assign(vars, ctx);`); this.generate(ctx); ctx.markResolved(this.alias!); this.resolveReferences(ctx); ctx.pushCode( "Object.keys(ctx).forEach(function (item) { delete vars[item]; });" ); ctx.pushCode("return { offset: offset, result: vars };"); ctx.pushCode("}"); return ctx; } private resolveReferences(ctx: Context) { const references = ctx.getUnresolvedReferences(); ctx.markRequested(references); references.forEach((alias) => { aliasRegistry.get(alias)?.addAliasedCode(ctx); }); } compile() { const importPath = "imports"; const ctx = this.getContext(importPath); this.compiled = new Function( importPath, "TextDecoder", `return function (buffer, constructorFn) { ${ctx.code} };` )(ctx.imports, TextDecoder); } sizeOf(): number { let size = NaN; if (Object.keys(PRIMITIVE_SIZES).indexOf(this.type) >= 0) { size = PRIMITIVE_SIZES[this.type as PrimitiveTypes]; // if this is a fixed length string } else if ( this.type === "string" && typeof this.options.length === "number" ) { size = this.options.length; // if this is a fixed length buffer } else if ( this.type === "buffer" && typeof this.options.length === "number" ) { size = this.options.length; // if this is a fixed length array } else if ( this.type === "array" && typeof this.options.length === "number" ) { let elementSize = NaN; if (typeof this.options.type === "string") { elementSize = PRIMITIVE_SIZES[this.options.type as PrimitiveTypes]; } else if (this.options.type instanceof Parser) { elementSize = this.options.type.sizeOf(); } size = this.options.length * elementSize; // if this a skip } else if (this.type === "seek") { size = this.options.length as number; // if this is a nested parser } else if (this.type === "nest") { size = (this.options.type as Parser).sizeOf(); } else if (!this.type) { size = 0; } if (this.next) { size += this.next.sizeOf(); } return size; } // Follow the parser chain till the root and start parsing from there parse(buffer: Buffer | Uint8Array) { if (!this.compiled) { this.compile(); } return this.compiled!(buffer, this.constructorFn); } private setNextParser( type: Types, varName: string, options: ParserOptions ): this { const parser = new Parser(); parser.type = type; parser.varName = varName; parser.options = options; parser.endian = this.endian; if (this.head) { this.head.next = parser; } else { this.next = parser; } this.head = parser; return this; } // Call code generator for this parser private generate(ctx: Context) { if (this.type) { switch (this.type) { case "uint8": case "uint16le": case "uint16be": case "uint32le": case "uint32be": case "int8": case "int16le": case "int16be": case "int32le": case "int32be": case "int64be": case "int64le": case "uint64be": case "uint64le": case "floatle": case "floatbe": case "doublele": case "doublebe": this.primitiveGenerateN(this.type, ctx); break; case "bit": this.generateBit(ctx); break; case "string": this.generateString(ctx); break; case "buffer": this.generateBuffer(ctx); break; case "seek": this.generateSeek(ctx); break; case "nest": this.generateNest(ctx); break; case "array": this.generateArray(ctx); break; case "choice": this.generateChoice(ctx); break; case "pointer": this.generatePointer(ctx); break; case "saveOffset": this.generateSaveOffset(ctx); break; case "wrapper": this.generateWrapper(ctx); break; } if (this.type !== "bit") this.generateAssert(ctx); } const varName = ctx.generateVariable(this.varName); if (this.options.formatter && this.type !== "bit") { this.generateFormatter(ctx, varName, this.options.formatter); } return this.generateNext(ctx); } private generateAssert(ctx: Context) { if (!this.options.assert) { return; } const varName = ctx.generateVariable(this.varName); switch (typeof this.options.assert) { case "function": { const func = ctx.addImport(this.options.assert); ctx.pushCode(`if (!${func}.call(vars, ${varName})) {`); } break; case "number": ctx.pushCode(`if (${this.options.assert} !== ${varName}) {`); break; case "string": ctx.pushCode( `if (${JSON.stringify(this.options.assert)} !== ${varName}) {` ); break; default: throw new Error( "assert option must be a string, number or a function." ); } ctx.generateError( `"Assertion error: ${varName} is " + ${JSON.stringify( this.options.assert.toString() )}` ); ctx.pushCode("}"); } // Recursively call code generators and append results private generateNext(ctx: Context): Context { if (this.next) { ctx = this.next.generate(ctx); } return ctx; } private generateBit(ctx: Context) { // TODO find better method to handle nested bit fields const parser = JSON.parse(JSON.stringify(this)); parser.options = this.options; parser.generateAssert = this.generateAssert.bind(this); parser.generateFormatter = this.generateFormatter.bind(this); parser.varName = ctx.generateVariable(parser.varName); ctx.bitFields.push(parser); if ( !this.next || (this.next && ["bit", "nest"].indexOf(this.next.type) < 0) ) { const val = ctx.generateTmpVariable(); ctx.pushCode(`var ${val} = 0;`); 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; if (sum + length > 32) break; sum += length; } return sum; }; const getBytes = (sum: number) => { if (sum <= 8) { ctx.pushCode(`${val} = dataView.getUint8(offset);`); sum = 8; } else if (sum <= 16) { ctx.pushCode(`${val} = dataView.getUint16(offset);`); sum = 16; } else if (sum <= 24) { ctx.pushCode( `${val} = (dataView.getUint16(offset) << 8) | dataView.getUint8(offset + 2);` ); sum = 24; } else { ctx.pushCode(`${val} = dataView.getUint32(offset);`); sum = 32; } ctx.pushCode(`offset += ${sum / 8};`); return sum; }; let bitOffset = 0; const isBigEndian = this.endian === "be"; let sum = 0; let rem = 0; ctx.bitFields.forEach((parser, i) => { let length = parser.options.length as number; if (length > rem) { if (rem) { const mask = -1 >>> (32 - rem); ctx.pushCode( `${parser.varName} = (${val} & 0x${mask.toString(16)}) << ${ length - rem };` ); length -= rem; } bitOffset = 0; rem = sum = getBytes(getMaxBits(i) - rem); } const offset = isBigEndian ? sum - bitOffset - length : bitOffset; const mask = -1 >>> (32 - length); ctx.pushCode( `${parser.varName} ${ length < (parser.options.length as number) ? "|=" : "=" } ${val} >> ${offset} & 0x${mask.toString(16)};` ); // Ensure value is unsigned if ((parser.options.length as number) === 32) { ctx.pushCode(`${parser.varName} >>>= 0`); } if (parser.options.assert) { parser.generateAssert(ctx); } if (parser.options.formatter) { parser.generateFormatter( ctx, parser.varName, parser.options.formatter ); } bitOffset += length; rem -= length; }); ctx.bitFields = []; } } private generateSeek(ctx: Context) { const length = ctx.generateOption(this.options.length!); ctx.pushCode(`offset += ${length};`); } private generateString(ctx: Context) { const name = ctx.generateVariable(this.varName); const start = ctx.generateTmpVariable(); const encoding = this.options.encoding!; const isHex = encoding.toLowerCase() === "hex"; const toHex = 'b => b.toString(16).padStart(2, "0")'; if (this.options.length && this.options.zeroTerminated) { const len = this.options.length; ctx.pushCode(`var ${start} = offset;`); ctx.pushCode( `while(dataView.getUint8(offset++) !== 0 && offset - ${start} < ${len});` ); const end = `offset - ${start} < ${len} ? offset - 1 : offset`; ctx.pushCode( isHex ? `${name} = Array.from(buffer.subarray(${start}, ${end}), ${toHex}).join('');` : `${name} = new TextDecoder('${encoding}').decode(buffer.subarray(${start}, ${end}));` ); } else if (this.options.length) { const len = ctx.generateOption(this.options.length); ctx.pushCode( isHex ? `${name} = Array.from(buffer.subarray(offset, offset + ${len}), ${toHex}).join('');` : `${name} = new TextDecoder('${encoding}').decode(buffer.subarray(offset, offset + ${len}));` ); ctx.pushCode(`offset += ${len};`); } else if (this.options.zeroTerminated) { ctx.pushCode(`var ${start} = offset;`); ctx.pushCode("while(dataView.getUint8(offset++) !== 0);"); ctx.pushCode( isHex ? `${name} = Array.from(buffer.subarray(${start}, offset - 1), ${toHex}).join('');` : `${name} = new TextDecoder('${encoding}').decode(buffer.subarray(${start}, offset - 1));` ); } else if (this.options.greedy) { ctx.pushCode(`var ${start} = offset;`); ctx.pushCode("while(buffer.length > offset++);"); ctx.pushCode( isHex ? `${name} = Array.from(buffer.subarray(${start}, offset), ${toHex}).join('');` : `${name} = new TextDecoder('${encoding}').decode(buffer.subarray(${start}, offset));` ); } if (this.options.stripNull) { ctx.pushCode(`${name} = ${name}.replace(/\\x00+$/g, '')`); } } private generateBuffer(ctx: Context) { const varName = ctx.generateVariable(this.varName); if (typeof this.options.readUntil === "function") { const pred = this.options.readUntil; const start = ctx.generateTmpVariable(); const cur = ctx.generateTmpVariable(); ctx.pushCode(`var ${start} = offset;`); ctx.pushCode(`var ${cur} = 0;`); ctx.pushCode(`while (offset < buffer.length) {`); ctx.pushCode(`${cur} = dataView.getUint8(offset);`); const func = ctx.addImport(pred); ctx.pushCode( `if (${func}.call(${ctx.generateVariable()}, ${cur}, buffer.subarray(offset))) break;` ); ctx.pushCode(`offset += 1;`); ctx.pushCode(`}`); ctx.pushCode(`${varName} = buffer.subarray(${start}, offset);`); } else if (this.options.readUntil === "eof") { ctx.pushCode(`${varName} = buffer.subarray(offset);`); } else { const len = ctx.generateOption(this.options.length!); ctx.pushCode(`${varName} = buffer.subarray(offset, offset + ${len});`); ctx.pushCode(`offset += ${len};`); } if (this.options.clone) { ctx.pushCode(`${varName} = buffer.constructor.from(${varName});`); } } private generateArray(ctx: Context) { const length = ctx.generateOption(this.options.length!); const lengthInBytes = ctx.generateOption(this.options.lengthInBytes!); const type = this.options.type; const counter = ctx.generateTmpVariable(); const lhs = ctx.generateVariable(this.varName); const item = ctx.generateTmpVariable(); const key = this.options.key; const isHash = typeof key === "string"; if (isHash) { ctx.pushCode(`${lhs} = {};`); } else { ctx.pushCode(`${lhs} = [];`); } if (typeof this.options.readUntil === "function") { ctx.pushCode("do {"); } else if (this.options.readUntil === "eof") { ctx.pushCode( `for (var ${counter} = 0; offset < buffer.length; ${counter}++) {` ); } else if (lengthInBytes !== undefined) { ctx.pushCode( `for (var ${counter} = offset + ${lengthInBytes}; offset < ${counter}; ) {` ); } else { ctx.pushCode( `for (var ${counter} = ${length}; ${counter} > 0; ${counter}--) {` ); } if (typeof type === "string") { if (!aliasRegistry.get(type)) { const typeName = PRIMITIVE_NAMES[type as PrimitiveTypes]; const littleEndian = PRIMITIVE_LITTLE_ENDIANS[type as PrimitiveTypes]; ctx.pushCode( `var ${item} = dataView.get${typeName}(offset, ${littleEndian});` ); ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]};`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`$parent: ${parentVar},`); ctx.pushCode(`$root: ${parentVar}.$root,`); if (!this.options.readUntil && lengthInBytes === undefined) { ctx.pushCode(`$index: ${length} - ${counter},`); } } ctx.pushCode(`});`); ctx.pushCode( `var ${item} = ${tempVar}.result; offset = ${tempVar}.offset;` ); if (type !== this.alias) ctx.addReference(type); } } else if (type instanceof Parser) { ctx.pushCode(`var ${item} = {};`); const parentVar = ctx.generateVariable(); ctx.pushScope(item); if (ctx.useContextVariables) { ctx.pushCode(`${item}.$parent = ${parentVar};`); ctx.pushCode(`${item}.$root = ${parentVar}.$root;`); if (!this.options.readUntil && lengthInBytes === undefined) { ctx.pushCode(`${item}.$index = ${length} - ${counter};`); } } type.generate(ctx); if (ctx.useContextVariables) { ctx.pushCode(`delete ${item}.$parent;`); ctx.pushCode(`delete ${item}.$root;`); ctx.pushCode(`delete ${item}.$index;`); } ctx.popScope(); } if (isHash) { ctx.pushCode(`${lhs}[${item}.${key}] = ${item};`); } else { ctx.pushCode(`${lhs}.push(${item});`); } ctx.pushCode("}"); if (typeof this.options.readUntil === "function") { const pred = this.options.readUntil; const func = ctx.addImport(pred); ctx.pushCode( `while (!${func}.call(${ctx.generateVariable()}, ${item}, buffer.subarray(offset)));` ); } } private generateChoiceCase( ctx: Context, varName: string, type: string | 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]; ctx.pushCode( `${varName} = dataView.get${typeName}(offset, ${littleEndian});` ); ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]}`); } else { const tempVar = ctx.generateTmpVariable(); ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset, {`); if (ctx.useContextVariables) { ctx.pushCode(`$parent: ${varName}.$parent,`); ctx.pushCode(`$root: ${varName}.$root,`); } ctx.pushCode(`});`); ctx.pushCode( `${varName} = ${tempVar}.result; offset = ${tempVar}.offset;` ); if (type !== this.alias) ctx.addReference(type); } } else if (type instanceof Parser) { ctx.pushPath(varName); type.generate(ctx); ctx.popPath(varName); } } private generateChoice(ctx: Context) { const tag = ctx.generateOption(this.options.tag!); const nestVar = ctx.generateVariable(this.varName); if (this.varName) { ctx.pushCode(`${nestVar} = {};`); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); } } ctx.pushCode(`switch(${tag}) {`); for (const tagString in this.options.choices) { const tag = parseInt(tagString, 10); const type = this.options.choices[tag]; ctx.pushCode(`case ${tag}:`); this.generateChoiceCase(ctx, this.varName, type); ctx.pushCode("break;"); } ctx.pushCode("default:"); if (this.options.defaultChoice) { this.generateChoiceCase(ctx, this.varName, this.options.defaultChoice); } else { ctx.generateError(`"Met undefined tag value " + ${tag} + " at choice"`); } ctx.pushCode("}"); if (this.varName && ctx.useContextVariables) { ctx.pushCode(`delete ${nestVar}.$parent;`); ctx.pushCode(`delete ${nestVar}.$root;`); } } private generateNest(ctx: Context) { const nestVar = ctx.generateVariable(this.varName); if (this.options.type instanceof Parser) { if (this.varName) { ctx.pushCode(`${nestVar} = {};`); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); } } ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); if (this.varName && ctx.useContextVariables) { if (ctx.useContextVariables) { ctx.pushCode(`delete ${nestVar}.$parent;`); ctx.pushCode(`delete ${nestVar}.$root;`); } } } else if (aliasRegistry.has(this.options.type!)) { const tempVar = ctx.generateTmpVariable(); ctx.pushCode( `var ${tempVar} = ${FUNCTION_PREFIX + this.options.type}(offset, {` ); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`$parent: ${parentVar},`); ctx.pushCode(`$root: ${parentVar}.$root,`); } ctx.pushCode(`});`); ctx.pushCode( `${nestVar} = ${tempVar}.result; offset = ${tempVar}.offset;` ); if (this.options.type !== this.alias) { ctx.addReference(this.options.type!); } } } private generateWrapper(ctx: Context) { const wrapperVar = ctx.generateVariable(this.varName); const wrappedBuf = ctx.generateTmpVariable(); if (typeof this.options.readUntil === "function") { const pred = this.options.readUntil; const start = ctx.generateTmpVariable(); const cur = ctx.generateTmpVariable(); ctx.pushCode(`var ${start} = offset;`); ctx.pushCode(`var ${cur} = 0;`); ctx.pushCode(`while (offset < buffer.length) {`); ctx.pushCode(`${cur} = dataView.getUint8(offset);`); const func = ctx.addImport(pred); ctx.pushCode( `if (${func}.call(${ctx.generateVariable()}, ${cur}, buffer.subarray(offset))) break;` ); ctx.pushCode(`offset += 1;`); ctx.pushCode(`}`); ctx.pushCode(`${wrappedBuf} = buffer.subarray(${start}, offset);`); } else if (this.options.readUntil === "eof") { ctx.pushCode(`${wrappedBuf} = buffer.subarray(offset);`); } else { const len = ctx.generateOption(this.options.length!); ctx.pushCode(`${wrappedBuf} = buffer.subarray(offset, offset + ${len});`); ctx.pushCode(`offset += ${len};`); } if (this.options.clone) { ctx.pushCode(`${wrappedBuf} = buffer.constructor.from(${wrappedBuf});`); } const tempBuf = ctx.generateTmpVariable(); const tempOff = ctx.generateTmpVariable(); const tempView = ctx.generateTmpVariable(); const func = ctx.addImport(this.options.wrapper); ctx.pushCode( `${wrappedBuf} = ${func}.call(this, ${wrappedBuf}).subarray(0);` ); ctx.pushCode(`var ${tempBuf} = buffer;`); ctx.pushCode(`var ${tempOff} = offset;`); ctx.pushCode(`var ${tempView} = dataView;`); ctx.pushCode(`buffer = ${wrappedBuf};`); ctx.pushCode(`offset = 0;`); ctx.pushCode( `dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);` ); if (this.options.type instanceof Parser) { if (this.varName) { ctx.pushCode(`${wrapperVar} = {};`); } ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); } else if (aliasRegistry.has(this.options.type!)) { const tempVar = ctx.generateTmpVariable(); ctx.pushCode( `var ${tempVar} = ${FUNCTION_PREFIX + this.options.type}(0);` ); ctx.pushCode(`${wrapperVar} = ${tempVar}.result;`); if (this.options.type !== this.alias) { ctx.addReference(this.options.type!); } } ctx.pushCode(`buffer = ${tempBuf};`); ctx.pushCode(`dataView = ${tempView};`); ctx.pushCode(`offset = ${tempOff};`); } private generateFormatter( ctx: Context, varName: string, formatter: Function ) { if (typeof formatter === "function") { const func = ctx.addImport(formatter); ctx.pushCode( `${varName} = ${func}.call(${ctx.generateVariable()}, ${varName});` ); } } private generatePointer(ctx: Context) { const type = this.options.type; const offset = ctx.generateOption(this.options.offset!); const tempVar = ctx.generateTmpVariable(); const nestVar = ctx.generateVariable(this.varName); // Save current offset ctx.pushCode(`var ${tempVar} = offset;`); // Move offset ctx.pushCode(`offset = ${offset};`); if (this.options.type instanceof Parser) { ctx.pushCode(`${nestVar} = {};`); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); } ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); if (ctx.useContextVariables) { ctx.pushCode(`delete ${nestVar}.$parent;`); ctx.pushCode(`delete ${nestVar}.$root;`); } } else if (aliasRegistry.has(this.options.type!)) { const tempVar = ctx.generateTmpVariable(); ctx.pushCode( `var ${tempVar} = ${FUNCTION_PREFIX + this.options.type}(offset, {` ); if (ctx.useContextVariables) { const parentVar = ctx.generateVariable(); ctx.pushCode(`$parent: ${parentVar},`); ctx.pushCode(`$root: ${parentVar}.$root,`); } ctx.pushCode(`});`); ctx.pushCode( `${nestVar} = ${tempVar}.result; offset = ${tempVar}.offset;` ); if (this.options.type !== this.alias) { 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]; ctx.pushCode( `${nestVar} = dataView.get${typeName}(offset, ${littleEndian});` ); ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]};`); } // Restore offset ctx.pushCode(`offset = ${tempVar};`); } private generateSaveOffset(ctx: Context) { const varName = ctx.generateVariable(this.varName); ctx.pushCode(`${varName} = offset`); } }