cbor-sync
Version:
CBOR encode/decode (synchronous, semantic, browser-compatible)
630 lines (606 loc) • 19.1 kB
JavaScript
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module !== 'undefined' && module.exports){
module.exports = factory();
} else {
global.CBOR = factory();
}
})(this, function () {
var CBOR = (function () {
function BinaryHex(hex) {
this.$hex = hex;
}
BinaryHex.prototype = {
length: function () {
return this.$hex.length/2;
},
toString: function (format) {
if (!format || format === 'hex' || format === 16) return this.$hex;
if (format === 'utf-8') {
var encoded = '';
for (var i = 0; i < this.$hex.length; i += 2) {
encoded += '%' + this.$hex.substring(i, i + 2);
}
return decodeURIComponent(encoded);
}
if (format === 'latin') {
var encoded = [];
for (var i = 0; i < this.$hex.length; i += 2) {
encoded.push(parseInt(this.$hex.substring(i, i + 2), 16));
}
return String.fromCharCode.apply(String, encoded);
}
throw new Error('Unrecognised format: ' + format);
}
};
BinaryHex.fromLatinString = function (latinString) {
var hex = '';
for (var i = 0; i < latinString.length; i++) {
var pair = latinString.charCodeAt(i).toString(16);
if (pair.length === 1) pair = "0" + pair;
hex += pair;
}
return new BinaryHex(hex);
};
BinaryHex.fromUtf8String = function (utf8String) {
var encoded = encodeURIComponent(utf8String);
var hex = '';
for (var i = 0; i < encoded.length; i++) {
if (encoded.charAt(i) === '%') {
hex += encoded.substring(i + 1, i + 3);
i += 2;
} else {
var hexPair = encoded.charCodeAt(i).toString(16);
if (hexPair.length < 2) hexPair = "0" + hexPair;
hex += hexPair;
}
}
return new BinaryHex(hex);
};
var semanticEncoders = [];
var semanticDecoders = {};
var notImplemented = function (label) {
return function () {
throw new Error(label + ' not implemented');
};
};
function Reader() {
}
Reader.prototype = {
peekByte: notImplemented('peekByte'),
readByte: notImplemented('readByte'),
readChunk: notImplemented('readChunk'),
readFloat16: function () {
var half = this.readUint16();
var exponent = (half&0x7fff) >> 10;
var mantissa = half&0x3ff;
var negative = half&0x8000;
if (exponent === 0x1f) {
if (mantissa === 0) {
return negative ? -Infinity : Infinity;
}
return NaN;
}
var magnitude = exponent ? Math.pow(2, exponent - 25)*(1024 + mantissa) : Math.pow(2, -24)*mantissa;
return negative ? -magnitude : magnitude;
},
readFloat32: function () {
var intValue = this.readUint32();
var exponent = (intValue&0x7fffffff) >> 23;
var mantissa = intValue&0x7fffff;
var negative = intValue&0x80000000;
if (exponent === 0xff) {
if (mantissa === 0) {
return negative ? -Infinity : Infinity;
}
return NaN;
}
var magnitude = exponent ? Math.pow(2, exponent - 23 - 127)*(8388608 + mantissa) : Math.pow(2, -23 - 126)*mantissa;
return negative ? -magnitude : magnitude;
},
readFloat64: function () {
var int1 = this.readUint32(), int2 = this.readUint32();
var exponent = (int1 >> 20)&0x7ff;
var mantissa = (int1&0xfffff)*4294967296 + int2;
var negative = int1&0x80000000;
if (exponent === 0x7ff) {
if (mantissa === 0) {
return negative ? -Infinity : Infinity;
}
return NaN;
}
var magnitude = exponent ? Math.pow(2, exponent - 52 - 1023)*(4503599627370496 + mantissa) : Math.pow(2, -52 - 1022)*mantissa;
return negative ? -magnitude : magnitude;
},
readUint16: function () {
return this.readByte()*256 + this.readByte();
},
readUint32: function () {
return this.readUint16()*65536 + this.readUint16();
},
readUint64: function () {
return this.readUint32()*4294967296 + this.readUint32();
}
};
function Writer() {
}
Writer.prototype = {
writeByte: notImplemented('writeByte'),
result: notImplemented('result'),
writeFloat16: notImplemented('writeFloat16'),
writeFloat32: notImplemented('writeFloat32'),
writeFloat64: notImplemented('writeFloat64'),
writeUint16: function (value) {
this.writeByte((value >> 8)&0xff);
this.writeByte(value&0xff);
},
writeUint32: function (value) {
this.writeUint16((value>>16)&0xffff);
this.writeUint16(value&0xffff);
},
writeUint64: function (value) {
if (value >= 9007199254740992 || value <= -9007199254740992) {
throw new Error('Cannot encode Uint64 of: ' + value + ' magnitude to big (floating point errors)');
}
this.writeUint32(Math.floor(value/4294967296));
this.writeUint32(value%4294967296);
},
writeString: notImplemented('writeString'),
canWriteBinary: function (chunk) {
return false;
},
writeBinary: notImplemented('writeChunk')
};
function readHeaderRaw(reader) {
var firstByte = reader.readByte();
var majorType = firstByte >> 5, value = firstByte&0x1f;
return {type: majorType, value: value};
}
function valueFromHeader(header, reader) {
var value = header.value;
if (value < 24) {
return value;
} else if (value == 24) {
return reader.readByte();
} else if (value == 25) {
return reader.readUint16();
} else if (value == 26) {
return reader.readUint32();
} else if (value == 27) {
return reader.readUint64();
} else if (value == 31) {
// special value for non-terminating arrays/objects
return null;
}
notImplemented('Additional info: ' + value)();
}
function writeHeaderRaw(type, value, writer) {
writer.writeByte((type<<5)|value);
}
function writeHeader(type, value, writer) {
var firstByte = type<<5;
if (value < 24) {
writer.writeByte(firstByte|value);
} else if (value < 256) {
writer.writeByte(firstByte|24);
writer.writeByte(value);
} else if (value < 65536) {
writer.writeByte(firstByte|25);
writer.writeUint16(value);
} else if (value < 4294967296) {
writer.writeByte(firstByte|26);
writer.writeUint32(value);
} else {
writer.writeByte(firstByte|27);
writer.writeUint64(value);
}
}
var stopCode = new Error(); // Just a unique object, that won't compare strictly equal to anything else
function decodeReader(reader) {
var header = readHeaderRaw(reader);
switch (header.type) {
case 0:
return valueFromHeader(header, reader);
case 1:
return -1 -valueFromHeader(header, reader);
case 2:
return reader.readChunk(valueFromHeader(header, reader));
case 3:
var buffer = reader.readChunk(valueFromHeader(header, reader));
return buffer.toString('utf-8');
case 4:
case 5:
var arrayLength = valueFromHeader(header, reader);
var result = [];
if (arrayLength !== null) {
if (header.type === 5) {
arrayLength *= 2;
}
for (var i = 0; i < arrayLength; i++) {
result[i] = decodeReader(reader);
}
} else {
var item;
while ((item = decodeReader(reader)) !== stopCode) {
result.push(item);
}
}
if (header.type === 5) {
var objResult = {};
for (var i = 0; i < result.length; i += 2) {
objResult[result[i]] = result[i + 1];
}
return objResult;
} else {
return result;
}
case 6:
var tag = valueFromHeader(header, reader);
var decoder = semanticDecoders[tag];
var result = decodeReader(reader);
return decoder ? decoder(result) : result;
case 7:
if (header.value === 25) {
return reader.readFloat16();
} else if (header.value === 26) {
return reader.readFloat32();
} else if (header.value === 27) {
return reader.readFloat64();
}
switch (valueFromHeader(header, reader)) {
case 20:
return false;
case 21:
return true;
case 22:
return null;
case 23:
return undefined;
case null:
return stopCode;
default:
throw new Error('Unknown fixed value: ' + header.value);
}
default:
throw new Error('Unsupported header: ' + JSON.stringify(header));
}
throw new Error('not implemented yet');
}
function encodeWriter(data, writer) {
for (var i = 0; i < semanticEncoders.length; i++) {
var replacement = semanticEncoders[i].fn(data);
if (replacement !== undefined) {
writeHeader(6, semanticEncoders[i].tag, writer);
return encodeWriter(replacement, writer);
}
}
if (data && typeof data.toCBOR === 'function') {
data = data.toCBOR();
}
if (data === false) {
writeHeader(7, 20, writer);
} else if (data === true) {
writeHeader(7, 21, writer);
} else if (data === null) {
writeHeader(7, 22, writer);
} else if (data === undefined) {
writeHeader(7, 23, writer);
} else if (typeof data === 'number') {
if (Math.floor(data) === data && data < 9007199254740992 && data > -9007199254740992) {
// Integer
if (data < 0) {
writeHeader(1, -1 - data, writer);
} else {
writeHeader(0, data, writer);
}
} else {
writeHeaderRaw(7, 27, writer);
writer.writeFloat64(data);
}
} else if (typeof data === 'string') {
writer.writeString(data, function (length) {
writeHeader(3, length, writer);
});
} else if (writer.canWriteBinary(data)) {
writer.writeBinary(data, function (length) {
writeHeader(2, length, writer);
});
} else if (typeof data === 'object') {
if (api.config.useToJSON && typeof data.toJSON === 'function') {
data = data.toJSON();
}
if (Array.isArray(data)) {
writeHeader(4, data.length, writer);
for (var i = 0; i < data.length; i++) {
encodeWriter(data[i], writer);
}
} else {
var keys = Object.keys(data);
writeHeader(5, keys.length, writer);
for (var i = 0; i < keys.length; i++) {
encodeWriter(keys[i], writer);
encodeWriter(data[keys[i]], writer);
}
}
} else {
throw new Error('CBOR encoding not supported: ' + data);
}
}
var readerFunctions = [];
var writerFunctions = [];
var api = {
config: {
useToJSON: true
},
addWriter: function (format, writerFunction) {
if (typeof format === 'string') {
writerFunctions.push(function (f) {
if (format === f) return writerFunction(f);
});
} else {
writerFunctions.push(format);
}
},
addReader: function (format, readerFunction) {
if (typeof format === 'string') {
readerFunctions.push(function (data, f) {
if (format === f) return readerFunction(data, f);
});
} else {
readerFunctions.push(format);
}
},
encode: function (data, format) {
for (var i = 0; i < writerFunctions.length; i++) {
var func = writerFunctions[i];
var writer = func(format);
if (writer) {
encodeWriter(data, writer);
return writer.result();
}
}
throw new Error('Unsupported output format: ' + format);
},
decode: function (data, format) {
for (var i = 0; i < readerFunctions.length; i++) {
var func = readerFunctions[i];
var reader = func(data, format);
if (reader) {
return decodeReader(reader);
}
}
throw new Error('Unsupported input format: ' + format);
},
addSemanticEncode: function (tag, fn) {
if (typeof tag !== 'number' || tag%1 !== 0 || tag < 0) {
throw new Error('Tag must be a positive integer');
}
semanticEncoders.push({tag: tag, fn: fn});
return this;
},
addSemanticDecode: function (tag, fn) {
if (typeof tag !== 'number' || tag%1 !== 0 || tag < 0) {
throw new Error('Tag must be a positive integer');
}
semanticDecoders[tag] = fn;
return this;
},
Reader: Reader,
Writer: Writer
};
/** Node.js Buffers **/
function BufferReader(buffer) {
this.buffer = buffer;
this.pos = 0;
}
BufferReader.prototype = Object.create(Reader.prototype);
BufferReader.prototype.peekByte = function () {
return this.buffer[this.pos];
};
BufferReader.prototype.readByte = function () {
return this.buffer[this.pos++];
};
BufferReader.prototype.readUint16 = function () {
var result = this.buffer.readUInt16BE(this.pos);
this.pos += 2;
return result;
};
BufferReader.prototype.readUint32 = function () {
var result = this.buffer.readUInt32BE(this.pos);
this.pos += 4;
return result;
};
BufferReader.prototype.readFloat32 = function () {
var result = this.buffer.readFloatBE(this.pos);
this.pos += 4;
return result;
};
BufferReader.prototype.readFloat64 = function () {
var result = this.buffer.readDoubleBE(this.pos);
this.pos += 8;
return result;
};
BufferReader.prototype.readChunk = function (length) {
var result = Buffer.alloc(length);
this.buffer.copy(result, 0, this.pos, this.pos += length);
return result;
};
function BufferWriter(stringFormat) {
this.byteLength = 0;
this.defaultBufferLength = 16384; // 16k
this.latestBuffer = Buffer.alloc(this.defaultBufferLength);
this.latestBufferOffset = 0;
this.completeBuffers = [];
this.stringFormat = stringFormat;
}
BufferWriter.prototype = Object.create(Writer.prototype);
BufferWriter.prototype.writeByte = function (value) {
this.latestBuffer[this.latestBufferOffset++] = value;
if (this.latestBufferOffset >= this.latestBuffer.length) {
this.completeBuffers.push(this.latestBuffer);
this.latestBuffer = Buffer.alloc(this.defaultBufferLength);
this.latestBufferOffset = 0;
}
this.byteLength++;
}
BufferWriter.prototype.writeFloat32 = function (value) {
var buffer = Buffer.alloc(4);
buffer.writeFloatBE(value, 0);
this.writeBuffer(buffer);
};
BufferWriter.prototype.writeFloat64 = function (value) {
var buffer = Buffer.alloc(8);
buffer.writeDoubleBE(value, 0);
this.writeBuffer(buffer);
};
BufferWriter.prototype.writeString = function (string, lengthFunc) {
var buffer = Buffer.from(string, 'utf-8');
lengthFunc(buffer.length);
this.writeBuffer(buffer);
};
BufferWriter.prototype.canWriteBinary = function (data) {
return data instanceof Buffer;
};
BufferWriter.prototype.writeBinary = function (buffer, lengthFunc) {
lengthFunc(buffer.length);
this.writeBuffer(buffer);
};
BufferWriter.prototype.writeBuffer = function (chunk) {
if (!(chunk instanceof Buffer)) throw new TypeError('BufferWriter only accepts Buffers');
if (!this.latestBufferOffset) {
this.completeBuffers.push(chunk);
} else if (this.latestBuffer.length - this.latestBufferOffset >= chunk.length) {
chunk.copy(this.latestBuffer, this.latestBufferOffset);
this.latestBufferOffset += chunk.length;
if (this.latestBufferOffset >= this.latestBuffer.length) {
this.completeBuffers.push(this.latestBuffer);
this.latestBuffer = Buffer.alloc(this.defaultBufferLength);
this.latestBufferOffset = 0;
}
} else {
this.completeBuffers.push(this.latestBuffer.slice(0, this.latestBufferOffset));
this.completeBuffers.push(chunk);
this.latestBuffer = Buffer.alloc(this.defaultBufferLength);
this.latestBufferOffset = 0;
}
this.byteLength += chunk.length;
}
BufferWriter.prototype.result = function () {
// Copies them all into a single Buffer
var result = Buffer.alloc(this.byteLength);
var offset = 0;
for (var i = 0; i < this.completeBuffers.length; i++) {
var buffer = this.completeBuffers[i];
buffer.copy(result, offset, 0, buffer.length);
offset += buffer.length;
}
if (this.latestBufferOffset) {
this.latestBuffer.copy(result, offset, 0, this.latestBufferOffset);
}
if (this.stringFormat) return result.toString(this.stringFormat);
return result;
}
if (typeof Buffer === 'function') {
api.addReader(function (data, format) {
if (data instanceof Buffer) {
return new BufferReader(data);
}
if (format === 'hex' || format === 'base64') {
var buffer = Buffer.from(data, format);
return new BufferReader(buffer);
}
});
api.addWriter(function (format) {
if (!format || format === 'buffer') {
return new BufferWriter();
} else if (format === 'hex' || format === 'base64') {
return new BufferWriter(format);
}
});
}
/** Hex-encoding (and Latin1) for browser **/
function HexReader(hex) {
this.hex = hex;
this.pos = 0;
}
HexReader.prototype = Object.create(Reader.prototype);
HexReader.prototype.peekByte = function () {
var pair = this.hex.substring(this.pos, 2);
return parseInt(pair, 16);
};
HexReader.prototype.readByte = function () {
var pair = this.hex.substring(this.pos, this.pos + 2);
this.pos += 2;
return parseInt(pair, 16);
};
HexReader.prototype.readChunk = function (length) {
var hex = this.hex.substring(this.pos, this.pos + length*2);
this.pos += length*2;
if (typeof Buffer === 'function') return Buffer.from(hex, 'hex');
return new BinaryHex(hex);
};
function HexWriter(finalFormat) {
this.$hex = '';
this.finalFormat = finalFormat || 'hex'
}
HexWriter.prototype = Object.create(Writer.prototype);
HexWriter.prototype.writeByte = function (value) {
if (value < 0 || value > 255) throw new Error('Byte value out of range: ' + value);
var hex = value.toString(16);
if (hex.length == 1) {
hex = '0' + hex;
}
this.$hex += hex;
}
HexWriter.prototype.canWriteBinary = function (chunk) {
return chunk instanceof BinaryHex || (typeof Buffer === 'function' && chunk instanceof Buffer);
}
HexWriter.prototype.writeBinary = function (chunk, lengthFunction) {
if (chunk instanceof BinaryHex) {
lengthFunction(chunk.length());
this.$hex += chunk.$hex;
} else if (typeof Buffer === 'function' && chunk instanceof Buffer) {
lengthFunction(chunk.length);
this.$hex += chunk.toString('hex');
} else {
throw new TypeError('HexWriter only accepts BinaryHex or Buffers');
}
}
HexWriter.prototype.result = function () {
if (this.finalFormat === 'buffer' && typeof Buffer === 'function') {
return Buffer.from(this.$hex, 'hex');
}
return new BinaryHex(this.$hex).toString(this.finalFormat);
}
HexWriter.prototype.writeString = function (string, lengthFunction) {
var buffer = BinaryHex.fromUtf8String(string);
lengthFunction(buffer.length());
this.$hex += buffer.$hex;
}
api.addReader(function (data, format) {
if (data instanceof BinaryHex || data.$hex) {
return new HexReader(data.$hex);
}
if (format === 'hex') {
return new HexReader(data)
}
});
api.addWriter(function (format) {
if (format === 'hex') {
return new HexWriter();
}
});
return api;
})();
CBOR.addSemanticEncode(0, function (data) {
if (data instanceof Date) {
return data.toISOString();
}
}).addSemanticDecode(0, function (isoString) {
return new Date(isoString);
}).addSemanticDecode(1, function (isoString) {
return new Date(isoString);
});
return CBOR;
});