struct-fu
Version:
Yet another node.js struct implementation (object↔︎buffer conversion)
409 lines (372 loc) • 13 kB
JavaScript
var _ = {};
// These functions approximately reproduce the (pre-node 10) noAssert functionality,
// which allowed reading/writing past the end of a buffer, but 0-padding/ignoring
// any extra content.
function truncatedReadUInt32BE(buffer, offset) {
var availableBytes = buffer.length - offset;
if (availableBytes >= 4) {
return buffer.readUInt32BE(offset);
} else if (availableBytes === 3) {
var first = buffer.readUInt16BE(offset);
var second = buffer.readUInt8(offset + 2);
return ((first << 8) + second) << 8 >>> 0;
} else if (availableBytes === 2) {
return buffer.readUInt16BE(offset) << 16 >>> 0;
} else if (availableBytes === 1) {
return buffer.readUInt8(offset) << 24 >>> 0;
} else if (availableBytes <= 0) {
return 0x0;
}
}
function truncatedWriteUInt32BE(buffer, offset, data) {
var availableBytes = buffer.length - offset;
if (availableBytes >= 4) {
buffer.writeUInt32BE(data, offset);
} else if (availableBytes === 3) {
buffer.writeUInt16BE(data >>> 16, offset);
buffer.writeUInt8((data >>> 8) & 0xff, offset + 2);
} else if (availableBytes === 2) {
buffer.writeUInt16BE(data >>> 16, offset);
} else if (availableBytes === 1) {
buffer.writeUInt8(data >>> 24, offset);
}
}
// new Buffer() is deprecated in recent node. This ensures
// we always use the correct method for the current node.
function newBuffer(size) {
if (Buffer.alloc) {
return Buffer.alloc(size);
} else {
return new Buffer(size);
}
}
function extend(obj) {
Array.prototype.slice.call(arguments, 1).forEach(function (ext) {
Object.keys(ext).forEach(function (key) {
obj[key] = ext[key];
});
});
return obj;
}
function addField(ctr, f) {
if ('width' in f) {
ctr.bits = (ctr.bits || 0) + f.width;
while (ctr.bits > 7) {
ctr.bytes += 1;
ctr.bits -= 8;
}
} else if (!ctr.bits) {
ctr.bytes += f.size;
} else {
throw Error("Improperly aligned bitfield before field: "+f.name);
}
return ctr;
}
function arrayizeField(f, count) {
var f2 = (typeof count === 'number') ? extend({
name: f.name,
field: f,
valueFromBytes: function (buf, off) {
off || (off = {bytes:0, bits:0});
var arr = new Array(count);
for (var idx = 0, len = arr.length; idx < len; idx += 1) {
arr[idx] = f.valueFromBytes(buf, off);
}
return arr;
},
bytesFromValue: function (arr, buf, off) {
arr || (arr = new Array(count));
buf || (buf = newBuffer(this.size));
off || (off = {bytes:0, bits:0});
for (var idx = 0, len = Math.min(arr.length, count); idx < len; idx += 1) {
f.bytesFromValue(arr[idx], buf, off);
}
while (idx++ < count) addField(off, f);
return buf;
}
}, ('width' in f) ? {width: f.width * count} : {size: f.size * count}) : f;
f2.pack = f2.bytesFromValue;
f2.unpack = f2.valueFromBytes;
return f2;
}
_.struct = function (name, fields, count) {
if (typeof name !== 'string') {
count = fields;
fields = name;
name = null;
}
var _size = {bytes:0, bits:0},
_padsById = Object.create(null),
fieldsObj = fields.reduce(function (obj, f, i) {
if ('_padTo' in f) {
// HACK: we really should just make local copy of *all* fields
f._id || (f._id = 'id'+Math.random().toFixed(20).slice(2)); // WORKAROUND: https://github.com/tessel/runtime/issues/716
var _f = _padsById[f._id] = (_size.bits) ? {
width: 8*(f._padTo - _size.bytes) - _size.bits
} : {
size: f._padTo - _size.bytes
};
if (_f.width < 0 || _f.size < 0) {
var xtraMsg = (_size.bits) ? (" and "+_size.bits+" bits") : '';
throw Error("Invalid .padTo("+f._padTo+") field, struct is already "+_size.bytes+" byte(s)"+xtraMsg+"!");
}
f = _f;
}
else if (f._hoistFields) Object.keys(f._hoistFields).forEach(function (name) {
var _f = Object.create(f._hoistFields[name]);
if ('width' in _f) _f.offset = {bytes:_f.offset.bytes+_size.bytes, bits:_f.offset.bits};
else _f.offset += _size.bytes;
obj[name] = _f;
});
else if (f.name) {
f = Object.create(f); // local overrides
f.offset = ('width' in f) ? {bytes:_size.bytes,bits:_size.bits} : _size.bytes,
obj[f.name] = f;
}
addField(_size, f);
return obj;
}, {});
if (_size.bits) throw Error("Improperly aligned bitfield at end of struct: "+name);
return arrayizeField({
valueFromBytes: function (buf, off) {
off || (off = {bytes:0, bits:0});
var obj = new Object();
fields.forEach(function (f) {
if ('_padTo' in f) return addField(off, _padsById[f._id]);
var value = f.valueFromBytes(buf, off);
if (f.name) obj[f.name] = value;
else if (typeof value === 'object') extend(obj, value);
});
return obj;
},
bytesFromValue: function (obj, buf, off) {
obj || (obj = {});
buf || (buf = newBuffer(this.size));
off || (off = {bytes:0, bits:0});
fields.forEach(function (f) {
if ('_padTo' in f) return addField(off, _padsById[f._id]);
var value = (f.name) ? obj[f.name] : obj;
f.bytesFromValue(value, buf, off);
});
return buf;
},
_hoistFields: (!name) ? fieldsObj : null,
fields: fieldsObj,
size: _size.bytes,
name: name
}, count);
};
_.padTo = function (off) {
return {_padTo:off};
};
// NOTE: bitfields must be embedded in a struct (C/C++ share this limitation)
var FULL = 0xFFFFFFFF;
function bitfield(name, width, count) {
width || (width = 1);
// NOTE: width limitation is so all values will align *within* a 4-byte word
if (width > 24) throw Error("Bitfields support a maximum width of 24 bits.");
var impl = this,
mask = FULL >>> (32 - width);
return arrayizeField({
valueFromBytes: function (buf, off) {
off || (off = {bytes:0, bits:0});
var end = (off.bits || 0) + width,
word = truncatedReadUInt32BE(buf, off.bytes) || 0,
over = word >>> (32 - end);
addField(off, this);
return impl.b2v.call(this, over & mask);
},
bytesFromValue: function (val, buf, off) {
val = impl.v2b.call(this, val || 0);
off || (off = {bytes:0, bits:0});
var end = (off.bits || 0) + width,
word = truncatedReadUInt32BE(buf, off.bytes) || 0,
zero = mask << (32 - end),
over = (val & mask) << (32 - end);
word &= ~zero;
word |= over;
word >>>= 0; // WORKAROUND: https://github.com/tessel/runtime/issues/644
truncatedWriteUInt32BE(buf, off.bytes, word);
addField(off, this);
return buf;
},
width: width,
name: name
}, count);
}
function swapBits(n, w) {
var o = 0;
while (w--) {
o <<= 1;
o |= n & 1;
n >>>= 1;
}
return o;
}
_.bool = function (name, count) {
return bitfield.call({
b2v: function (b) { return Boolean(b); },
v2b: function (v) { return (v) ? FULL : 0; }
}, name, 1, count);
};
_.ubit = bitfield.bind({
b2v: function (b) { return b; },
v2b: function (v) { return v; }
});
_.ubitLE = bitfield.bind({
b2v: function (b) { return swapBits(b, this.width); },
v2b: function (v) { return swapBits(v, this.width); }
});
_.sbit = bitfield.bind({ // TODO: handle sign bit…
b2v: function (b) {
var m = 1 << (this.width-1),
s = b & m;
return (s) ? -(b &= ~m) : b;
},
v2b: function (v) {
var m = 1 << (this.width-1),
s = (v < 0);
return (s) ? (-v | m) : v;
}
});
function bytefield(name, size, count) {
if (typeof name !== 'string') {
count = size;
size = name;
name = null;
}
size = (typeof size === 'number') ? size : 1;
var impl = this;
return arrayizeField({
valueFromBytes: function (buf, off) {
off || (off = {bytes:0, bits:0});
var val = buf.slice(off.bytes, off.bytes+this.size);
addField(off, this);
return impl.b2v.call(this, val);
},
bytesFromValue: function (val, buf, off) {
off || (off = {bytes:0});
buf || (buf = newBuffer(this.size));
var blk = buf.slice(off.bytes, off.bytes+this.size),
len = impl.vTb.call(this, val, blk);
if (len < blk.length) blk.fill(0, len);
addField(off, this);
return buf;
},
size: size,
name: name
}, count);
}
// http://stackoverflow.com/a/7460958/72637
function swapBytesPairs(fromBuffer, toBuffer) {
toBuffer = toBuffer || fromBuffer
var l = fromBuffer.length;
for (var i = 1; i < l; i += 2) {
var a = fromBuffer[i - 1];
toBuffer[i - 1] = fromBuffer[i];
toBuffer[i] = a;
}
return toBuffer
}
_.byte = bytefield.bind({
b2v: function (b) { return b; },
vTb: function (v,b) { if (!v) return 0; v.copy(b); return v.length; }
});
_.char = bytefield.bind({
b2v: function (b) {
var v = b.toString('utf8'),
z = v.indexOf('\0');
return (~z) ? v.slice(0, z) : v;
},
vTb: function (v,b) {
v || (v = '');
return b.write(v, 'utf8');
}
});
_.char16le = bytefield.bind({
b2v: function (b) {
var v = b.toString('utf16le'),
z = v.indexOf('\0');
return (~z) ? v.slice(0, z) : v;
},
vTb: function (v,b) {
v || (v = '');
return b.write(v, 'utf16le');
}
});
_.char16be = bytefield.bind({
b2v: function (b) {
var temp = newBuffer(b.length);
swapBytesPairs(b, temp);
var v = temp.toString('utf16le'),
z = v.indexOf('\0');
return (~z) ? v.slice(0, z) : v;
},
vTb: function (v,b) {
v || (v = '');
var len = b.write(v, 'utf16le');
swapBytesPairs(b)
return len;
}
});
function standardField(sig, size) {
var read = 'read'+sig,
dump = 'write'+sig;
size || (size = +sig.match(/\d+/)[0] >> 3);
return function (name, count) {
if (typeof name !== 'string') {
count = name;
name = null;
}
return arrayizeField({
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var val = buf[read](off.bytes);
addField(off, this);
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = 0);
buf || (buf = newBuffer(this.size));
off || (off = {bytes:0});
buf[dump](val, off.bytes);
addField(off, this);
return buf;
},
size: size,
name: name
}, count);
};
}
_.float32 = standardField('FloatBE',4);
_.float64 = standardField('DoubleBE',8);
_.float32le = standardField('FloatLE',4);
_.float64le = standardField('DoubleLE',8);
_.uint8 = standardField('UInt8');
_.uint16 = standardField('UInt16BE');
_.uint32 = standardField('UInt32BE');
_.uint16le = standardField('UInt16LE');
_.uint32le = standardField('UInt32LE');
_.int8 = standardField('Int8');
_.int16 = standardField('Int16BE');
_.int32 = standardField('Int32BE');
_.int16le = standardField('Int16LE');
_.int32le = standardField('Int32LE');
_.derive = function (orig, pack, unpack) {
return function (name, count) {
if (typeof name !== 'string') {
count = name;
name = null;
}
return arrayizeField(extend({
valueFromBytes: function (buf, off) {
return unpack(orig.valueFromBytes(buf, off));
},
bytesFromValue: function (val, buf, off) {
return orig.bytesFromValue(pack(val), buf, off);
},
name: name
}, ('width' in orig) ? {width:orig.width} : {size:orig.size}), count);
}
};
module.exports = _;