marshal
Version:
Parse Ruby's Marshal strings into JavaScript objects/JSON.
341 lines (299 loc) • 9.68 kB
JavaScript
var debug = require('debug')('marshal');
var MarshalError = require('./marshalError');
var Ivar = require('./ivar');
var Marshal;
Marshal = (function () {
function Marshal (buffer, encoding) {
if (buffer !== undefined) this.load(buffer, encoding);
return this;
};
Marshal.prototype._parse = function () {
var typeCode = this.buffer.readUInt8(this._index++);
debug('type code ' + '0x' + this.buffer.toString('hex', this._index - 1, this._index) + ' index ' + (this._index - 1));
switch (typeCode) {
case 0x30: // 0 - nil
return null;
case 0x54: // T - true
return true;
case 0x46: // F - false
return false;
case 0x69: // i - integer
return this._parseInteger();
case 0x22: // " - string
return this._parseString();
case 0x3A: // : - symbol
return this._parseSymbol();
case 0x3B: // ; - symbol symlink
return this._parseSymbolLink();
case 0x40: // @ - object link
return this._parseObjectLink();
case 0x49: // I - IVAR (encoded string or regexp)
return this._parseIvar();
case 0x5B: // [ - array
return this._parseArray();
case 0x6F: // o - object
return this._parseObject();
case 0x7B: // { - hash
return this._parseHash();
case 0x6C: // l - bignum
return this._parseBignum();
case 0x66: // f - float
return this._parseFloat();
case 0x2F: // / - regexp
case 0x63: // c - class
case 0x6D: // m - module
default:
throw new MarshalError('unsupported typecode ' + typeCode, this);
}
};
Marshal.prototype._getLength = function () {
var length = this.buffer.readInt8(this._index++); // read the number of "machine words" - 16bit integers
if (length === 0) {
length = 0;
}
else if (length >= 6) {
length = length - 5;
}
else if (length <= -6) {
length = length + 5;
}
return length;
};
/** Parse a float.
* @return The decoded float.
*/
Marshal.prototype._parseFloat = function () {
var length = this._getLength();
var floatValue = this.buffer.slice(this._index, this._index + length);
this._index = this._index + length;
if (floatValue.toString('utf8') === 'inf') {
return Infinity;
}
if (floatValue.toString('utf8') === '-inf') {
return -Infinity;
}
if (floatValue.toString('utf8') === 'nan') {
return NaN;
}
return parseFloat(floatValue.toString('utf8'));
};
/** Parse a bignum.
* @return The bignum represented as a string.
*/
Marshal.prototype._parseBignum = function () {
var isNegative = (this.buffer.readInt8(this._index++) === 0x2d); // read the sign byte and check for '-'
debug('isNegative: %s', isNegative);
// the word length appears to always be positive but still follows the Marshal length logic
var wordLength = this._getLength();
debug('wordLength: %s', wordLength);
// it appears that words are always zero padded - no odd number of bytes
var byteLength = wordLength * 2;
var byteString = '';
for (var i = 0; i < byteLength; i++, this._index++) {
byteString = this.buffer.toString('hex', this._index, this._index + 1) + byteString;
}
debug('byteString: %j', byteString);
var bignum = BigInt('0x' + byteString).toString()
if (isNegative) {
bignum = '-' + bignum;
}
debug('bignum: %s', bignum);
return bignum;
}
/** Parse an integer.
* Used for reading of integer types and array lengths.
* @return The decoded integer.
*/
Marshal.prototype._parseInteger = function () {
var small = this.buffer.readInt8(this._index++); // read a signed byte
if (small === 0) {
return 0;
}
else if (small >= 6) {
return small - 5;
}
else if (small <= -6) {
return small + 5;
}
else if (small === 1) {
var large = this.buffer.readUInt8(this._index);
this._index += 1;
return large;
}
else if (small === 2) {
var large = this.buffer.readUInt16LE(this._index);
this._index += 2;
return large;
}
else if (small === 3) {
var large = Buffer.from(this.buffer.toString('hex', this._index, this._index + 3) + '00', 'hex').readUInt32LE(0);
this._index += 3;
return large;
}
else if (small === 4) {
var large = this.buffer.readUInt32LE(this._index);
this._index += 4;
return large;
}
else if (small === -1) {
var large = -(~(0xffffff00 + this.buffer.readUInt8(this._index)) + 1);
this._index += 1;
return large;
}
else if (small === -2) {
var large = this.buffer.readInt16LE(this._index);
this._index += 2;
return large;
}
else if (small === -3) {
var large = -(~(((0xffff0000 + this.buffer.readUInt16LE(this._index + 1)) << 8) + this.buffer.readUInt8(this._index)) + 1);
this._index += 3;
return large;
}
else if (small === -4) {
var large = this.buffer.readInt32LE(this._index);
this._index += 4;
return large;
}
else {
throw new MarshalError('unable to parse integer', this);
}
};
Marshal.prototype._parseArray = function () {
// a = ['foo', 'bar']
// \x04\b[\aI\"\bfoo\x06:\x06ETI\"\bbar\x06;\x00T
var arr = [];
var length = this._parseInteger();
if (length > 0) {
var value;
while (arr.length < length) {
value = this._parse();
arr.push(value);
}
}
return arr;
};
Marshal.prototype._parseObject = function () {
// Object.new
// \x04\bo:\vObject\x00
// o.instance_variable_set(:@foo, 'bar')
// \x04\bo:\vObject\x06:\t@fooI\"\bbar\x06:\x06ET
// o.instance_variable_set(:@bar, 'baz')
// \x04\bo:\vObject\a:\t@fooI\"\bbar\x06:\x06ET:\t@barI\"\bbaz\x06;\aT
// symbol name - either a symbol or a symbol link
var name = this._parse();
// hash
var object = this._parseHash();
// attach name
object['_name'] = name;
return object;
};
Marshal.prototype._parseHash = function () {
// {"first"=>"john", "middle"=>"clay", "last"=>"walker", "age"=>28}
// \x04\b{\tI\"\nfirst\x06:\x06ETI\"\tjohn\x06;\x00TI\"\vmiddle\x06;\x00TI\"\tclay\x06;\x00TI\"\tlast\x06;\x00TI\"\vwalker\x06;\x00TI\"\bage\x06;\x00Ti!
var hash = {};
var length = this._parseInteger();
if (length > 0) {
var key, value;
while (Object.keys(hash).length < length) {
key = this._parse();
value = this._parse();
hash[key] = value;
}
}
return hash;
};
Marshal.prototype._parseSymbol = function () {
var symbol = this._parseString();
this._symbols.push(symbol);
return symbol;
};
Marshal.prototype._parseSymbolLink = function () {
var index = this._parseInteger();
var symbol = this._symbols[index];
return symbol;
};
Marshal.prototype._parseObjectLink = function () {
var index = this._parseInteger();
var object = this._objects[index];
return object;
};
Marshal.prototype._parseString = function () {
var length = this._parseInteger();
var string = this.buffer.slice(this._index, this._index + length).toString();
this._index += length;
return string;
};
Marshal.prototype._parseIvar = function () {
var string = this._parse();
var encoding;
var lengthOfSymbolChar = this._parseInteger();
if (lengthOfSymbolChar === 1) {
// one character coming up, hopefully a symbol, treat it as a typecode
var symbol = this._parse();
var value = this._parse();
this._objects.push(value); // values are saved in the object cache before the ivar
if (symbol === 'E') {
// 'E' means we have a common encoding, the following boolean determines UTF-8 or ASCII
if (value === true) {
encoding = 'utf8';
}
else {
encoding = 'ascii';
}
}
else if (symbol === 'encoding') {
// 'encoding' means we have some other encoding
if (value === 'ISO-8859-1') {
encoding = 'binary';
} else {
encoding = value;
}
}
else {
throw new MarshalError('invalid IVAR encoding specification ' + symbol, this);
}
}
else {
throw new MarshalError('invalid IVAR, expected a single character', this);
}
var ivar = new Ivar(string, encoding);
this._objects.push(ivar); // completed ivars are saved in the object cache
debug('ivar ' + ivar);
return ivar.toString();
};
Marshal.prototype.load = function (buffer, encoding) {
if (buffer === undefined || buffer.length === 0) {
throw new MarshalError('no buffer specified', this);
}
else if (buffer instanceof Buffer) {
this.buffer = buffer;
}
else {
this.buffer = Buffer.from(buffer, encoding);
}
debug('buffer length ' + this.buffer.length);
// reset the index
this._index = 0;
// parse Marshal version
this._version = this.buffer.readUInt8(this._index++) + '.' + this.buffer.readUInt8(this._index++);
// create cache tables
this._symbols = [];
this._objects = [];
if (this._index < this.buffer.length) {
this.parsed = this._parse();
}
return this;
};
// Marshal.prototype.dump = function () {
// //TODO
// };
Marshal.prototype.toString = function (encoding) {
return this.buffer.toString(encoding || 'base64');
};
Marshal.prototype.toJSON = function () {
return this.parsed;
};
return Marshal;
})();
module.exports = Marshal;