UNPKG

opensistemas-parser

Version:

Parser for binaries in Javascript based in the binary-parser module from npm

695 lines (590 loc) 23.1 kB
//======================================================================================== // Globals //======================================================================================== var Context = require("./context").Context; var PRIMITIVE_TYPES = { 'UInt8' : 1, 'UInt16LE' : 2, 'UInt16BE' : 2, 'UInt32LE' : 4, 'UInt32BE' : 4, 'Int8' : 1, 'Int16LE' : 2, 'Int16BE' : 2, 'Int32LE' : 4, 'Int32BE' : 4, 'FloatLE' : 4, 'FloatBE' : 4, 'DoubleLE' : 8, 'DoubleBE' : 8 }; var SPECIAL_TYPES = { 'String' : null, 'Buffer' : null, 'Array' : null, 'Skip' : null, 'Choice' : null, 'Nest' : null, 'Bit' : null }; var aliasRegistry = {}; var FUNCTION_PREFIX = '___parser_'; var BIT_RANGE = []; (function() { var i; for (i = 1; i <= 32; i++) { BIT_RANGE.push(i); } })(); // Converts Parser's method names to internal type names var NAME_MAP = {}; Object.keys(PRIMITIVE_TYPES) .concat(Object.keys(SPECIAL_TYPES)) .forEach(function(type) { NAME_MAP[type.toLowerCase()] = type; }); //======================================================================================== // class Parser //======================================================================================== //---------------------------------------------------------------------------------------- // constructor //---------------------------------------------------------------------------------------- var Parser = function() { this.varName = ''; this.type = ''; this.options = {}; this.next = null; this.head = null; this.compiled = null; this.endian = 'be'; this.constructorFn = null; this.alias = null; }; //---------------------------------------------------------------------------------------- // public methods //---------------------------------------------------------------------------------------- Parser.start = function() { return new Parser(); }; Object.keys(PRIMITIVE_TYPES) .forEach(function(type) { Parser.prototype[type.toLowerCase()] = function(varName, options) { return this.setNextParser(type.toLowerCase(), varName, options); }; var typeWithoutEndian = type.replace(/BE|LE/, '').toLowerCase(); if (!(typeWithoutEndian in Parser.prototype)) { Parser.prototype[typeWithoutEndian] = function(varName, options) { return this[typeWithoutEndian + this.endian](varName, options); }; } }); BIT_RANGE.forEach(function(i) { Parser.prototype['bit' + i.toString()] = function(varName, options) { if (!options) { options = {}; } options.length = i; return this.setNextParser('bit', varName, options); }; }); Parser.prototype.namely = function(alias) { aliasRegistry[alias] = this; this.alias = alias; return this; } Parser.prototype.skip = function(length, options) { if (options && options.assert) { throw new Error('assert option on skip is not allowed.'); } return this.setNextParser('skip', '', {length: length}); }; Parser.prototype.string = function(varName, options) { if (!options.zeroTerminated && !options.length && !options.greedy) { throw new Error('Neither length, zeroTerminated, nor greedy is 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 defined.'); } options.encoding = options.encoding || 'utf8'; return this.setNextParser('string', varName, options); }; Parser.prototype.buffer = function(varName, options) { if (!options.length && !options.readUntil) { throw new Error('Length nor readUntil is defined in buffer parser'); } return this.setNextParser('buffer', varName, options); }; Parser.prototype.array = function(varName, options) { if (!options.readUntil && !options.length && !options.lengthInBytes) { throw new Error('Length option of array is not defined.'); } if (!options.type) { throw new Error('Type option of array is not defined.'); } if ((typeof options.type === 'string') && !aliasRegistry[options.type] && Object.keys(PRIMITIVE_TYPES).indexOf(NAME_MAP[options.type]) < 0) { throw new Error('Specified primitive type "' + options.type + '" is not supported.'); } return this.setNextParser('array', varName, options); }; Parser.prototype.choice = function(varName, options) { if (!options.tag) { throw new Error('Tag option of array is not defined.'); } if (!options.choices) { throw new Error('Choices option of array is not defined.'); } Object.keys(options.choices).forEach(function(key) { if (isNaN(parseInt(key, 10))) { throw new Error('Key of choices must be a number.'); } if (!options.choices[key]) { throw new Error('Choice Case ' + key + ' of ' + varName + ' is not valid.'); } if ((typeof options.choices[key] === 'string') && !aliasRegistry[options.choices[key]] && (Object.keys(PRIMITIVE_TYPES).indexOf(NAME_MAP[options.choices[key]]) < 0)) { throw new Error('Specified primitive type "' + options.choices[key] + '" is not supported.'); } }, this); return this.setNextParser('choice', varName, options); }; Parser.prototype.nest = function(varName, options) { if (!options.type) { throw new Error('Type option of nest is not defined.'); } if (!(options.type instanceof Parser) && !aliasRegistry[options.type]) { throw new Error('Type option of nest must be a Parser object.'); } return this.setNextParser('nest', varName, options); }; Parser.prototype.endianess = function(endianess) { switch (endianess.toLowerCase()) { case 'little': this.endian = 'le'; break; case 'big': this.endian = 'be'; break; default: throw new Error('Invalid endianess: ' + endianess); } return this; }; Parser.prototype.create = function(constructorFn) { if (!(constructorFn instanceof Function)) { throw new Error('Constructor must be a Function object.'); } this.constructorFn = constructorFn; return this; }; Parser.prototype.getCode = function() { var ctx = new Context(); ctx.pushCode('if (!Buffer.isBuffer(buffer)) {'); ctx.generateError('"argument buffer is not a Buffer object"'); ctx.pushCode('}'); if (!this.alias) { this.addRawCode(ctx); } else { this.addAliasedCode(ctx); } if (this.alias) { ctx.pushCode('return {0}(0).result;', FUNCTION_PREFIX + this.alias); } else { ctx.pushCode('return vars;'); } return ctx.code; }; Parser.prototype.addRawCode = function(ctx) { ctx.pushCode('var offset = 0;'); if (this.constructorFn) { ctx.pushCode('var vars = new constructorFn();'); } else { ctx.pushCode('var vars = {};'); } this.generate(ctx); this.resolveReferences(ctx); ctx.pushCode('return vars;'); }; Parser.prototype.addAliasedCode = function(ctx) { ctx.pushCode('function {0}(offset) {', FUNCTION_PREFIX + this.alias); if (this.constructorFn) { ctx.pushCode('var vars = new constructorFn();'); } else { ctx.pushCode('var vars = {};'); } this.generate(ctx); ctx.markResolved(this.alias); this.resolveReferences(ctx); ctx.pushCode('return { offset: offset, result: vars };'); ctx.pushCode('}'); return ctx; }; Parser.prototype.resolveReferences = function(ctx) { var references = ctx.getUnresolvedReferences(); ctx.markRequested(references); references.forEach(function(alias) { var parser = aliasRegistry[alias]; parser.addAliasedCode(ctx); }); }; Parser.prototype.compile = function() { this.compiled = new Function('buffer', 'callback', 'constructorFn', this.getCode()); }; Parser.prototype.sizeOf = function() { var size = NaN; if (Object.keys(PRIMITIVE_TYPES).indexOf(this.type) >= 0) { size = PRIMITIVE_TYPES[this.type]; // 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') { var elementSize = NaN; if (typeof this.options.type === 'string'){ elementSize = PRIMITIVE_TYPES[NAME_MAP[this.options.type]]; } 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 === 'Skip') { size = this.options.length; // if this is a nested parser } else if (this.type === 'Nest') { size = this.options.type.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 Parser.prototype.parse = function(buffer, callback) { if (!this.compiled) { this.compile(); } return this.compiled(buffer, callback, this.constructorFn); }; //---------------------------------------------------------------------------------------- // private methods //---------------------------------------------------------------------------------------- Parser.prototype.setNextParser = function(type, varName, options) { var parser = new Parser(); parser.type = NAME_MAP[type]; parser.varName = varName; parser.options = options || parser.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 Parser.prototype.generate = function(ctx) { if (this.type) { this['generate' + this.type](ctx); this.generateAssert(ctx); } var varName = ctx.generateVariable(this.varName); if (this.options.formatter) { this.generateFormatter(ctx, varName, this.options.formatter); } return this.generateNext(ctx); }; Parser.prototype.generateAssert = function(ctx) { if (!this.options.assert) { return; } var varName = ctx.generateVariable(this.varName); switch (typeof this.options.assert) { case 'function': ctx.pushCode('if (!({0}).call(vars, {1})) {', this.options.assert, varName); break; case 'number': ctx.pushCode('if ({0} !== {1}) {', this.options.assert, varName); break; case 'string': ctx.pushCode('if ("{0}" !== {1}) {', this.options.assert, varName); break; default: throw new Error('Assert option supports only strings, numbers and assert functions.'); } ctx.generateError('"Assert error: {0} is " + {0}', varName); ctx.pushCode('}'); }; // Recursively call code generators and append results Parser.prototype.generateNext = function(ctx) { if (this.next) { ctx = this.next.generate(ctx); } return ctx; }; Object.keys(PRIMITIVE_TYPES).forEach(function(type) { Parser.prototype['generate' + type] = function(ctx) { ctx.pushCode('{0} = buffer.read{1}(offset);', ctx.generateVariable(this.varName), type); ctx.pushCode('offset += {0};', PRIMITIVE_TYPES[type]); }; }); Parser.prototype.generateBit = function(ctx) { // TODO find better method to handle nested bit fields var parser = JSON.parse(JSON.stringify(this)); parser.varName = ctx.generateVariable(parser.varName); ctx.bitFields.push(parser); if (!this.next || (this.next && ['Bit', 'Nest'].indexOf(this.next.type) < 0)) { var sum = 0; ctx.bitFields.forEach(function(parser) { sum += parser.options.length; }); var val = ctx.generateTmpVariable(); if (sum <= 8) { ctx.pushCode('var {0} = buffer.readUInt8(offset);', val); sum = 8; } else if (sum <= 16) { ctx.pushCode('var {0} = buffer.readUInt16BE(offset);', val); sum = 16; } else if (sum <= 24) { var val1 = ctx.generateTmpVariable(); var val2 = ctx.generateTmpVariable(); ctx.pushCode('var {0} = buffer.readUInt16BE(offset);', val1); ctx.pushCode('var {0} = buffer.readUInt8(offset + 2);', val2); ctx.pushCode('var {2} = ({0} << 8) | {1};', val1, val2, val); sum = 24; } else if (sum <= 32) { ctx.pushCode('var {0} = buffer.readUInt32BE(offset);', val); sum = 32; } else if (sum <= 40) { var val1 = ctx.generateTmpVariable(); var val2 = ctx.generateTmpVariable(); ctx.pushCode('var {0} = buffer.readUInt32BE(offset);', val1); ctx.pushCode('var {0} = buffer.readUInt8(offset + 4);', val2); ctx.pushCode('var {2} = ({0} << 8) | {1};', val1, val2, val); sum = 40; } else if (sum <= 48) { var val1 = ctx.generateTmpVariable(); var val2 = ctx.generateTmpVariable(); ctx.pushCode('var {0} = buffer.readUInt32BE(offset);', val1); ctx.pushCode('var {0} = buffer.readUInt16BE(offset + 4);', val2); ctx.pushCode('var {2} = ({0} << 16) | {1};', val1, val2, val); sum = 48; } else if (sum <= 56) { var val1 = ctx.generateTmpVariable(); var val2 = ctx.generateTmpVariable(); var val3 = ctx.generateTmpVariable(); var val4 = ctx.generateTmpVariable(); ctx.pushCode('var {0} = buffer.readUInt32BE(offset);', val1); ctx.pushCode('var {0} = buffer.readUInt16BE(offset + 4);', val2); ctx.pushCode('var {2} = ({0} << 16) | {1};', val1, val2, val3); ctx.pushCode('var {0} = buffer.readUInt8(offset + 6);', val4); ctx.pushCode('var {2} = ({0} << 8) | {1}', val3, val4, val); sum = 56; }else if (sum <= 64){ ctx.pushCode('var {0} = buffer.readDoubleBE(offset);', val); sum = 64; } else { throw new Error('Currently, bit field sequence longer than 8-bytes is not supported.'); } ctx.pushCode('offset += {0};', sum / 8); var bitOffset = 0; var isBigEndian = this.endian === 'be'; ctx.bitFields.forEach(function(parser) { ctx.pushCode('{0} = {1} >> {2} & {3};', parser.varName, val, isBigEndian ? sum - bitOffset - parser.options.length : bitOffset, (1 << parser.options.length) - 1 ); bitOffset += parser.options.length; }); ctx.bitFields = []; } }; Parser.prototype.generateSkip = function(ctx) { var length = ctx.generateOption(this.options.length); ctx.pushCode('offset += {0};', length); }; Parser.prototype.generateString = function(ctx) { var name = ctx.generateVariable(this.varName); var start = ctx.generateTmpVariable(); if (this.options.length && this.options.zeroTerminated) { ctx.pushCode('var {0} = offset;', start); ctx.pushCode('while(buffer.readUInt8(offset++) !== 0 && offset - {0} < {1});', start, this.options.length ); ctx.pushCode('{0} = buffer.toString(\'{1}\', {2}, offset - {2} < {3} ? offset - 1 : offset);', name, this.options.encoding, start, this.options.length ); } else if(this.options.length) { ctx.pushCode('{0} = buffer.toString(\'{1}\', offset, offset + {2});', name, this.options.encoding, ctx.generateOption(this.options.length) ); ctx.pushCode('offset += {0};', ctx.generateOption(this.options.length)); } else if (this.options.zeroTerminated) { ctx.pushCode('var {0} = offset;', start); ctx.pushCode('while(buffer.readUInt8(offset++) !== 0);'); ctx.pushCode('{0} = buffer.toString(\'{1}\', {2}, offset - 1);', name, this.options.encoding, start ); } else if (this.options.greedy) { ctx.pushCode('var {0} = offset;', start); ctx.pushCode('while(buffer.length > offset++);'); ctx.pushCode('{0} = buffer.toString(\'{1}\', {2}, offset);', name, this.options.encoding, start ); } if(this.options.stripNull) { ctx.pushCode('{0} = {0}.replace(/\\x00+$/g, \'\')', name); } }; Parser.prototype.generateBuffer = function(ctx) { if (this.options.readUntil === 'eof') { ctx.pushCode('{0} = buffer.slice(offset);', ctx.generateVariable(this.varName) ); } else { ctx.pushCode('{0} = buffer.slice(offset, offset + {1});', ctx.generateVariable(this.varName), ctx.generateOption(this.options.length) ); ctx.pushCode('offset += {0};', ctx.generateOption(this.options.length)); } if (this.options.clone) { var buf = ctx.generateTmpVariable(); ctx.pushCode('var {0} = new Buffer({1}.length);', buf, ctx.generateVariable(this.varName)); ctx.pushCode('{0}.copy({1});', ctx.generateVariable(this.varName), buf); ctx.pushCode('{0} = {1}', ctx.generateVariable(this.varName), buf); } }; Parser.prototype.generateArray = function(ctx) { var length = ctx.generateOption(this.options.length); var lengthInBytes = ctx.generateOption(this.options.lengthInBytes); var type = this.options.type; var counter = ctx.generateTmpVariable(); var lhs = ctx.generateVariable(this.varName); var item = ctx.generateTmpVariable(); var key = this.options.key; var isHash = typeof key === 'string'; if (isHash) { ctx.pushCode('{0} = {};', lhs); } else { ctx.pushCode('{0} = [];', lhs); } if (typeof this.options.readUntil === 'function') { ctx.pushCode('do {'); } else if (this.options.readUntil === 'eof') { ctx.pushCode('for (var {0} = 0; offset < buffer.length; {0}++) {', counter); } else if (lengthInBytes !== undefined) { ctx.pushCode('for (var {0} = offset; offset - {0} < {1}; ) {', counter, lengthInBytes); } else { ctx.pushCode('for (var {0} = 0; {0} < {1}; {0}++) {', counter, length); } if (typeof type === 'string') { if (!aliasRegistry[type]) { ctx.pushCode('var {0} = buffer.read{1}(offset);', item, NAME_MAP[type]); ctx.pushCode('offset += {0};', PRIMITIVE_TYPES[NAME_MAP[type]]); } else { var tempVar = ctx.generateTmpVariable(); ctx.pushCode('var {0} = {1}(offset);', tempVar, FUNCTION_PREFIX + type); ctx.pushCode('var {0} = {1}.result; offset = {1}.offset;', item, tempVar); if (type !== this.alias) ctx.addReference(type); } } else if (type instanceof Parser) { ctx.pushCode('var {0} = {};', item); ctx.pushScope(item); type.generate(ctx); ctx.popScope(); } if (isHash) { ctx.pushCode('{0}[{2}.{1}] = {2};', lhs, key, item); } else { ctx.pushCode('{0}.push({1});', lhs, item); } ctx.pushCode('}'); if (typeof this.options.readUntil === 'function') { ctx.pushCode(' while (!({0}).call(this, {1}, buffer.slice(offset)));', this.options.readUntil, item); } }; Parser.prototype.generateChoiceCase = function(ctx, varName, type) { if (typeof type === 'string') { if (!aliasRegistry[type]) { ctx.pushCode('{0} = buffer.read{1}(offset);', ctx.generateVariable(this.varName), NAME_MAP[type]); ctx.pushCode('offset += {0};', PRIMITIVE_TYPES[NAME_MAP[type]]); } else { var tempVar = ctx.generateTmpVariable(); ctx.pushCode('var {0} = {1}(offset);', tempVar, FUNCTION_PREFIX + type); ctx.pushCode('{0} = {1}.result; offset = {1}.offset;', ctx.generateVariable(this.varName), tempVar); if (type !== this.alias) ctx.addReference(type); } } else if (type instanceof Parser) { ctx.pushPath(varName); type.generate(ctx); ctx.popPath(varName); } }; Parser.prototype.generateChoice = function(ctx) { var tag = ctx.generateOption(this.options.tag); if (this.varName) { ctx.pushCode('{0} = {};', ctx.generateVariable(this.varName)); } ctx.pushCode('switch({0}) {', tag); Object.keys(this.options.choices).forEach(function(tag) { var type = this.options.choices[tag]; ctx.pushCode('case {0}:', tag); this.generateChoiceCase(ctx, this.varName, type); ctx.pushCode('break;'); }, this); ctx.pushCode('default:'); if (this.options.defaultChoice) { this.generateChoiceCase(ctx, this.varName, this.options.defaultChoice); } else { ctx.generateError('"Met undefined tag value " + {0} + " at choice"', tag); } ctx.pushCode('}'); }; Parser.prototype.generateNest = function(ctx) { var nestVar = ctx.generateVariable(this.varName); if (this.options.type instanceof Parser) { ctx.pushCode('{0} = {};', nestVar); ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); } else if (aliasRegistry[this.options.type]) { var tempVar = ctx.generateTmpVariable(); ctx.pushCode('var {0} = {1}(offset);', tempVar, FUNCTION_PREFIX + this.options.type); ctx.pushCode('{0} = {1}.result; offset = {1}.offset;', nestVar, tempVar); if (this.options.type !== this.alias) ctx.addReference(this.options.type); } }; Parser.prototype.generateFormatter = function(ctx, varName, formatter) { if (typeof formatter === 'function') { ctx.pushCode('{0} = ({1}).call(this, {0});', varName, formatter); } }; Parser.prototype.isInteger = function() { return !!this.type.match(/U?Int[8|16|32][BE|LE]?|Bit\d+/); }; //======================================================================================== // Exports //======================================================================================== exports.Parser = Parser;