byte-beta
Version:
Input Buffer and Output Buffer, just like Java ByteBuffer
658 lines (580 loc) • 17 kB
JavaScript
'use strict';
var Long = require('long');
var debug = require('debug')('byte');
var numbers = require('./number');
var utility = require('utility');
var DEFAULT_SIZE = 1024;
var BIG_ENDIAN = 1;
var LITTLE_ENDIAN = 2;
var MAX_INT_31 = Math.pow(2, 31);
var ONE_HUNDRED_MB = 100 * 1024 * 1024;
function ByteBuffer(options) {
options = options || {};
this._order = options.order || BIG_ENDIAN;
this._size = options.size || DEFAULT_SIZE;
this._offset = 0;
this._limit = this._size;
var array = options.array;
if (array) {
this._bytes = array;
} else {
this._bytes = new Buffer(this._size);
}
}
ByteBuffer.BIG_ENDIAN = BIG_ENDIAN;
ByteBuffer.LITTLE_ENDIAN = LITTLE_ENDIAN;
ByteBuffer.allocate = function (capacity) {
return new ByteBuffer({size: capacity});
};
ByteBuffer.wrap = function (array, offset, length) {
if (offset) {
var end = offset + (length || array.length);
array = array.slice(offset, end);
}
return new ByteBuffer({array: array, size: array.length});
};
ByteBuffer.prototype.reset = function () {
this._offset = 0;
};
ByteBuffer.prototype.order = function (order) {
this._order = order;
return this;
};
ByteBuffer.prototype._checkSize = function (afterSize) {
if (this._size >= afterSize) {
return;
}
var old = this._size;
this._size = afterSize * 2;
this._limit = this._size;
debug('allocate new Buffer: from %d to %d bytes', old, this._size);
var bytes;
if (Buffer.allocUnsafe) {
bytes = Buffer.allocUnsafe(this._size);
} else {
bytes = new Buffer(this._size);
}
this._bytes.copy(bytes, 0);
this._bytes = bytes;
};
ByteBuffer.prototype.put = function (src, offset, length) {
// byte
// bytes, offset, length
// index, byte
if (typeof src !== 'number') {
// bytes, offset, length
var index = this._offset;
offset = offset || 0;
length = length || src.length;
this._offset += length;
this._checkSize(this._offset);
src.copy(this._bytes, index, offset, offset + length);
return this;
}
if (offset === undefined) {
// byte
offset = src;
src = this._offset;
this._offset++;
this._checkSize(this._offset);
}
// index, byte
this._bytes[src] = offset;
return this;
};
ByteBuffer.prototype.get = function (index, length) {
if (index == null && length == null) {
return this._bytes[this._offset++];
}
if (typeof index === 'number' && length == null) {
return this._bytes[index];
}
if (typeof index === 'number' && typeof length === 'number') {
// offset, length => Buffer
return this._copy(index, index + length);
}
var dst = index;
var offset = length || 0;
length = dst.length;
this.checkArraySize(dst.length, offset, length);
this.checkForUnderflow(length);
if (Buffer.isBuffer(dst)) {
this._bytes.copy(dst, offset, this._offset, (this._offset + length));
} else {
for (var i = offset; i < (offset + length); i++) {
dst[i] = this._bytes[i];
}
}
this._offset += length;
return this;
};
ByteBuffer.prototype.read = function (size) {
var index = this._offset;
this._offset += size;
return this._bytes.slice(index, this._offset);
};
ByteBuffer.prototype.putChar = function (index, value) {
// char
// int, char
if (value === undefined) {
// char
value = charCode(index);
index = this._offset;
this._offset++;
this._checkSize(this._offset);
} else {
// int, char
value = charCode(value);
}
this._bytes[index] = value;
return this;
};
function charCode(c) {
return typeof c === 'string' ? c.charCodeAt(0) : c;
}
ByteBuffer.prototype.getChar = function (index) {
var b = this.get(index);
return String.fromCharCode(b);
};
Object.keys(numbers).forEach(function (type) {
var putMethod = 'put' + type;
var getMethod = 'get' + type;
var handles = numbers[type];
var size = handles.size;
ByteBuffer.prototype[putMethod] = function (index, value) {
// index, value
// value
if (value === undefined) {
// index, value
value = index;
index = this._offset;
this._offset += size;
this._checkSize(this._offset);
}
var handle = this._order === BIG_ENDIAN
? handles.writeBE
: handles.writeLE;
this._bytes[handle](value, index);
return this;
};
ByteBuffer.prototype[getMethod] = function (index) {
if (typeof index !== 'number') {
index = this._offset;
this._offset += size;
}
var handle = this._order === BIG_ENDIAN
? handles.readBE
: handles.readLE;
return this._bytes[handle](index);
};
});
ByteBuffer.prototype._putZero = function (index) {
this._bytes[index] = 0;
this._bytes[index + 1] = 0;
this._bytes[index + 2] = 0;
this._bytes[index + 3] = 0;
};
ByteBuffer.prototype._putFF = function (index) {
this._bytes[index] = 0xff;
this._bytes[index + 1] = 0xff;
this._bytes[index + 2] = 0xff;
this._bytes[index + 3] = 0xff;
};
ByteBuffer.prototype.putLong = function (index, value) {
// long
// int, long
var offset = 0;
if (value === undefined) {
// long
offset = this._offset;
this._offset += 8;
this._checkSize(this._offset);
value = index;
} else {
// int, long
offset = index;
}
// get the offset
var highOffset = offset;
var lowOffset = offset + 4;
if (this._order !== BIG_ENDIAN) {
highOffset = offset + 4;
lowOffset = offset;
}
var isNumber = typeof value === 'number';
// convert safe number string to number
if (!isNumber && utility.isSafeNumberString(value)) {
isNumber = true;
value = Number(value);
}
// int
if (isNumber
&& value < MAX_INT_31
&& value >= -MAX_INT_31) {
// put high
value < 0
? this._putFF(highOffset)
: this._putZero(highOffset);
if (this._order === BIG_ENDIAN) {
this._bytes.writeInt32BE(value, lowOffset);
} else {
this._bytes.writeInt32LE(value, lowOffset);
}
return this;
}
// long number or string, make it a Long Object
// TODO: Long object's performence has big problem
if (typeof value.low !== 'number'
|| typeof value.high !== 'number') {
// not Long instance, must be Number or String
value = isNumber
? Long.fromNumber(value)
: Long.fromString(value);
}
// write
if (this._order === BIG_ENDIAN) {
this._bytes.writeInt32BE(value.high, highOffset);
this._bytes.writeInt32BE(value.low, lowOffset);
} else {
this._bytes.writeInt32LE(value.high, highOffset);
this._bytes.writeInt32LE(value.low, lowOffset);
}
return this;
};
ByteBuffer.prototype.putInt64 = ByteBuffer.prototype.putLong;
ByteBuffer.prototype.getLong = function (index) {
if (typeof index !== 'number') {
index = this._offset;
this._offset += 8;
}
if (this._order === BIG_ENDIAN) {
return new Long(
this._bytes.readInt32BE(index + 4), // low, high
this._bytes.readInt32BE(index)
);
} else {
return new Long(
this._bytes.readInt32LE(index),
this._bytes.readInt32LE(index + 4)
);
}
};
ByteBuffer.prototype.getInt64 = ByteBuffer.prototype.getLong;
ByteBuffer.prototype._putString = function (index, value, format) {
if (!value || value.length === 0) {
// empty string
if (index === null || index === undefined) {
index = this._offset;
this._offset += 4;
this._checkSize(this._offset);
} else {
this._checkSize(index + 4);
}
return this.putInt(index, 0);
}
var isBuffer = Buffer.isBuffer(value);
var length = isBuffer
? value.length
: Buffer.byteLength(value);
if (format === 'c') {
length++;
}
if (index === null || index === undefined) {
index = this._offset;
this._offset += length + 4;
this._checkSize(this._offset);
} else {
this._checkSize(index + length + 4);
}
this.putInt(index, length);
var valueOffset = index + 4;
if (isBuffer) {
value.copy(this._bytes, valueOffset);
} else {
this._bytes.write(value, valueOffset);
}
if (format === 'c') {
this.put(valueOffset + length, 0);
}
return this;
};
ByteBuffer.prototype.putUTF8String = function (str) {
var len = str && str.length;
if (!len) {
return this;
}
return this.put(new Buffer(str));
};
// Prints a string to the Buffer, encoded as CESU-8
ByteBuffer.prototype.putRawString = function (index, str) {
if (typeof index === 'string') {
// putRawString(str)
str = index;
index = this._offset;
// Note that an UTF-8 encoder will encode a character that is outside BMP
// as 4 bytes, yet a CESU-8 encoder will encode as 6 bytes, ergo 6 / 4 = 1.5
// @see https://en.wikipedia.org/wiki/CESU-8
// this._checkSize(this._offset + Math.ceil(Buffer.byteLength(str) * 1.5));
// use big memory to exchange better performence
// one char => max bytes is 3
var maxIncreaseSize = str.length * 3;
if (maxIncreaseSize > ONE_HUNDRED_MB) {
maxIncreaseSize = Math.ceil(Buffer.byteLength(str) * 1.5);
}
this._checkSize(this._offset + maxIncreaseSize);
}
// CESU-8 Bit Distribution
// @see http://www.unicode.org/reports/tr26/
//
// UTF-16 Code Unit | 1st Byte | 2nd Byte | 3rd Byte
// 000000000xxxxxxx (0x0000 ~ 0x007f) | 0xxxxxxx (0x00 ~ 0x7f) | |
// 00000yyyyyxxxxxx (0x0080 ~ 0x07ff) | 110yyyyy (0xc0 ~ 0xdf) | 10xxxxxx (0x80 ~ 0xbf) |
// zzzzyyyyyyxxxxxx (0x0800 ~ 0xffff) | 1110zzzz (0xe0 ~ 0xef) | 10yyyyyy (0x80 ~ 0xbf) | 10xxxxxx (0x80 ~ 0xbf)
var len = str && str.length;
if (!len) {
return this;
}
for (var i = 0; i < len; i++) {
var ch = str.charCodeAt(i);
// 0x80: 128
if (ch < 0x80) {
this._bytes[index++] = ch;
} else if (ch < 0x800) {
// 0x800: 2048
this._bytes[index++] = (0xc0 + ((ch >> 6) & 0x1f)) >>> 32;
this._bytes[index++] = (0x80 + (ch & 0x3f)) >>> 32;
} else {
this._bytes[index++] = (0xe0 + ((ch >> 12) & 0xf)) >>> 32;
this._bytes[index++] = (0x80 + ((ch >> 6) & 0x3f)) >>> 32;
this._bytes[index++] = (0x80 + (ch & 0x3f)) >>> 32;
}
}
// index is now probably less than @_offset and reflects the real length
this._offset = index;
return this;
};
ByteBuffer.prototype._copy = function (start, end) {
// magic number here..
// @see benchmark/buffer_slice_and_copy.js
// if (end - start > 2048) {
// return this._bytes.slice(start, end);
// }
var buf = new Buffer(end - start);
this._bytes.copy(buf, 0, start, end);
return buf;
};
ByteBuffer.prototype.getUTF8String = function (length) {
var start = this._offset;
this._offset += length;
return this._bytes.toString('utf8', start, this._offset);
};
ByteBuffer.prototype.getRawStringFast = function (length) {
// short string `getRawStringByStringLength` has better performance
if (length <= 24) return this.getRawStringByStringLength(length);
var numInts = length >> 2;
var mod = length % 4;
var start = this._offset;
if (length > 0 && numInts === 0) return this.getRawStringByStringLength(length);
for (var i = 0; i < numInts; i++) {
var pos = i * 4;
var num = this._bytes.readInt32BE(this._offset + pos);
if ((num & 0x80808080) !== 0) {
this._offset += i;
return pos === 0 ?
this.getRawStringByStringLength(length) :
this._bytes.toString('utf8', start, this._offset) + this.getRawStringByStringLength(length - i);
}
}
this._offset += numInts * 4;
return mod ?
this._bytes.toString('utf8', start, this._offset) + this.getRawStringByStringLength(mod) :
this._bytes.toString('utf8', start, this._offset);
};
ByteBuffer.prototype.getRawStringByStringLength = function (index, length) {
var needUpdateOffset = false;
if (arguments.length === 1) {
length = index;
index = this._offset;
needUpdateOffset = true;
}
var data = [];
var bufLength = 0;
while (length--) {
var pos = index + bufLength;
var ch = this._bytes[pos];
if (ch < 0x80) {
data.push(ch);
bufLength += 1;
} else if ((ch & 0xe0) === 0xc0) {
var ch1 = this._bytes[++pos];
var v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
data.push(v);
bufLength += 2;
} else if ((ch & 0xf0) === 0xe0) {
var ch1 = this._bytes[++pos];
var ch2 = this._bytes[++pos];
var v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
data.push(v);
bufLength += 3;
} else {
throw new Error('string is not valid UTF-8 encode');
}
}
if (needUpdateOffset) this._offset += bufLength;
return String.fromCharCode.apply(String, data);
};
ByteBuffer.prototype.getRawString = function (index, length) {
if (typeof index !== 'number') {
// getRawString() => current offset char string
index = this._offset++;
} else if (typeof length === 'number') {
var data = [];
for (var pos = index, end = index + length; pos < end; pos++) {
var ch = this._bytes[pos];
if (ch < 0x80) {
data.push(ch);
} else if ((ch & 0xe0) === 0xc0) {
var ch1 = this._bytes[++pos];
var v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
data.push(v);
} else if ((ch & 0xf0) === 0xe0) {
var ch1 = this._bytes[++pos];
var ch2 = this._bytes[++pos];
var v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
data.push(v);
}
}
return String.fromCharCode.apply(String, data);
}
return String.fromCharCode(this._bytes[index]);
};
ByteBuffer.prototype.readRawString = function (index, length) {
if (arguments.length === 2) {
// readRawString(index, length)
} else {
// readRawString(length);
length = index;
index = this._offset;
this._offset += length;
}
return this._bytes.toString('utf8', index, index + length);
};
[ 'getString', 'getCString' ].forEach(function (method) {
ByteBuffer.prototype[method] = function (index) {
var moveOffset = false;
if (index === null || index === undefined) {
index = this._offset;
moveOffset = true;
}
var length = this.getInt(index);
index += 4;
if (moveOffset) {
this._offset += 4 + length;
}
if (length === 0) {
// empty string
return '';
}
if (method === 'getCString') {
length--;
}
return this._bytes.toString('utf8', index, index + length);
};
});
ByteBuffer.prototype.putString = function (index, value) {
if (typeof value === 'undefined') {
value = index;
index = null;
}
return this._putString(index, value, 'java');
};
ByteBuffer.prototype.putCString = function (index, value) {
if (typeof value === 'undefined') {
value = index;
index = null;
}
return this._putString(index, value, 'c');
};
ByteBuffer.prototype.toString = function () {
var s = '<ByteBuffer';
for (var i = 0; i < this._offset; i++) {
var c = this._bytes[i].toString('16');
if (c.length === 1) {
c = '0' + c;
}
s += ' ' + c;
}
s += '>';
return s;
};
ByteBuffer.prototype.copy = ByteBuffer.prototype.array = function (start, end) {
if (arguments.length === 0) {
// copy()
start = 0;
end = this._offset;
} else if (arguments.length === 1) {
// copy(start)
end = this._offset;
}
if (end > this._offset) {
end = this._offset;
}
return this._copy(start, end);
};
ByteBuffer.prototype.position = function (newPosition) {
if (typeof newPosition === 'number') {
this._offset = newPosition;
// make `bytes.position(1).read();` chain
return this;
}
return this._offset;
};
ByteBuffer.prototype.skip = function (size) {
this._offset += size;
};
ByteBuffer.prototype.flip = function () {
// switch to read mode
this.limit(this.position());
this.position(0);
return this;
};
ByteBuffer.prototype.clear = function () {
this._limit = this._size;
this._offset = 0;
this._bytes = new Buffer(this._size);
return this;
}
ByteBuffer.prototype.limit = function (newLimit) {
if(typeof newLimit === 'number') {
if ((newLimit < 0) || (newLimit > this._size)) {
throw new Error('IllegalArgumentException');
}
if (this._offset > newLimit) {
this._offset = newLimit;
}
this._limit = newLimit;
return this;
}
return this._limit;
};
ByteBuffer.prototype.capacity = function () {
return this._size;
};
ByteBuffer.prototype.remaining = function () {
return this.limit() - this.position();
};
ByteBuffer.prototype.hasRemaining = function () {
return this.remaining() > 0;
};
ByteBuffer.prototype.checkArraySize = function (arrayLength, offset, length) {
if ((offset < 0) || (length < 0) || (arrayLength < length + offset)) {
throw new Error('IndexOutOfBoundsException');
}
};
ByteBuffer.prototype.checkForUnderflow = function (length) {
length = length || 0;
if (this.remaining() < length) {
throw new Error('BufferOverflowException');
}
};
module.exports = ByteBuffer;