360 lines
9.7 KiB
JavaScript
360 lines
9.7 KiB
JavaScript
|
|
/**
|
|
* An interface for modeling and instantiating C-style data structures. This is
|
|
* not a constructor per-say, but a constructor generator. It takes an array of
|
|
* tuples, the left side being the type, and the right side being a field name.
|
|
* The order should be the same order it would appear in the C-style struct
|
|
* definition. It returns a function that can be used to construct an object that
|
|
* reads and writes to the data structure using properties specified by the
|
|
* initial field list.
|
|
*
|
|
* The only verboten field names are "ref", which is used used on struct
|
|
* instances as a function to retrieve the backing Buffer instance of the
|
|
* struct, and "ref.buffer" which contains the backing Buffer instance.
|
|
*
|
|
*
|
|
* Example:
|
|
*
|
|
* ``` javascript
|
|
* var ref = require('ref')
|
|
* var Struct = require('ref-struct')
|
|
*
|
|
* // create the `char *` type
|
|
* var charPtr = ref.refType(ref.types.char)
|
|
* var int = ref.types.int
|
|
*
|
|
* // create the struct "type" / constructor
|
|
* var PasswordEntry = Struct({
|
|
* 'username': 'string'
|
|
* , 'password': 'string'
|
|
* , 'salt': int
|
|
* })
|
|
*
|
|
* // create an instance of the struct, backed a Buffer instance
|
|
* var pwd = new PasswordEntry()
|
|
* pwd.username = 'ricky'
|
|
* pwd.password = 'rbransonlovesnode.js'
|
|
* pwd.salt = (Math.random() * 1000000) | 0
|
|
*
|
|
* pwd.username // → 'ricky'
|
|
* pwd.password // → 'rbransonlovesnode.js'
|
|
* pwd.salt // → 820088
|
|
* ```
|
|
*/
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var ref = require('ref')
|
|
var util = require('util')
|
|
var assert = require('assert')
|
|
var debug = require('debug')('ref:struct')
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = Struct
|
|
|
|
/**
|
|
* The Struct "type" meta-constructor.
|
|
*/
|
|
|
|
function Struct () {
|
|
debug('defining new struct "type"')
|
|
|
|
/**
|
|
* This is the "constructor" of the Struct type that gets returned.
|
|
*
|
|
* Invoke it with `new` to create a new Buffer instance backing the struct.
|
|
* Pass it an existing Buffer instance to use that as the backing buffer.
|
|
* Pass in an Object containing the struct fields to auto-populate the
|
|
* struct with the data.
|
|
*/
|
|
|
|
function StructType (arg, data) {
|
|
if (!(this instanceof StructType)) {
|
|
return new StructType(arg, data)
|
|
}
|
|
debug('creating new struct instance')
|
|
var store
|
|
if (Buffer.isBuffer(arg)) {
|
|
debug('using passed-in Buffer instance to back the struct', arg)
|
|
assert(arg.length >= StructType.size, 'Buffer instance must be at least ' +
|
|
StructType.size + ' bytes to back this struct type')
|
|
store = arg
|
|
arg = data
|
|
} else {
|
|
debug('creating new Buffer instance to back the struct (size: %d)', StructType.size)
|
|
store = new Buffer(StructType.size)
|
|
}
|
|
|
|
// set the backing Buffer store
|
|
store.type = StructType
|
|
this['ref.buffer'] = store
|
|
|
|
if (arg) {
|
|
for (var key in arg) {
|
|
// hopefully hit the struct setters
|
|
this[key] = arg[key]
|
|
}
|
|
}
|
|
StructType._instanceCreated = true
|
|
}
|
|
|
|
// make instances inherit from the `proto`
|
|
StructType.prototype = Object.create(proto, {
|
|
constructor: {
|
|
value: StructType
|
|
, enumerable: false
|
|
, writable: true
|
|
, configurable: true
|
|
}
|
|
})
|
|
|
|
StructType.defineProperty = defineProperty
|
|
StructType.toString = toString
|
|
StructType.fields = {}
|
|
|
|
var opt = (arguments.length > 0 && arguments[1]) ? arguments[1] : {};
|
|
// Setup the ref "type" interface. The constructor doubles as the "type" object
|
|
StructType.size = 0
|
|
StructType.alignment = 0
|
|
StructType.indirection = 1
|
|
StructType.isPacked = opt.packed ? Boolean(opt.packed) : false
|
|
StructType.get = get
|
|
StructType.set = set
|
|
|
|
// Read the fields list and apply all the fields to the struct
|
|
// TODO: Better arg handling... (maybe look at ES6 binary data API?)
|
|
var arg = arguments[0]
|
|
if (Array.isArray(arg)) {
|
|
// legacy API
|
|
arg.forEach(function (a) {
|
|
var type = a[0]
|
|
var name = a[1]
|
|
StructType.defineProperty(name, type)
|
|
})
|
|
} else if (typeof arg === 'object') {
|
|
Object.keys(arg).forEach(function (name) {
|
|
var type = arg[name]
|
|
StructType.defineProperty(name, type)
|
|
})
|
|
}
|
|
|
|
return StructType
|
|
}
|
|
|
|
/**
|
|
* The "get" function of the Struct "type" interface
|
|
*/
|
|
|
|
function get (buffer, offset) {
|
|
debug('Struct "type" getter for buffer at offset', buffer, offset)
|
|
if (offset > 0) {
|
|
buffer = buffer.slice(offset)
|
|
}
|
|
return new this(buffer)
|
|
}
|
|
|
|
/**
|
|
* The "set" function of the Struct "type" interface
|
|
*/
|
|
|
|
function set (buffer, offset, value) {
|
|
debug('Struct "type" setter for buffer at offset', buffer, offset, value)
|
|
var isStruct = value instanceof this
|
|
if (isStruct) {
|
|
// optimization: copy the buffer contents directly rather
|
|
// than going through the ref-struct constructor
|
|
value['ref.buffer'].copy(buffer, offset, 0, this.size)
|
|
} else {
|
|
if (offset > 0) {
|
|
buffer = buffer.slice(offset)
|
|
}
|
|
new this(buffer, value)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Custom `toString()` override for struct type instances.
|
|
*/
|
|
|
|
function toString () {
|
|
return '[StructType]'
|
|
}
|
|
|
|
/**
|
|
* Adds a new field to the struct instance with the given name and type.
|
|
* Note that this function will throw an Error if any instances of the struct
|
|
* type have already been created, therefore this function must be called at the
|
|
* beginning, before any instances are created.
|
|
*/
|
|
|
|
function defineProperty (name, type) {
|
|
debug('defining new struct type field', name)
|
|
|
|
// allow string types for convenience
|
|
type = ref.coerceType(type)
|
|
|
|
assert(!this._instanceCreated, 'an instance of this Struct type has already ' +
|
|
'been created, cannot add new "fields" anymore')
|
|
assert.equal('string', typeof name, 'expected a "string" field name')
|
|
assert(type && /object|function/i.test(typeof type) && 'size' in type &&
|
|
'indirection' in type
|
|
, 'expected a "type" object describing the field type: "' + type + '"')
|
|
assert(type.indirection > 1 || type.size > 0,
|
|
'"type" object must have a size greater than 0')
|
|
assert(!(name in this.prototype), 'the field "' + name +
|
|
'" already exists in this Struct type')
|
|
|
|
var field = {
|
|
type: type
|
|
}
|
|
this.fields[name] = field
|
|
|
|
// define the getter/setter property
|
|
var desc = { enumerable: true , configurable: true }
|
|
desc.get = function () {
|
|
debug('getting "%s" struct field (offset: %d)', name, field.offset)
|
|
return ref.get(this['ref.buffer'], field.offset, type)
|
|
}
|
|
desc.set = function (value) {
|
|
debug('setting "%s" struct field (offset: %d)', name, field.offset, value)
|
|
return ref.set(this['ref.buffer'], field.offset, value, type)
|
|
}
|
|
|
|
// calculate the new size and field offsets
|
|
recalc(this)
|
|
|
|
Object.defineProperty(this.prototype, name, desc)
|
|
}
|
|
|
|
function recalc (struct) {
|
|
|
|
// reset size and alignment
|
|
struct.size = 0
|
|
struct.alignment = 0
|
|
|
|
var fieldNames = Object.keys(struct.fields)
|
|
|
|
// first loop through is to determine the `alignment` of this struct
|
|
fieldNames.forEach(function (name) {
|
|
var field = struct.fields[name]
|
|
var type = field.type
|
|
var alignment = type.alignment || ref.alignof.pointer
|
|
if (type.indirection > 1) {
|
|
alignment = ref.alignof.pointer
|
|
}
|
|
if (struct.isPacked) {
|
|
struct.alignment = Math.min(struct.alignment || alignment, alignment)
|
|
} else {
|
|
struct.alignment = Math.max(struct.alignment, alignment)
|
|
}
|
|
})
|
|
|
|
// second loop through sets the `offset` property on each "field"
|
|
// object, and sets the `struct.size` as we go along
|
|
fieldNames.forEach(function (name) {
|
|
var field = struct.fields[name]
|
|
var type = field.type
|
|
|
|
if (null != type.fixedLength) {
|
|
// "ref-array" types set the "fixedLength" prop. don't treat arrays like one
|
|
// contiguous entity. instead, treat them like individual elements in the
|
|
// struct. doing this makes the padding end up being calculated correctly.
|
|
field.offset = addType(type.type)
|
|
for (var i = 1; i < type.fixedLength; i++) {
|
|
addType(type.type)
|
|
}
|
|
} else {
|
|
field.offset = addType(type)
|
|
}
|
|
})
|
|
|
|
function addType (type) {
|
|
var offset = struct.size
|
|
var align = type.indirection === 1 ? type.alignment : ref.alignof.pointer
|
|
var padding = struct.isPacked ? 0 : (align - (offset % align)) % align
|
|
var size = type.indirection === 1 ? type.size : ref.sizeof.pointer
|
|
|
|
offset += padding
|
|
|
|
if (!struct.isPacked) {
|
|
assert.equal(offset % align, 0, "offset should align")
|
|
}
|
|
|
|
// adjust the "size" of the struct type
|
|
struct.size = offset + size
|
|
|
|
// return the calulated offset
|
|
return offset
|
|
}
|
|
|
|
// any final padding?
|
|
var left = struct.size % struct.alignment
|
|
if (left > 0) {
|
|
debug('additional padding to the end of struct:', struct.alignment - left)
|
|
struct.size += struct.alignment - left
|
|
}
|
|
}
|
|
|
|
/**
|
|
* this is the custom prototype of Struct type instances.
|
|
*/
|
|
|
|
var proto = {}
|
|
|
|
/**
|
|
* set a placeholder variable on the prototype so that defineProperty() will
|
|
* throw an error if you try to define a struct field with the name "buffer".
|
|
*/
|
|
|
|
proto['ref.buffer'] = ref.NULL
|
|
|
|
/**
|
|
* Flattens the Struct instance into a regular JavaScript Object. This function
|
|
* "gets" all the defined properties.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
proto.toObject = function toObject () {
|
|
var obj = {}
|
|
Object.keys(this.constructor.fields).forEach(function (k) {
|
|
obj[k] = this[k]
|
|
}, this)
|
|
return obj
|
|
}
|
|
|
|
/**
|
|
* Basic `JSON.stringify(struct)` support.
|
|
*/
|
|
|
|
proto.toJSON = function toJSON () {
|
|
return this.toObject()
|
|
}
|
|
|
|
/**
|
|
* `.inspect()` override. For the REPL.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
proto.inspect = function inspect () {
|
|
var obj = this.toObject()
|
|
// add instance's "own properties"
|
|
Object.keys(this).forEach(function (k) {
|
|
obj[k] = this[k]
|
|
}, this)
|
|
return util.inspect(obj)
|
|
}
|
|
|
|
/**
|
|
* returns a Buffer pointing to this struct data structure.
|
|
*/
|
|
|
|
proto.ref = function ref () {
|
|
return this['ref.buffer']
|
|
}
|