/* * rm - Feb 2011 * ctype.js * * This module provides a simple abstraction towards reading and writing * different types of binary data. It is designed to use ctio.js and provide a * richer and more expressive API on top of it. * * By default we support the following as built in basic types: * int8_t * int16_t * int32_t * uint8_t * uint16_t * uint32_t * uint64_t * float * double * char * char[] * * Each type is returned as a Number, with the exception of char and char[] * which are returned as Node Buffers. A char is considered a uint8_t. * * Requests to read and write data are specified as an array of JSON objects. * This is also the same way that one declares structs. Even if just a single * value is requested, it must be done as a struct. The array order determines * the order that we try and read values. Each entry has the following format * with values marked with a * being optional. * * { key: { type: /type/, value*: /value/, offset*: /offset/ } * * If offset is defined, we lseek(offset, SEEK_SET) before reading the next * value. Value is defined when we're writing out data, otherwise it's ignored. * */ var mod_ctf = require('./ctf.js'); var mod_ctio = require('./ctio.js'); var mod_assert = require('assert'); /* * This is the set of basic types that we support. * * read The function to call to read in a value from a buffer * * write The function to call to write a value to a buffer * */ var deftypes = { 'uint8_t': { read: ctReadUint8, write: ctWriteUint8 }, 'uint16_t': { read: ctReadUint16, write: ctWriteUint16 }, 'uint32_t': { read: ctReadUint32, write: ctWriteUint32 }, 'uint64_t': { read: ctReadUint64, write: ctWriteUint64 }, 'int8_t': { read: ctReadSint8, write: ctWriteSint8 }, 'int16_t': { read: ctReadSint16, write: ctWriteSint16 }, 'int32_t': { read: ctReadSint32, write: ctWriteSint32 }, 'int64_t': { read: ctReadSint64, write: ctWriteSint64 }, 'float': { read: ctReadFloat, write: ctWriteFloat }, 'double': { read: ctReadDouble, write: ctWriteDouble }, 'char': { read: ctReadChar, write: ctWriteChar }, 'char[]': { read: ctReadCharArray, write: ctWriteCharArray } }; /* * The following are wrappers around the CType IO low level API. They encode * knowledge about the size and return something in the expected format. */ function ctReadUint8(endian, buffer, offset) { var val = mod_ctio.ruint8(buffer, endian, offset); return ({ value: val, size: 1 }); } function ctReadUint16(endian, buffer, offset) { var val = mod_ctio.ruint16(buffer, endian, offset); return ({ value: val, size: 2 }); } function ctReadUint32(endian, buffer, offset) { var val = mod_ctio.ruint32(buffer, endian, offset); return ({ value: val, size: 4 }); } function ctReadUint64(endian, buffer, offset) { var val = mod_ctio.ruint64(buffer, endian, offset); return ({ value: val, size: 8 }); } function ctReadSint8(endian, buffer, offset) { var val = mod_ctio.rsint8(buffer, endian, offset); return ({ value: val, size: 1 }); } function ctReadSint16(endian, buffer, offset) { var val = mod_ctio.rsint16(buffer, endian, offset); return ({ value: val, size: 2 }); } function ctReadSint32(endian, buffer, offset) { var val = mod_ctio.rsint32(buffer, endian, offset); return ({ value: val, size: 4 }); } function ctReadSint64(endian, buffer, offset) { var val = mod_ctio.rsint64(buffer, endian, offset); return ({ value: val, size: 8 }); } function ctReadFloat(endian, buffer, offset) { var val = mod_ctio.rfloat(buffer, endian, offset); return ({ value: val, size: 4 }); } function ctReadDouble(endian, buffer, offset) { var val = mod_ctio.rdouble(buffer, endian, offset); return ({ value: val, size: 8 }); } /* * Reads a single character into a node buffer */ function ctReadChar(endian, buffer, offset) { var res = new Buffer(1); res[0] = mod_ctio.ruint8(buffer, endian, offset); return ({ value: res, size: 1 }); } function ctReadCharArray(length, endian, buffer, offset) { var ii; var res = new Buffer(length); for (ii = 0; ii < length; ii++) res[ii] = mod_ctio.ruint8(buffer, endian, offset + ii); return ({ value: res, size: length }); } function ctWriteUint8(value, endian, buffer, offset) { mod_ctio.wuint8(value, endian, buffer, offset); return (1); } function ctWriteUint16(value, endian, buffer, offset) { mod_ctio.wuint16(value, endian, buffer, offset); return (2); } function ctWriteUint32(value, endian, buffer, offset) { mod_ctio.wuint32(value, endian, buffer, offset); return (4); } function ctWriteUint64(value, endian, buffer, offset) { mod_ctio.wuint64(value, endian, buffer, offset); return (8); } function ctWriteSint8(value, endian, buffer, offset) { mod_ctio.wsint8(value, endian, buffer, offset); return (1); } function ctWriteSint16(value, endian, buffer, offset) { mod_ctio.wsint16(value, endian, buffer, offset); return (2); } function ctWriteSint32(value, endian, buffer, offset) { mod_ctio.wsint32(value, endian, buffer, offset); return (4); } function ctWriteSint64(value, endian, buffer, offset) { mod_ctio.wsint64(value, endian, buffer, offset); return (8); } function ctWriteFloat(value, endian, buffer, offset) { mod_ctio.wfloat(value, endian, buffer, offset); return (4); } function ctWriteDouble(value, endian, buffer, offset) { mod_ctio.wdouble(value, endian, buffer, offset); return (8); } /* * Writes a single character into a node buffer */ function ctWriteChar(value, endian, buffer, offset) { if (!(value instanceof Buffer)) throw (new Error('Input must be a buffer')); mod_ctio.ruint8(value[0], endian, buffer, offset); return (1); } /* * We're going to write 0s into the buffer if the string is shorter than the * length of the array. */ function ctWriteCharArray(value, length, endian, buffer, offset) { var ii; if (!(value instanceof Buffer)) throw (new Error('Input must be a buffer')); if (value.length > length) throw (new Error('value length greater than array length')); for (ii = 0; ii < value.length && ii < length; ii++) mod_ctio.wuint8(value[ii], endian, buffer, offset + ii); for (; ii < length; ii++) mod_ctio.wuint8(0, endian, offset + ii); return (length); } /* * Each parser has their own set of types. We want to make sure that they each * get their own copy as they may need to modify it. */ function ctGetBasicTypes() { var ret = {}; var key; for (key in deftypes) ret[key] = deftypes[key]; return (ret); } /* * Given a string in the form of type[length] we want to split this into an * object that extracts that information. We want to note that we could possibly * have nested arrays so this should only check the furthest one. It may also be * the case that we have no [] pieces, in which case we just return the current * type. */ function ctParseType(str) { var begInd, endInd; var type, len; if (typeof (str) != 'string') throw (new Error('type must be a Javascript string')); endInd = str.lastIndexOf(']'); if (endInd == -1) { if (str.lastIndexOf('[') != -1) throw (new Error('found invalid type with \'[\' but ' + 'no corresponding \']\'')); return ({ type: str }); } begInd = str.lastIndexOf('['); if (begInd == -1) throw (new Error('found invalid type with \']\' but ' + 'no corresponding \'[\'')); if (begInd >= endInd) throw (new Error('malformed type, \']\' appears before \'[\'')); type = str.substring(0, begInd); len = str.substring(begInd + 1, endInd); return ({ type: type, len: len }); } /* * Given a request validate that all of the fields for it are valid and make * sense. This includes verifying the following notions: * - Each type requested is present in types * - Only allow a name for a field to be specified once * - If an array is specified, validate that the requested field exists and * comes before it. * - If fields is defined, check that each entry has the occurrence of field */ function ctCheckReq(def, types, fields) { var ii, jj; var req, keys, key; var found = {}; if (!(def instanceof Array)) throw (new Error('definition is not an array')); if (def.length === 0) throw (new Error('definition must have at least one element')); for (ii = 0; ii < def.length; ii++) { req = def[ii]; if (!(req instanceof Object)) throw (new Error('definition must be an array of' + 'objects')); keys = Object.keys(req); if (keys.length != 1) throw (new Error('definition entry must only have ' + 'one key')); if (keys[0] in found) throw (new Error('Specified name already ' + 'specified: ' + keys[0])); if (!('type' in req[keys[0]])) throw (new Error('missing required type definition')); key = ctParseType(req[keys[0]]['type']); /* * We may have nested arrays, we need to check the validity of * the types until the len field is undefined in key. However, * each time len is defined we need to verify it is either an * integer or corresponds to an already seen key. */ while (key['len'] !== undefined) { if (isNaN(parseInt(key['len'], 10))) { if (!(key['len'] in found)) throw (new Error('Given an array ' + 'length without a matching type')); } key = ctParseType(key['type']); } /* Now we can validate if the type is valid */ if (!(key['type'] in types)) throw (new Error('type not found or typdefed: ' + key['type'])); /* Check for any required fields */ if (fields !== undefined) { for (jj = 0; jj < fields.length; jj++) { if (!(fields[jj] in req[keys[0]])) throw (new Error('Missing required ' + 'field: ' + fields[jj])); } } found[keys[0]] = true; } } /* * Create a new instance of the parser. Each parser has its own store of * typedefs and endianness. Conf is an object with the following required * values: * * endian Either 'big' or 'little' do determine the endianness we * want to read from or write to. * * And the following optional values: * * char-type Valid options here are uint8 and int8. If uint8 is * specified this changes the default behavior of a single * char from being a buffer of a single character to being * a uint8_t. If int8, it becomes an int8_t instead. */ function CTypeParser(conf) { if (!conf) throw (new Error('missing required argument')); if (!('endian' in conf)) throw (new Error('missing required endian value')); if (conf['endian'] != 'big' && conf['endian'] != 'little') throw (new Error('Invalid endian type')); if ('char-type' in conf && (conf['char-type'] != 'uint8' && conf['char-type'] != 'int8')) throw (new Error('invalid option for char-type: ' + conf['char-type'])); this.endian = conf['endian']; this.types = ctGetBasicTypes(); /* * There may be a more graceful way to do this, but this will have to * serve. */ if ('char-type' in conf && conf['char-type'] == 'uint8') this.types['char'] = this.types['uint8_t']; if ('char-type' in conf && conf['char-type'] == 'int8') this.types['char'] = this.types['int8_t']; } /* * Sets the current endian value for the Parser. If the value is not valid, * throws an Error. * * endian Either 'big' or 'little' do determine the endianness we * want to read from or write to. * */ CTypeParser.prototype.setEndian = function (endian) { if (endian != 'big' && endian != 'little') throw (new Error('invalid endian type, must be big or ' + 'little')); this.endian = endian; }; /* * Returns the current value of the endian value for the parser. */ CTypeParser.prototype.getEndian = function () { return (this.endian); }; /* * A user has requested to add a type, let us honor their request. Yet, if their * request doth spurn us, send them unto the Hells which Dante describes. * * name The string for the type definition we're adding * * value Either a string that is a type/array name or an object * that describes a struct. */ CTypeParser.prototype.typedef = function (name, value) { var type; if (name === undefined) throw (new (Error('missing required typedef argument: name'))); if (value === undefined) throw (new (Error('missing required typedef argument: value'))); if (typeof (name) != 'string') throw (new (Error('the name of a type must be a string'))); type = ctParseType(name); if (type['len'] !== undefined) throw (new Error('Cannot have an array in the typedef name')); if (name in this.types) throw (new Error('typedef name already present: ' + name)); if (typeof (value) != 'string' && !(value instanceof Array)) throw (new Error('typedef value must either be a string or ' + 'struct')); if (typeof (value) == 'string') { type = ctParseType(value); if (type['len'] !== undefined) { if (isNaN(parseInt(type['len'], 10))) throw (new (Error('typedef value must use ' + 'fixed size array when outside of a ' + 'struct'))); } this.types[name] = value; } else { /* We have a struct, validate it */ ctCheckReq(value, this.types); this.types[name] = value; } }; /* * Include all of the typedefs, but none of the built in types. This should be * treated as read-only. */ CTypeParser.prototype.lstypes = function () { var key; var ret = {}; for (key in this.types) { if (key in deftypes) continue; ret[key] = this.types[key]; } return (ret); }; /* * Given a type string that may have array types that aren't numbers, try and * fill them in from the values object. The object should be of the format where * indexing into it should return a number for that type. * * str The type string * * values An object that can be used to fulfill type information */ function ctResolveArray(str, values) { var ret = ''; var type = ctParseType(str); while (type['len'] !== undefined) { if (isNaN(parseInt(type['len'], 10))) { if (typeof (values[type['len']]) != 'number') throw (new Error('cannot sawp in non-number ' + 'for array value')); ret = '[' + values[type['len']] + ']' + ret; } else { ret = '[' + type['len'] + ']' + ret; } type = ctParseType(type['type']); } ret = type['type'] + ret; return (ret); } /* * [private] Either the typedef resolves to another type string or to a struct. * If it resolves to a struct, we just pass it off to read struct. If not, we * can just pass it off to read entry. */ CTypeParser.prototype.resolveTypedef = function (type, dispatch, buffer, offset, value) { var pt; mod_assert.ok(type in this.types); if (typeof (this.types[type]) == 'string') { pt = ctParseType(this.types[type]); if (dispatch == 'read') return (this.readEntry(pt, buffer, offset)); else if (dispatch == 'write') return (this.writeEntry(value, pt, buffer, offset)); else throw (new Error('invalid dispatch type to ' + 'resolveTypedef')); } else { if (dispatch == 'read') return (this.readStruct(this.types[type], buffer, offset)); else if (dispatch == 'write') return (this.writeStruct(value, this.types[type], buffer, offset)); else throw (new Error('invalid dispatch type to ' + 'resolveTypedef')); } }; /* * [private] Try and read in the specific entry. */ CTypeParser.prototype.readEntry = function (type, buffer, offset) { var parse, len; /* * Because we want to special case char[]s this is unfortunately * a bit uglier than it really should be. We want to special * case char[]s so that we return a node buffer, thus they are a * first class type where as all other arrays just call into a * generic array routine which calls their data-specific routine * the specified number of times. * * The valid dispatch options we have are: * - Array and char => char[] handler * - Generic array handler * - Generic typedef handler * - Basic type handler */ if (type['len'] !== undefined) { len = parseInt(type['len'], 10); if (isNaN(len)) throw (new Error('somehow got a non-numeric length')); if (type['type'] == 'char') parse = this.types['char[]']['read'](len, this.endian, buffer, offset); else parse = this.readArray(type['type'], len, buffer, offset); } else { if (type['type'] in deftypes) parse = this.types[type['type']]['read'](this.endian, buffer, offset); else parse = this.resolveTypedef(type['type'], 'read', buffer, offset); } return (parse); }; /* * [private] Read an array of data */ CTypeParser.prototype.readArray = function (type, length, buffer, offset) { var ii, ent, pt; var baseOffset = offset; var ret = new Array(length); pt = ctParseType(type); for (ii = 0; ii < length; ii++) { ent = this.readEntry(pt, buffer, offset); offset += ent['size']; ret[ii] = ent['value']; } return ({ value: ret, size: offset - baseOffset }); }; /* * [private] Read a single struct in. */ CTypeParser.prototype.readStruct = function (def, buffer, offset) { var parse, ii, type, entry, key; var baseOffset = offset; var ret = {}; /* Walk it and handle doing what's necessary */ for (ii = 0; ii < def.length; ii++) { key = Object.keys(def[ii])[0]; entry = def[ii][key]; /* Resolve all array values */ type = ctParseType(ctResolveArray(entry['type'], ret)); if ('offset' in entry) offset = baseOffset + entry['offset']; parse = this.readEntry(type, buffer, offset); offset += parse['size']; ret[key] = parse['value']; } return ({ value: ret, size: (offset-baseOffset)}); }; /* * This is what we were born to do. We read the data from a buffer and return it * in an object whose keys match the values from the object. * * def The array definition of the data to read in * * buffer The buffer to read data from * * offset The offset to start writing to * * Returns an object where each key corresponds to an entry in def and the value * is the read value. */ CTypeParser.prototype.readData = function (def, buffer, offset) { /* Sanity check for arguments */ if (def === undefined) throw (new Error('missing definition for what we should be' + 'parsing')); if (buffer === undefined) throw (new Error('missing buffer for what we should be ' + 'parsing')); if (offset === undefined) throw (new Error('missing offset for what we should be ' + 'parsing')); /* Sanity check the object definition */ ctCheckReq(def, this.types); return (this.readStruct(def, buffer, offset)['value']); }; /* * [private] Write out an array of data */ CTypeParser.prototype.writeArray = function (value, type, length, buffer, offset) { var ii, pt; var baseOffset = offset; if (!(value instanceof Array)) throw (new Error('asked to write an array, but value is not ' + 'an array')); if (value.length != length) throw (new Error('asked to write array of length ' + length + ' but that does not match value length: ' + value.length)); pt = ctParseType(type); for (ii = 0; ii < length; ii++) offset += this.writeEntry(value[ii], pt, buffer, offset); return (offset - baseOffset); }; /* * [private] Write the specific entry */ CTypeParser.prototype.writeEntry = function (value, type, buffer, offset) { var len, ret; if (type['len'] !== undefined) { len = parseInt(type['len'], 10); if (isNaN(len)) throw (new Error('somehow got a non-numeric length')); if (type['type'] == 'char') ret = this.types['char[]']['write'](value, len, this.endian, buffer, offset); else ret = this.writeArray(value, type['type'], len, buffer, offset); } else { if (type['type'] in deftypes) ret = this.types[type['type']]['write'](value, this.endian, buffer, offset); else ret = this.resolveTypedef(type['type'], 'write', buffer, offset, value); } return (ret); }; /* * [private] Write a single struct out. */ CTypeParser.prototype.writeStruct = function (value, def, buffer, offset) { var ii, entry, type, key; var baseOffset = offset; var vals = {}; for (ii = 0; ii < def.length; ii++) { key = Object.keys(def[ii])[0]; entry = def[ii][key]; type = ctParseType(ctResolveArray(entry['type'], vals)); if ('offset' in entry) offset = baseOffset + entry['offset']; offset += this.writeEntry(value[ii], type, buffer, offset); /* Now that we've written it out, we can use it for arrays */ vals[key] = value[ii]; } return (offset); }; /* * Unfortunately, we're stuck with the sins of an initial poor design. Because * of that, we are going to have to support the old way of writing data via * writeData. There we insert the values that you want to write into the * definition. A little baroque. Internally, we use the new model. So we need to * just get those values out of there. But to maintain the principle of least * surprise, we're not going to modify the input data. */ function getValues(def) { var ii, out, key; out = []; for (ii = 0; ii < def.length; ii++) { key = Object.keys(def[ii])[0]; mod_assert.ok('value' in def[ii][key]); out.push(def[ii][key]['value']); } return (out); } /* * This is the second half of what we were born to do, write out the data * itself. Historically this function required you to put your values in the * definition section. This was not the smartest thing to do and a bit of an * oversight to be honest. As such, this function now takes a values argument. * If values is non-null and non-undefined, it will be used to determine the * values. This means that the old method is still supported, but is no longer * acceptable. * * def The array definition of the data to write out with * values * * buffer The buffer to write to * * offset The offset in the buffer to write to * * values An array of values to write. */ CTypeParser.prototype.writeData = function (def, buffer, offset, values) { var hv; if (def === undefined) throw (new Error('missing definition for what we should be' + 'parsing')); if (buffer === undefined) throw (new Error('missing buffer for what we should be ' + 'parsing')); if (offset === undefined) throw (new Error('missing offset for what we should be ' + 'parsing')); hv = (values != null && values != undefined); if (hv) { if (!Array.isArray(values)) throw (new Error('missing values for writing')); ctCheckReq(def, this.types); } else { ctCheckReq(def, this.types, [ 'value' ]); } this.writeStruct(hv ? values : getValues(def), def, buffer, offset); }; /* * Functions to go to and from 64 bit numbers in a way that is compatible with * Javascript limitations. There are two sets. One where the user is okay with * an approximation and one where they are definitely not okay with an * approximation. */ /* * Attempts to convert an array of two integers returned from rsint64 / ruint64 * into an absolute 64 bit number. If however the value would exceed 2^52 this * will instead throw an error. The mantissa in a double is a 52 bit number and * rather than potentially give you a value that is an approximation this will * error. If you would rather an approximation, please see toApprox64. * * val An array of two 32-bit integers */ function toAbs64(val) { if (val === undefined) throw (new Error('missing required arg: value')); if (!Array.isArray(val)) throw (new Error('value must be an array')); if (val.length != 2) throw (new Error('value must be an array of length 2')); /* We have 20 bits worth of precision in this range */ if (val[0] >= 0x100000) throw (new Error('value would become approximated')); return (val[0] * Math.pow(2, 32) + val[1]); } /* * Will return the 64 bit value as returned in an array from rsint64 / ruint64 * to a value as close as it can. Note that Javascript stores all numbers as a * double and the mantissa only has 52 bits. Thus this version may approximate * the value. * * val An array of two 32-bit integers */ function toApprox64(val) { if (val === undefined) throw (new Error('missing required arg: value')); if (!Array.isArray(val)) throw (new Error('value must be an array')); if (val.length != 2) throw (new Error('value must be an array of length 2')); return (Math.pow(2, 32) * val[0] + val[1]); } function parseCTF(json, conf) { var ctype = new CTypeParser(conf); mod_ctf.ctfParseJson(json, ctype); return (ctype); } /* * Export the few things we actually want to. Currently this is just the CType * Parser and ctio. */ exports.Parser = CTypeParser; exports.toAbs64 = toAbs64; exports.toApprox64 = toApprox64; exports.parseCTF = parseCTF; exports.ruint8 = mod_ctio.ruint8; exports.ruint16 = mod_ctio.ruint16; exports.ruint32 = mod_ctio.ruint32; exports.ruint64 = mod_ctio.ruint64; exports.wuint8 = mod_ctio.wuint8; exports.wuint16 = mod_ctio.wuint16; exports.wuint32 = mod_ctio.wuint32; exports.wuint64 = mod_ctio.wuint64; exports.rsint8 = mod_ctio.rsint8; exports.rsint16 = mod_ctio.rsint16; exports.rsint32 = mod_ctio.rsint32; exports.rsint64 = mod_ctio.rsint64; exports.wsint8 = mod_ctio.wsint8; exports.wsint16 = mod_ctio.wsint16; exports.wsint32 = mod_ctio.wsint32; exports.wsint64 = mod_ctio.wsint64; exports.rfloat = mod_ctio.rfloat; exports.rdouble = mod_ctio.rdouble; exports.wfloat = mod_ctio.wfloat; exports.wdouble = mod_ctio.wdouble;