diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c43c556 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["airbnb"] +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2783b6c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["airbnb"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efe7361 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +.idea/ +*.iml +data/ +out/ +lib/ +# .vscode/ \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..20848e2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +/node_modules/ +/src/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0378499..fff7684 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - 4 - - 5 - - 6 -script: node test.js + - node +before_script: + - npm run build +script: npm run test diff --git a/README.md b/README.md index 13d2b91..8c62413 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ binn.js ======= -[![Build Status](https://travis-ci.org/liteserver/binn.js.svg?branch=master)](https://travis-ci.org/liteserver/binn.js) +[![Build Status](https://travis-ci.org/MintT-SA/binn.js.svg?branch=master)](https://travis-ci.org/MintT-SA/binn.js) [![Deps](https://img.shields.io/badge/dependencies-none-brightgreen.svg)]() Binary serialization using the Binn format. @@ -16,20 +16,22 @@ Usage Header ```javascript -var binn = require('binn.js'); +import { encode, decode } from 'binn.js'; +// Or with require +const { encode, decode } = require('binn.js'); ``` Encoding ```javascript -var obj = {hello: 'world', number: 123}; -var data = binn.encode(obj); +const obj = { hello: 'world', number: 123 }; +const data = encode(obj); ``` Decoding ```javascript -var obj = binn.decode(data); +const obj = decode(data); ``` Undefined diff --git a/binn.js b/binn.js deleted file mode 100644 index d66dc59..0000000 --- a/binn.js +++ /dev/null @@ -1,510 +0,0 @@ -"use strict"; - -// modified BufferBuilder - -function BufferBuilder(initialCapacity) { - var buffer = Buffer.isBuffer(initialCapacity) ? initialCapacity : new Buffer(initialCapacity || 512); - this.buffers = [buffer]; - - this.writeIndex = 0; - this.length = 0; -} - -/* Append a Buffer */ -BufferBuilder.prototype.appendBuffer = function(source) { - if (source.length === 0) return this; - - /* slice the tail buffer */ - var tail = this.buffers[this.buffers.length-1]; - this.buffers[this.buffers.length-1] = tail.slice(0, this.writeIndex); - - /* add the new buffer */ - this.buffers.push(source); - this.writeIndex = source.length; - this.length += source.length; - - return this; -}; - -/* Prepend a Buffer */ -BufferBuilder.prototype.prependBuffer = function(source) { - if (source.length === 0) return this; - this.buffers.unshift(source); - this.length += source.length; - return this; -} - -BufferBuilder.prototype.getBufferWithSpace = function(size) { - var buf = this.buffers[this.buffers.length-1]; - if (this.writeIndex + size > buf.length) { - /* slice the current buffer */ - this.buffers[this.buffers.length-1] = buf.slice(0, this.writeIndex); - /* add a new buffer */ - var newbuf = new Buffer(Math.max(buf.length*2, size)); - this.buffers.push(newbuf); - this.writeIndex = 0; - buf = newbuf - } - return buf; -} - -function makeAppender(encoder, size) { - return function(x) { - var buf = this.getBufferWithSpace(size); - encoder.call(buf, x, this.writeIndex, true); - this.writeIndex += size; - this.length += size; - - return this; - }; -} - -BufferBuilder.prototype.appendUInt8 = makeAppender(Buffer.prototype.writeUInt8, 1); -BufferBuilder.prototype.appendUInt16LE = makeAppender(Buffer.prototype.writeUInt16LE, 2); -BufferBuilder.prototype.appendUInt16BE = makeAppender(Buffer.prototype.writeUInt16BE, 2); -BufferBuilder.prototype.appendUInt32LE = makeAppender(Buffer.prototype.writeUInt32LE, 4); -BufferBuilder.prototype.appendUInt32BE = makeAppender(Buffer.prototype.writeUInt32BE, 4); -BufferBuilder.prototype.appendInt8 = makeAppender(Buffer.prototype.writeInt8, 1); -BufferBuilder.prototype.appendInt16LE = makeAppender(Buffer.prototype.writeInt16LE, 2); -BufferBuilder.prototype.appendInt16BE = makeAppender(Buffer.prototype.writeInt16BE, 2); -BufferBuilder.prototype.appendInt32LE = makeAppender(Buffer.prototype.writeInt32LE, 4); -BufferBuilder.prototype.appendInt32BE = makeAppender(Buffer.prototype.writeInt32BE, 4); -BufferBuilder.prototype.appendFloatLE = makeAppender(Buffer.prototype.writeFloatLE, 4); -BufferBuilder.prototype.appendFloatBE = makeAppender(Buffer.prototype.writeFloatBE, 4); -BufferBuilder.prototype.appendDoubleLE = makeAppender(Buffer.prototype.writeDoubleLE, 8); -BufferBuilder.prototype.appendDoubleBE = makeAppender(Buffer.prototype.writeDoubleBE, 8); - -BufferBuilder.prototype.appendStringEx = function(str, size, encoding) { - if (!size) return; - var buf = this.getBufferWithSpace(size); - buf.write(str, this.writeIndex, size, encoding); - this.writeIndex += size; - this.length += size; - - return this; -}; - -BufferBuilder.prototype.appendString = function(str, encoding) { - return this.appendStringEx(str, Buffer.byteLength(str, encoding), encoding); -} - -BufferBuilder.prototype.appendStringZero = function(str, encoding) { - return this.appendString(str + '\0', encoding); -} - -BufferBuilder.prototype.appendFill = function(value, count) { - if (!count) return; - var buf = this.getBufferWithSpace(count); - buf.fill(value, this.writeIndex, this.writeIndex + count); - this.writeIndex += count; - this.length += count; - - return this; -}; - -/* Convert to a plain Buffer */ -BufferBuilder.prototype.get = function() { - /* slice the tail buffer */ - var tail = this.buffers[this.buffers.length-1]; - this.buffers[this.buffers.length-1] = tail.slice(0, this.writeIndex); - /* concatenate them */ - return Buffer.concat(this.buffers); -}; - - -// Binn - -exports.encode = encode; -exports.decode = decode; - -function Decoder(buffer, offset) { - this.offset = offset || 0; - this.buffer = buffer; -} -Decoder.prototype.list = function (length) { - var value = new Array(length); - for (var i = 0; i < length; i++) { - value[i] = this.parse(); - } - return value; -}; -Decoder.prototype.map = function (length) { - var value = {}; - for (var i = 0; i < length; i++) { - var key = this.buffer.readInt32BE(this.offset); - this.offset += 4; - value[key] = this.parse(); - } - return value; -}; -Decoder.prototype.obj = function (count) { - var value = {}; - for (var i = 0; i < count; i++) { - var keylen = this.buffer[this.offset]; - this.offset++; - var key = this.buffer.toString('utf8', this.offset, this.offset + keylen); - this.offset += keylen; - value[key] = this.parse(); - } - return value; -}; -Decoder.prototype.blob = function (length) { - var value = this.buffer.slice(this.offset, this.offset + length); - this.offset += length; - return value; -}; -Decoder.prototype.string = function (length) { - var value = this.buffer.toString('utf8', this.offset, this.offset + length); - this.offset += length + 1; // null terminator - return value; -}; -Decoder.prototype.getVarint = function () { - var value = this.buffer[this.offset]; - if (value & 0x80) { - value = this.buffer.readInt32BE(this.offset); - value &= 0x7FFFFFFF; - this.offset += 4; - } else { - this.offset++; - } - return value; -}; -Decoder.prototype.parse = function () { - var type, size, count, value; - - type = this.buffer[this.offset]; - this.offset++; - - switch (type) { - // List - case 0xE0: - size = this.getVarint(); - count = this.getVarint(); - return this.list(count); - // Map - case 0xE1: - size = this.getVarint(); - count = this.getVarint(); - return this.map(count); - // Object - case 0xE2: - size = this.getVarint(); - count = this.getVarint(); - return this.obj(count); - - // nil - case 0x00: - return null; - // true - case 0x01: - return true; - // false - case 0x02: - return false; - // undefined - case 0x03: - return undefined; - - // uint8 - case 0x20: - value = this.buffer[this.offset]; - this.offset++; - return value; - // uint 16 - case 0x40: - value = this.buffer.readUInt16BE(this.offset); - this.offset += 2; - return value; - // uint 32 - case 0x60: - value = this.buffer.readUInt32BE(this.offset); - this.offset += 4; - return value; - // uint64 - case 0x80: - value = this.buffer.readUInt64BE(this.offset); - this.offset += 8; - return value; - - // int 8 - case 0x21: - value = this.buffer.readInt8(this.offset); - this.offset++; - return value; - // int 16 - case 0x41: - value = this.buffer.readInt16BE(this.offset); - this.offset += 2; - return value; - // int 32 - case 0x61: - value = this.buffer.readInt32BE(this.offset); - this.offset += 4; - return value; - // int 64 - case 0x81: - value = this.buffer.readInt64BE(this.offset); - this.offset += 8; - return value; - - // float 32 - case 0x62: - value = this.buffer.readFloatBE(this.offset); - this.offset += 4; - return value; - // float 64 / double - case 0x82: - value = this.buffer.readDoubleBE(this.offset); - this.offset += 8; - return value; - - // string - case 0xA0: - size = this.getVarint(); - return this.string(size); - - // datetime, date, time, decimalstr - // TODO - - - // blob - case 0xC0: - size = this.buffer.readInt32BE(this.offset); - this.offset += 4; - return this.blob(size); - - } - - throw new Error("Unknown type 0x" + type.toString(16)); -}; -function decode(buffer) { - var decoder = new Decoder(buffer); - var value = decoder.parse(); - if (decoder.offset !== buffer.length) throw new Error((buffer.length - decoder.offset) + " trailing bytes"); - return value; -} - - -function encodeableKeys (value) { - return Object.keys(value).filter(function (e) { - return typeof value[e] !== 'function' || value[e].toJSON; - }); -} -function encode(value, builder) { - var type = typeof value; - var count, size; - - // String - if (type === "string") { - size = Buffer.byteLength(value); - // store the type - builder.appendUInt8(0xA0); - // store the size - if (size > 127) { - builder.appendInt32BE(size | 0x80000000); - } else { - builder.appendUInt8(size); - } - // store the string - builder.appendStringEx(value + '\0', size + 1); - return; - } - - // Blob - if (Buffer.isBuffer(value)) { - size = value.length; - // store the type - builder.appendUInt8(0xC0); - // store the size - builder.appendInt32BE(size); - // store the string - builder.appendBuffer(value); - return; - } - - if (type === "number") { - // Floating Point - if ((value << 0) !== value) { - // store the type - builder.appendUInt8(0x82); - // store the value - builder.appendDoubleBE(value); - return; - } - - // Integers - if (value >=0) { - // uint 8 - if (value < 0x100) { - // store the type - builder.appendUInt8(0x20); - // store the value - builder.appendUInt8(value); - return; - } - // uint 16 - if (value < 0x10000) { - // store the type - builder.appendUInt8(0x40); - // store the value - builder.appendUInt16BE(value); - return; - } - // uint 32 - if (value < 0x100000000) { - // store the type - builder.appendUInt8(0x60); - // store the value - builder.appendUInt32BE(value); - return 5; - } - // uint 64 - if (value < 0x10000000000000000) { - // store the type - builder.appendUInt8(0x80); - // store the value - builder.appendUInt64BE(value); - return; - } - throw new Error("Number too big 0x" + value.toString(16)); - } - // int 8 - if (value >= -0x80) { - // store the type - builder.appendUInt8(0x21); - // store the value - builder.appendInt8(value); - return; - } - // int 16 - if (value >= -0x8000) { - // store the type - builder.appendUInt8(0x41); - // store the value - builder.appendInt16BE(value); - return; - } - // int 32 - if (value >= -0x80000000) { - // store the type - builder.appendUInt8(0x61); - // store the value - builder.appendInt32BE(value); - return; - } - // int 64 - if (value >= -0x8000000000000000) { - // store the type - builder.appendUInt8(0x81); - // store the value - builder.appendInt64BE(value); - return; - } - throw new Error("Number too small -0x" + value.toString(16).substr(1)); - } - - if (type === "undefined") { - // store the type - builder.appendUInt8(0x03); // special type - return; - } - - // null - if (value === null) { - // store the type - builder.appendUInt8(0x00); - return; - } - - // Boolean - if (type === "boolean") { - // store the type - builder.appendUInt8(value ? 0x01 : 0x02); - return; - } - - // Custom toJSON function. - if (typeof value.toJSON === 'function') { - return encode(value.toJSON(), builder); - } - - // Container Types - if (type === "object") { - - // create a new buffer builder - var builder2 = new BufferBuilder(); - var isArray = Array.isArray(value); - - if (isArray) { - count = value.length; - } - else { - var keys = encodeableKeys(value); - count = keys.length; - } - - if (isArray) { - type = 0xE0; - // add the values to it - for (var i = 0; i < count; i++) { - encode(value[i], builder2); - } - } - else { - type = 0xE2; - // add the key value pairs to it - for (var i = 0; i < count; i++) { - var key = keys[i]; - // store the key - size = Buffer.byteLength(key); - if (size > 255) { - throw new Error("Key is longer than 255: " + key); - } - builder2.appendUInt8(size); - builder2.appendStringEx(key, size); - // store the value - encode(value[key], builder2); - } - } - - // save the header - var header = Buffer.allocUnsafe(9); - header.writeUInt8(type, 0); - // calculate the total size including the header - size = builder2.length + 3; - if (count > 127) size += 3; - if (size > 127) size += 3; - var offset; - // write the size - if (size > 127) { - header.writeInt32BE(size | 0x80000000, 1); - offset = 5; - } else { - header.writeUInt8(size, 1); - offset = 2; - } - // write the count - if (count > 127) { - header.writeInt32BE(count | 0x80000000, offset); - offset += 4; - } else { - header.writeUInt8(count, offset); - offset += 1; - } - // if the header is smaller than the max, resize it - if (offset < 9) { - header = header.slice(0, offset); - } - // prepend the header in the builder and return the resulting buffer - builder2.prependBuffer(header); - if (builder) { - builder.appendBuffer(builder2.get()); - return; - } else { - return builder2.get(); - } - } - - if (type === "function") return undefined; - throw new Error("Unknown type: " + type); -} diff --git a/package.json b/package.json index 7161818..73de328 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,35 @@ { - "author": "Bernardo Ramos ", + "author": "Bernardo Ramos , Nolan Vanmoortel ", "name": "binn.js", "description": "binn encoder and decoder in pure javascript", - "version": "0.1.0", + "version": "0.2.0", "repository": { "type": "git", "url": "git://github.com/liteserver/binn.js.git" }, - "main": "binn.js", - "devDependencies": { - "tape": "~1.0.2" - }, "scripts": { - "test": "node test.js" + "build": "babel src --out-dir lib", + "test": "npm run test:eslint && npm run test:e2e", + "test:e2e": "node lib/test.js", + "test:eslint": "eslint src", + "prepublish": "npm run build" + }, + "main": "lib/index.js", + "dependencies": { + "@babel/cli": "^7.2.0", + "@babel/core": "^7.2.0", + "@babel/node": "^7.2.0", + "babel-preset-airbnb": "^3.1.0", + "chai": "^4.2.0" + }, + "devDependencies": { + "babel-eslint": "^10.0.1", + "eslint": "^5.10.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-plugin-flowtype": "^3.2.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jsx-a11y": "^6.1.2", + "eslint-plugin-react": "^7.11.1" }, "license": "MIT" } diff --git a/src/BufferBuilder.js b/src/BufferBuilder.js new file mode 100644 index 0000000..5b8fbcd --- /dev/null +++ b/src/BufferBuilder.js @@ -0,0 +1,144 @@ +export default class BufferBuilder { + constructor(initialCapacity = 512) { + this.buffers = [Buffer.isBuffer(initialCapacity) + ? initialCapacity : Buffer.alloc(initialCapacity)]; + + this.writeIndex = 0; + this.length = 0; + } + + appendBuffer(source) { + if (!source.length) return this; + + /* slice the tail buffer */ + this.buffers[this.buffers.length - 1] = this.buffers[this.buffers.length - 1] + .slice(0, this.writeIndex); + + /* add the new buffer */ + this.buffers.push(source); + this.writeIndex = source.length; + this.length += source.length; + + return this; + } + + prependBuffer(source) { + if (!source.length) return this; + + this.buffers.unshift(source); + this.length += source.length; + return this; + } + + getBufferWithSpace(size) { + const buf = this.buffers[this.buffers.length - 1]; + if (this.writeIndex + size > buf.length) { + /* slice the current buffer */ + this.buffers[this.buffers.length - 1] = buf.slice(0, this.writeIndex); + /* add a new buffer */ + const newbuf = Buffer.alloc(Math.max(buf.length * 2, size)); + this.buffers.push(newbuf); + this.writeIndex = 0; + return newbuf; + } + return buf; + } + + makeAppender(size, writeFunc) { + const buf = this.getBufferWithSpace(size); + writeFunc(buf, this.writeIndex); + this.writeIndex += size; + this.length += size; + return this; + } + + appendUInt8(value) { + return this.makeAppender(1, (buf, writeIndex) => buf.writeUInt8(value, writeIndex)); + } + + appendUInt16LE(value) { + return this.makeAppender(2, (buf, writeIndex) => buf.writeUInt16LE(value, writeIndex)); + } + + appendUInt16BE(value) { + return this.makeAppender(2, (buf, writeIndex) => buf.writeUInt16BE(value, writeIndex)); + } + + appendUInt32LE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeUInt32LE(value, writeIndex)); + } + + appendUInt32BE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeUInt32BE(value, writeIndex)); + } + + appendInt8(value) { + return this.makeAppender(1, (buf, writeIndex) => buf.writeInt8(value, writeIndex)); + } + + appendInt16LE(value) { + return this.makeAppender(2, (buf, writeIndex) => buf.writeInt16LE(value, writeIndex)); + } + + appendInt16BE(value) { + return this.makeAppender(2, (buf, writeIndex) => buf.writeInt16BE(value, writeIndex)); + } + + appendInt32LE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeInt32LE(value, writeIndex)); + } + + appendInt32BE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeInt32BE(value, writeIndex)); + } + + appendFloatLE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeFloatLE(value, writeIndex)); + } + + appendFloatBE(value) { + return this.makeAppender(4, (buf, writeIndex) => buf.writeFloatBE(value, writeIndex)); + } + + appendDoubleLE(value) { + return this.makeAppender(8, (buf, writeIndex) => buf.writeDoubleLE(value, writeIndex)); + } + + appendDoubleBE(value) { + return this.makeAppender(8, (buf, writeIndex) => buf.writeDoubleBE(value, writeIndex)); + } + + appendStringEx(str, size, encoding) { + if (!size) return this; + const buf = this.getBufferWithSpace(size); + buf.write(str, this.writeIndex, size, encoding); + this.writeIndex += size; + this.length += size; + return this; + } + + appendString(str, encoding) { + return this.appendStringEx(str, Buffer.byteLength(str, encoding), encoding); + } + + appendStringZero(str, encoding) { + return this.appendString(`${str}\0`, encoding); + } + + appendFill(value, count) { + if (!count) return this; + const buf = this.getBufferWithSpace(count); + buf.fill(value, this.writeIndex, this.writeIndex + count); + this.writeIndex += count; + this.length += count; + return this; + } + + get() { + /* slice the tail buffer */ + this.buffers[this.buffers.length - 1] = this.buffers[this.buffers.length - 1] + .slice(0, this.writeIndex); + /* concatenate them */ + return Buffer.concat(this.buffers); + } +} diff --git a/src/Decoder.js b/src/Decoder.js new file mode 100644 index 0000000..aba7df9 --- /dev/null +++ b/src/Decoder.js @@ -0,0 +1,169 @@ +/* eslint-disable no-bitwise */ +export default class Decoder { + constructor(buffer, offset = 0) { + this.offset = offset; + this.buffer = buffer; + } + + list(length) { + const value = new Array(length); + for (let i = 0; i < length; i += 1) { + value[i] = this.parse(); + } + return value; + } + + map(length) { + const value = {}; + for (let i = 0; i < length; i += 1) { + const key = this.buffer.readInt32BE(this.offset); + this.offset += 4; + value[key] = this.parse(); + } + return value; + } + + obj(count) { + const value = {}; + for (let i = 0; i < count; i += 1) { + const keylen = this.buffer[this.offset]; + this.offset += 1; + const key = this.buffer.toString('utf8', this.offset, this.offset + keylen); + this.offset += keylen; + value[key] = this.parse(); + } + return value; + } + + blob(length) { + const value = this.buffer.slice(this.offset, this.offset + length); + this.offset += length; + return value; + } + + string(length) { + const value = this.buffer.toString('utf8', this.offset, this.offset + length); + this.offset += length + 1; // null terminator + return value; + } + + getVarint() { + let value = this.buffer[this.offset]; + if (value & 0x80) { + value = this.buffer.readInt32BE(this.offset); + value &= 0x7FFFFFFF; + this.offset += 4; + } else { + this.offset += 1; + } + return value; + } + + parse() { + const type = this.buffer[this.offset]; + let size; + let count; + let value; + + this.offset += 1; + switch (type) { + // List + case 0xE0: + size = this.getVarint(); + count = this.getVarint(); + return this.list(count); + // Map + case 0xE1: + size = this.getVarint(); + count = this.getVarint(); + return this.map(count); + // Object + case 0xE2: + size = this.getVarint(); + count = this.getVarint(); + return this.obj(count); + + // nil + case 0x00: + return null; + // true + case 0x01: + return true; + // false + case 0x02: + return false; + // undefined + case 0x03: + return undefined; + + // uint8 + case 0x20: + value = this.buffer[this.offset]; + this.offset += 1; + return value; + // uint 16 + case 0x40: + value = this.buffer.readUInt16BE(this.offset); + this.offset += 2; + return value; + // uint 32 + case 0x60: + value = this.buffer.readUInt32BE(this.offset); + this.offset += 4; + return value; + // uint64 + case 0x80: + value = this.buffer.readUInt64BE(this.offset); + this.offset += 8; + return value; + + // int 8 + case 0x21: + value = this.buffer.readInt8(this.offset); + this.offset += 1; + return value; + // int 16 + case 0x41: + value = this.buffer.readInt16BE(this.offset); + this.offset += 2; + return value; + // int 32 + case 0x61: + value = this.buffer.readInt32BE(this.offset); + this.offset += 4; + return value; + // int 64 + case 0x81: + value = this.buffer.readInt64BE(this.offset); + this.offset += 8; + return value; + + // float 32 + case 0x62: + value = this.buffer.readFloatBE(this.offset); + this.offset += 4; + return value; + // float 64 / double + case 0x82: + value = this.buffer.readDoubleBE(this.offset); + this.offset += 8; + return value; + + // string + case 0xA0: + size = this.getVarint(); + return this.string(size); + + // datetime, date, time, decimalstr + // TODO + + // blob + case 0xC0: + size = this.buffer.readInt32BE(this.offset); + this.offset += 4; + return this.blob(size); + default: + throw new Error(`Unknown type 0x${type.toString(16)}`); + } + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f9984ad --- /dev/null +++ b/src/index.js @@ -0,0 +1,182 @@ +/* eslint-disable no-bitwise */ +// Updated version of https://github.com/liteserver/binn.js/blob/master/binn.js +import BufferBuilder from './BufferBuilder'; +import Decoder from './Decoder'; + +const encodeableKeys = value => Object.keys(value).filter(e => typeof value[e] !== 'function' || value[e].toJSON); + +export const decode = (buffer) => { + const decoder = new Decoder(buffer); + const value = decoder.parse(); + if (decoder.offset !== buffer.length) throw new Error(`${buffer.length - decoder.offset} trailing bytes`); + return value; +}; + +export const encode = (value, builder) => { + let type = typeof value; + let count; + let size; + + // String + if (type === 'string') { + size = Buffer.byteLength(value); + // store the type + builder.appendUInt8(0xA0); + // store the size + if (size > 127) { + builder.appendInt32BE(size | 0x80000000); + } else { + builder.appendUInt8(size); + } + // store the string + builder.appendStringEx(`${value}\0`, size + 1); + } else if (Buffer.isBuffer(value)) { // Blob + size = value.length; + // store the type + builder.appendUInt8(0xC0); + // store the size + builder.appendInt32BE(size); + // store the string + builder.appendBuffer(value); + } else if (type === 'number') { + // Floating Point + if ((value << 0) !== value) { + // store the type + builder.appendUInt8(0x82); + // store the value + builder.appendDoubleBE(value); + } else if (value >= 0) { // Integers + // uint 8 + if (value < 0x100) { + // store the type + builder.appendUInt8(0x20); + // store the value + builder.appendUInt8(value); + } else if (value < 0x10000) { // uint 16 + // store the type + builder.appendUInt8(0x40); + // store the value + builder.appendUInt16BE(value); + } else if (value < 0x100000000) { // uint 32 + // store the type + builder.appendUInt8(0x60); + // store the value + builder.appendUInt32BE(value); + } else if (value < 0x10000000000000000) { // uint 64 + // store the type + builder.appendUInt8(0x80); + // store the value + builder.appendUInt64BE(value); + } else { + throw new Error(`Number too big 0x${value.toString(16)}`); + } + } else if (value >= -0x80) { // int 8 + // store the type + builder.appendUInt8(0x21); + // store the value + builder.appendInt8(value); + } else if (value >= -0x8000) { // int 16 + // store the type + builder.appendUInt8(0x41); + // store the value + builder.appendInt16BE(value); + } else if (value >= -0x80000000) { // int 32 + // store the type + builder.appendUInt8(0x61); + // store the value + builder.appendInt32BE(value); + } else if (value >= -0x8000000000000000) { // int 64 + // store the type + builder.appendUInt8(0x81); + // store the value + builder.appendInt64BE(value); + } else { + throw new Error(`Number too small -0x${value.toString(16).substr(1)}`); + } + } else if (type === 'undefined') { + // store the type + builder.appendUInt8(0x03); // special type + } else if (value === null) { // null + // store the type + builder.appendUInt8(0x00); + } else if (type === 'boolean') { // Boolean + // store the type + builder.appendUInt8(value ? 0x01 : 0x02); + } else if (typeof value.toJSON === 'function') { // Custom toJSON function. + encode(value.toJSON(), builder); + } else if (type === 'object') { // Container Types + // create a new buffer builder + const builder2 = new BufferBuilder(); + const isArray = Array.isArray(value); + + if (isArray) { + count = value.length; + + type = 0xE0; + // add the values to it + for (let i = 0; i < count; i += 1) { + encode(value[i], builder2); + } + } else { + const keys = encodeableKeys(value); + count = keys.length; + + type = 0xE2; + // add the key value pairs to it + for (let i = 0; i < count; i += 1) { + const key = keys[i]; + // store the key + size = Buffer.byteLength(key); + if (size > 255) { + throw new Error(`Key is longer than 255: ${key}`); + } + builder2.appendUInt8(size); + builder2.appendStringEx(key, size); + // store the value + encode(value[key], builder2); + } + } + + // save the header + let header = Buffer.allocUnsafe(9); + header.writeUInt8(type, 0); + // calculate the total size including the header + size = builder2.length + 3; + if (count > 127) size += 3; + if (size > 127) size += 3; + let offset; + // write the size + if (size > 127) { + header.writeInt32BE(size | 0x80000000, 1); + offset = 5; + } else { + header.writeUInt8(size, 1); + offset = 2; + } + // write the count + if (count > 127) { + header.writeInt32BE(count | 0x80000000, offset); + offset += 4; + } else { + header.writeUInt8(count, offset); + offset += 1; + } + // if the header is smaller than the max, resize it + if (offset < 9) { + header = header.slice(0, offset); + } + // prepend the header in the builder and return the resulting buffer + builder2.prependBuffer(header); + if (builder) { + builder.appendBuffer(builder2.get()); + } else { + return builder2.get(); + } + } else if (type === 'function') return undefined; + else { + throw new Error(`Unknown type: ${type}`); + } + return true; +}; + +export default { encode, decode }; diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000..39b313d --- /dev/null +++ b/src/test.js @@ -0,0 +1,70 @@ +/* eslint-disable no-console */ +import { assert } from 'chai'; + +import { encode, decode } from '.'; + +const tests = [ + true, false, null, undefined, + 0, 1, -1, 2, -2, 4, -4, 6, -6, + 0x10, -0x10, 0x20, -0x20, 0x40, -0x40, + 0x80, -0x80, 0x100, -0x100, 0x200, -0x100, + 0x1000, -0x1000, 0x10000, -0x10000, + 0x20000, -0x20000, 0x40000, -0x40000, + 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, + 10000000000, 100000000000, 1000000000000, + -10, -100, -1000, -10000, -100000, -1000000, -10000000, -100000000, + -1000000000, -10000000000, -100000000000, -1000000000000, + 1.1, 0.1, -0.02, + 'hello', 'world', Buffer.from('Hello'), Buffer.from('World'), + [1, 2, 3], [], { name: 'Tim', age: 29 }, {}, + { a: 1, b: 2, c: [1, 2, 3] }, +]; + +class Foo { + constructor() { + this.instance = true; + this.blah = 324; + } + + doThing() { + return this.blah + 1; + } +} + + +function jsonableFunction() { + console.log("can be json'ed"); +} + +// eslint-disable-next-line func-names +jsonableFunction.toJSON = function () { return this.toString(); }; + +const noop = () => {}; + +const jsonLikes = [ + { fun() {}, string: 'hello' }, + { + toJSON() { + return { object: true }; + }, + }, + new Date(0), + /regexp/, + new Foo(), + { fun: jsonableFunction }, + jsonableFunction, +]; + +console.log('Start test 1 ...'); +const packed = encode(tests); +console.log(packed); +assert.deepEqual(tests, decode(packed)); +console.log('test 1 done.'); + +console.log('Start test 2, treats functions same as json ...'); +assert.deepEqual(JSON.parse(JSON.stringify(jsonLikes)), decode(encode(jsonLikes))); +console.log('test 2 done.'); + +console.log('Start test 3, returns undefined for a function ...'); +assert.isUndefined(encode(noop)); +console.log('test 3done.'); diff --git a/test.js b/test.js deleted file mode 100644 index f081ec4..0000000 --- a/test.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; -var test = require('tape'); -var binn = require('./binn'); -var util = require('util'); - -var tests = [ - true, false, null, undefined, - 0, 1, -1, 2, -2, 4, -4, 6, -6, - 0x10, -0x10, 0x20, -0x20, 0x40, -0x40, - 0x80, -0x80, 0x100, -0x100, 0x200, -0x100, - 0x1000, -0x1000, 0x10000, -0x10000, - 0x20000, -0x20000, 0x40000,-0x40000, - 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, - 10000000000, 100000000000, 1000000000000, - -10, -100, -1000, -10000, -100000, -1000000, -10000000, -100000000, - -1000000000, -10000000000, -100000000000, -1000000000000, - 1.1, 0.1, -0.02, - 'hello', 'world', new Buffer("Hello"), new Buffer("World"), - [1,2,3], [], {name: "Tim", age: 29}, {}, - {a: 1, b: 2, c: [1, 2, 3]} -]; - -test('codec works as expected', function(assert) { - - var packed = binn.encode(tests); - console.log(packed); - var output = binn.decode(packed); - - assert.deepEqual(tests, output); - assert.end(); - -}); - -function Foo () { - this.instance = true -} - -Foo.prototype.blah = 324 - -Foo.prototype.doThing = function () {} - -function jsonableFunction () { - console.log("can be json'ed") -} - -jsonableFunction.toJSON = function () { return this.toString() } - -var jsonLikes = [ - {fun: function () {}, string: 'hello'}, - {toJSON: function () { - return {object: true} - }}, - new Date(0), - /regexp/, - new Foo(), - {fun: jsonableFunction}, - jsonableFunction, -] - -test('treats functions same as json', function (assert) { - assert.deepEqual( - binn.decode(binn.encode(jsonLikes)), - JSON.parse(JSON.stringify(jsonLikes)), - util.inspect(jsonLikes) - ) - assert.end() -}) - -test('returns undefined for a function', function (assert) { - function noop () {} - assert.equal(binn.encode(noop), JSON.stringify(noop)) - assert.end() -})