database-jones/Adapter/common/MySQLSerialize.js (590 lines of code) (raw):
/*
Copyright (c) 2016, Oracle and/or its affiliates. All rights
reserved.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2 of
the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA
*/
"use strict";
/* wl#8132 JSON binary encoding
https://dev.mysql.com/worklog/task/?id=8132
*/
var assert = require("assert"),
unified_debug = require("unified_debug"),
udebug = unified_debug.getLogger("MySQLSerialize.js"),
TYPE_SMALL_OBJ = 0x00,
TYPE_LARGE_OBJ = 0x01,
TYPE_SMALL_ARRAY = 0x02,
TYPE_LARGE_ARRAY = 0x03,
TYPE_LITERAL = 0x04,
TYPE_INT16 = 0x05,
TYPE_UINT16 = 0x06,
TYPE_INT32 = 0x07,
TYPE_UINT32 = 0x08,
TYPE_INT64 = 0x09,
TYPE_UINT64 = 0x0A,
TYPE_DOUBLE = 0x0B,
TYPE_STRING = 0x0C,
LITERAL_NULL = 0x00,
LITERAL_TRUE = 0x01,
LITERAL_FALSE = 0x02,
BINARY_KEY = 99,
binaryNull,
binaryTrue,
binaryFalse,
binaryUndefined,
inlineBufsize,
keyEntrySizeArray, valueEntrySizeArray, headerSizeArray,
serialize;
/* Begin Polyfill */
Number.isInteger = Number.isInteger || function(value) {
return typeof value === "number" &&
isFinite(value) &&
Math.floor(value) === value;
};
if(!String.prototype.repeat) {
String.prototype.repeat = function(count) {
var str = "";
while(count-- > 0) { str += this; }
return str;
};
}
/* End Polyfill */
inlineBufsize = { 4:1, 5:2, 6:2, 7:4, 8:4 }; // decimal type => inline size
keyEntrySizeArray = [ 4,6 ]; // small object, large object
valueEntrySizeArray = [ 3,5,3,5 ]; // sm object, lg object, sm array, lg array
headerSizeArray = [ 4,8,4,8 ]; // sm object, lg object, sm array, lg array
/* Binary representation of a JavaScript value
*/
function Binary(type, jsValue, buffer) {
assert.notStrictEqual(type, undefined);
assert(type <= TYPE_STRING || type === BINARY_KEY);
this.type = type;
this.jsValue = jsValue;
this.buffer = buffer || null;
this.isUndefined = (type === TYPE_LITERAL && jsValue === undefined);
if(this.type <= TYPE_LARGE_ARRAY) {
this.isLarge = (type == TYPE_LARGE_OBJ || type == TYPE_LARGE_ARRAY);
this.keyEntrySize = keyEntrySizeArray[this.type];
this.valueEntrySize = valueEntrySizeArray[this.type];
this.headerSize = headerSizeArray[this.type];
}
}
/* element-count ::= uint16 | uint32
size ::= uint16 | uint32
*/
Binary.prototype.writeHeader = function(count, size) {
var buffer = Buffer.alloc(this.isLarge ? 8 : 4);
if(this.isLarge) {
buffer.writeUInt32LE(count, 0);
buffer.writeUInt32LE(size, 4);
} else {
buffer.writeUInt16LE(count, 0);
buffer.writeUInt16LE(size, 2);
}
return buffer;
};
Binary.prototype.readHeader = function() {
var recSize;
if((this.elementCount !== undefined) // Already read
|| (this.type > TYPE_LARGE_ARRAY)) // Value is a scalar
{
return;
}
if(this.isLarge) {
this.elementCount = this.buffer.readUInt32LE(0);
recSize = this.buffer.readUInt32LE(4);
} else {
this.elementCount = this.buffer.readUInt16LE(0);
recSize = this.buffer.readUInt16LE(2);
}
this.buffer = this.buffer.slice(0, recSize); // truncate buffer
if(this.type <= TYPE_LARGE_OBJ) { /* Object */
this.valueEntryStartPos = this.headerSize +
(this.keyEntrySize * this.elementCount);
}
};
Binary.prototype.isInline = function(isLarge) {
var sz = inlineBufsize[this.type];
return((sz !== undefined) && (isLarge || sz < 4));
};
Binary.prototype.writeInline = function(writeBuffer, offset) {
return this.buffer.copy(writeBuffer, offset);
};
Binary.prototype.readInline = function(sourceBuffer, offset) {
this.buffer = sourceBuffer.slice(offset, offset + inlineBufsize[this.type]);
udebug.log_detail("readInline", this.buffer);
};
Binary.prototype.setLarge = function() {
this.type += 1; // e.g. from TYPE_SMALL_OBJ to TYPE_LARGE_OBJ
this.isLarge = true;
this.keyEntrySize = keyEntrySizeArray[this.type];
this.valueEntrySize = valueEntrySizeArray[this.type];
this.headerSize = headerSizeArray[this.type];
};
Binary.prototype.write = function() {
return this.isUndefined ? null :
Buffer.concat([Buffer.from([this.type]), this.buffer], this.buffer.length+1);
};
function VariableLength(length) {
this.length = length || 0;
this.nBytes = 0;
}
VariableLength.prototype.parse = function(buffer, offset) {
var i, n;
for(i = 0 ; i < 5 ; i++) {
n = buffer[i+offset];
this.length |= (n & 0x7f) << (7 * i);
if((n & 0x80) == 0) {
/* This is the last byte */
this.nBytes = i+1;
return true; // success
}
}
this.length = 0; // failure
return false;
};
VariableLength.prototype.serialize = function() {
var length = this.length;
var lengthArray = [];
var byte;
do {
byte = length & 0x7f; // get the seven LSBs of length
length = length >> 7; // right shift to drop them
if(length) {
byte = byte | 0x80; // set the high bit to indicate more
}
lengthArray.push(byte);
} while(length);
return Buffer.from(lengthArray);
};
// string ::= data-length utf8-data
function serializeString(jsString) {
var stringBuffer, lengthBuffer, binary, vlen;
binary = new Binary(TYPE_STRING, jsString);
stringBuffer = Buffer.from(jsString, 'utf8');
vlen = new VariableLength(stringBuffer.length);
lengthBuffer = vlen.serialize();
binary.buffer = Buffer.concat([ lengthBuffer, stringBuffer ]);
return binary;
}
function serializeDouble(jsNumber) {
var binary = new Binary(TYPE_DOUBLE, jsNumber, Buffer.alloc(8));
binary.buffer.writeDoubleLE(jsNumber, 0);
return binary;
}
function serializeInt16(jsNumber) {
var binary = new Binary(TYPE_INT16, jsNumber, Buffer.alloc(2));
if(jsNumber < 32768) {
binary.buffer.writeInt16LE(jsNumber, 0);
} else {
binary.type = TYPE_UINT16;
binary.buffer.writeUInt16LE(jsNumber, 0);
}
return binary;
}
function serializeInt32(jsNumber) {
var binary = new Binary(TYPE_INT32, jsNumber, Buffer.alloc(4));
if(jsNumber < 2147483648) {
binary.buffer.writeInt32LE(jsNumber, 0);
} else {
binary.type = TYPE_UINT32;
binary.buffer.writeUInt32LE(jsNumber, 0);
}
return binary;
}
function serializeInt64(jsNumber) {
var binary = new Binary(TYPE_INT64, jsNumber);
binary.buffer = Buffer.from([0,0,0,0, 0,0,0,0]);
if(jsNumber < 0) {
assert.ifError("Encoding of large negative values is not implemented");
} else {
binary.type = TYPE_UINT64;
binary.buffer.writeUIntLE(jsNumber, 2, 6); // CORRECT? VERIFY ME!
}
return binary;
}
function serializeNumber(jsNumber) {
if(! Number.isInteger(jsNumber)) {
return serializeDouble(jsNumber);
}
if(jsNumber < 0) {
if(jsNumber >= -32768) {
return serializeInt16(jsNumber);
}
if(jsNumber >= -2147483648) {
return serializeInt32(jsNumber);
}
} else {
if(jsNumber <= 65535) {
return serializeInt16(jsNumber);
}
if(jsNumber <= 4294967295) {
return serializeInt32(jsNumber);
}
}
return serializeInt64(jsNumber);
}
function ValueEntry(binary, offset) {
this.binary = binary;
this.offset = offset;
}
/* value-entry ::= type offset-or-inlined-value */
// write = function(buffer, offset, cursor, isLarge) {
ValueEntry.prototype.write = function(buffer, dataStartPos, cursor, isLarge) {
buffer[cursor++] = this.binary.type;
if(this.binary.isInline(isLarge)) {
this.binary.writeInline(buffer, cursor);
} else if(isLarge) {
buffer.writeUInt32LE(dataStartPos + this.offset, cursor);
} else {
buffer.writeUInt16LE(dataStartPos + this.offset, cursor);
}
};
ValueEntry.prototype.read = function(buffer, cursor, isLarge) {
this.binary = new Binary(buffer[cursor++]);
if(this.binary.isInline(isLarge)) {
this.binary.readInline(buffer, cursor);
} else {
this.offset = isLarge ?
buffer.readUInt32LE(cursor) : buffer.readUInt16LE(cursor);
this.binary.buffer = buffer.slice(this.offset);
}
};
ValueEntry.prototype.parse = function() {
return this.binary.parse();
};
function KeyEntry(binary, offset) {
this.binary = binary;
this.offset = offset;
this.length = 0;
this.key = "";
}
/* key-entry ::= key-offset key-length */
KeyEntry.prototype.write = function(buffer, dataStartPos, cursor, isLarge) {
if(isLarge) {
buffer.writeUInt32LE(dataStartPos + this.offset, cursor);
cursor += 4;
} else {
buffer.writeUInt16LE(dataStartPos + this.offset, cursor);
cursor += 2;
}
buffer.writeUInt16LE(this.binary.buffer.length, cursor);
};
KeyEntry.prototype.read = function(buffer, cursor, isLarge) {
if(isLarge) {
this.offset = buffer.readUInt32LE(cursor);
cursor += 4;
} else {
this.offset = buffer.readUInt16LE(cursor);
cursor += 2;
}
this.length = buffer.readUInt16LE(cursor);
this.key = buffer.toString('utf8', this.offset, this.offset + this.length);
};
function List(binary, entrySizeArray, itemConstructor, dataBuffer) {
this.parent = binary;
this.entrySizeArray = entrySizeArray;
this.entrySize = entrySizeArray[this.parent.type];
this.ItemConstructor = itemConstructor;
this.entries = [];
this.data = dataBuffer || Buffer.from("");
}
List.prototype.push = function(binaryForm) {
var item = new this.ItemConstructor(binaryForm, this.data.length);
this.entries.push(item);
this.data = Buffer.concat( [this.data, binaryForm.buffer] );
};
List.prototype.writeEntries = function(dataStartPos) {
var buffer, elemSize, cursor, isLarge;
elemSize = this.entrySize;
buffer = Buffer.alloc(this.entries.length * elemSize);
cursor = 0;
isLarge = this.parent.isLarge;
this.entries.forEach(function(entry) {
entry.write(buffer, dataStartPos, cursor, isLarge);
cursor += elemSize;
});
return buffer;
};
List.prototype.readEntries = function(offset, count) {
var index;
for(index = 0; index < count ; index++) {
this.entries[index] = new this.ItemConstructor();
this.entries[index].read(this.parent.buffer,
offset + (index * this.entrySize),
this.parent.isLarge);
}
};
List.prototype.getSizeOfEntries = function() {
return this.entrySize * this.entries.length;
};
List.prototype.getTotalSize = function() {
this.entrySize = this.entrySizeArray[this.parent.type]; // recalculate
return this.getSizeOfEntries() + this.data.length;
};
List.prototype.parse = function() {
var result = [];
this.entries.forEach(function(item) {
result.push(item.parse());
});
return result;
};
/* Serialize an array.
Following the behavior of JSON.stringify(),
if an array element is undefined, replace it with Null.
*/
function serializeArray(jsArray) {
var binary = new Binary(TYPE_SMALL_ARRAY, jsArray);
var valueList = new List(binary, valueEntrySizeArray, ValueEntry);
var size;
jsArray.forEach(function(item) {
var bin = serialize(item);
if(bin.isUndefined) { bin = binaryNull; }
valueList.push(bin);
});
size = binary.headerSize + valueList.getTotalSize();
if(size > 65535) {
binary.setLarge();
size = binary.headerSize + valueList.getTotalSize(); // recalculates
}
/* array ::= element-count size value-entries values */
binary.buffer = Buffer.concat( [
binary.writeHeader(valueList.entries.length, size),
valueList.writeEntries(binary.headerSize + valueList.getSizeOfEntries()),
valueList.data
] );
return binary;
}
/* When serializing an object, keys are sorted on length, and keys
with the same length are sorted lexicographically */
function sortKeys(a,b) {
if(a.length > b.length) {
return 1;
}
if(a.length === b.length) {
return (a < b) ? -1 : 1;
}
return -1;
}
/* key ::= utf8-data */
function serializeKey(jsString) {
return new Binary(BINARY_KEY, jsString, Buffer.from(jsString, 'utf8'));
}
function serializeObject(jsObject) {
var binary = new Binary(TYPE_SMALL_OBJ, jsObject);
var keyList = new List(binary, keyEntrySizeArray, KeyEntry);
var valueList = new List(binary, valueEntrySizeArray, ValueEntry);
var sortedKeys;
var sortedValues = [];
var validityCheck = [];
var size;
var valueEntryStartPos, keyDataStartPos, valueDataStartPos;
/* Build an ordered list of keys */
sortedKeys = Object.keys(jsObject).sort(sortKeys);
/* Build a list of values in key order */
sortedKeys.forEach(function(key, index) {
sortedValues[index] = jsObject[key];
});
/* Encode the values, skipping those that cannot be serialized */
sortedValues.forEach(function(item, index) {
var bin = serialize(item);
var valid = ! bin.isUndefined;
validityCheck[index] = valid;
if(valid) {
valueList.push(bin);
}
});
/* Encode the keys */
sortedKeys.forEach(function(key, index) {
if(validityCheck[index]) {
keyList.push(serializeKey(key));
}
});
size = binary.headerSize + valueList.getTotalSize() + keyList.getTotalSize();
if(size > 65535) {
binary.setLarge(); // then recalculate sizes:
size = binary.headerSize + valueList.getTotalSize() + keyList.getTotalSize();
}
/* object ::= element-count size key-entries value-entries keys values */
valueEntryStartPos = binary.headerSize + keyList.getSizeOfEntries();
keyDataStartPos = valueEntryStartPos+ valueList.getSizeOfEntries();
valueDataStartPos = keyDataStartPos + keyList.data.length;
binary.buffer = Buffer.concat( [
binary.writeHeader(valueList.entries.length, size),
keyList.writeEntries(keyDataStartPos),
valueList.writeEntries(valueDataStartPos),
keyList.data,
valueList.data
] );
assert.equal(binary.buffer.length, size);
return binary;
}
/* Some pre-fabricated Binary values */
binaryNull = new Binary(TYPE_LITERAL, null, Buffer.from( [LITERAL_NULL] ));
binaryTrue = new Binary(TYPE_LITERAL, true, Buffer.from( [LITERAL_TRUE] ));
binaryFalse = new Binary(TYPE_LITERAL, false, Buffer.from( [LITERAL_FALSE] ));
binaryUndefined = new Binary(TYPE_LITERAL);
/* internal serialize() returns a Binary.
Behavior should follow Crockford's reference implementation
of JSON.stringify() in json2.js where possible.
*/
serialize = function(jsValue) {
switch(typeof jsValue) {
case 'undefined':
case 'function':
return binaryUndefined;
case 'boolean':
return jsValue ? binaryTrue : binaryFalse;
case 'number':
return serializeNumber(jsValue);
case 'string':
return serializeString(jsValue);
case 'object':
if(jsValue === null) {
return binaryNull;
}
if(Array.isArray(jsValue)) {
return serializeArray(jsValue);
}
if(typeof jsValue.toJSON === 'function') {
return serialize(jsValue.toJSON());
}
return serializeObject(jsValue);
default:
assert.ifError("Unsupported data type" + typeof jsValue);
}
};
//////////// Parser
Binary.prototype.parse = function() {
switch(this.type) {
case TYPE_LITERAL:
return this.parseLiteral();
case TYPE_INT16:
case TYPE_UINT16:
return this.parse16();
case TYPE_INT32:
case TYPE_UINT32:
return this.parse32();
case TYPE_DOUBLE:
return this.parseDouble();
case TYPE_STRING:
return this.parseString();
case TYPE_SMALL_ARRAY:
case TYPE_LARGE_ARRAY:
return this.parseArray();
case TYPE_SMALL_OBJ:
case TYPE_LARGE_OBJ:
return this.parseObject();
default:
assert.ifError("Parser for type not implemented " + this.type);
}
};
Binary.prototype.parseLiteral = function() {
if(Buffer.isBuffer(this.buffer)) {
switch(this.buffer[0]) {
case LITERAL_NULL:
return null;
case LITERAL_TRUE:
return true;
case LITERAL_FALSE:
return false;
default:
assert.ifError("Parser Error; badly formed literal");
}
} // if buffer is null, return undefined
};
Binary.prototype.parseDouble = function() {
return this.buffer.readDoubleLE(0);
};
Binary.prototype.parse16 = function() {
if(this.type == TYPE_INT16) {
return this.buffer.readInt16LE(0);
}
return this.buffer.readUInt16LE(0);
};
Binary.prototype.parse32 = function() {
if(this.type == TYPE_INT32) {
return this.buffer.readInt32LE(0);
}
return this.buffer.readUInt32LE(0);
};
Binary.prototype.parseString = function() {
var len = new VariableLength();
len.parse(this.buffer, 0);
return this.buffer.toString('utf8', len.nBytes, len.nBytes + len.length);
};
/* Set up parser for serialized arrays and objects.
array ::= element-count size value-entries values
object ::= element-count size key-entries value-entries keys values
*/
Binary.prototype.parseArray = function() {
var valueList;
this.readHeader();
valueList = new List(this, valueEntrySizeArray, ValueEntry);
valueList.readEntries(this.headerSize, this.elementCount);
return valueList.parse();
};
Binary.prototype.parseObject = function() {
var keyList, valueList, result, i;
this.readHeader();
valueList = new List(this, valueEntrySizeArray, ValueEntry);
keyList = new List(this, keyEntrySizeArray, KeyEntry);
if(this.elementCount > 0) {
keyList.readEntries(this.headerSize, this.elementCount);
valueList.readEntries(this.valueEntryStartPos, this.elementCount);
}
result = {};
for(i = 0 ; i < this.elementCount ; i++) {
result[keyList.entries[i].key] = valueList.entries[i].parse();
}
udebug.log_detail("parseObject result:", result);
return result;
};
Binary.prototype.getValueForKey = function(key) {
this.setupParser();
if(this.type <= TYPE_LARGE_OBJ) {
return this.getNamedValue(key);
}
return this.getIndexedValue(key);
};
Binary.prototype.getIndexedValue = function(key) {
var valueEntry, offset, valueBuffer;
valueEntry = new ValueEntry();
offset = this.valueEntryStartPos + (key * this.valueEntrySize);
valueBuffer = this.buffer.slice(this.valueDataStartPos);
valueEntry.read(this.buffer, offset, valueBuffer, this.isLarge);
return valueEntry.binary;
};
/* getNamedValue():
Keys are sorted by length, and then key string.
TODO: First conduct a binary search of the key space to find all keys of the
appropriate length; then search that set for the actual key string.
*/
Binary.prototype.getNamedValue = function(key) {
var index=0;
return this.getIndexedValue(index);
};
/* public serialize()
returns a Buffer containing rfc#8132 serialization of a JS object
*/
exports.serialize = function(jsValue) {
return serialize(jsValue).write();
};
function getBinaryForBuffer(sourceBuffer) {
if(Buffer.isBuffer(sourceBuffer) && sourceBuffer.length > 1) {
return new Binary(sourceBuffer[0], undefined, sourceBuffer.slice(1));
}
return binaryUndefined;
}
/* public parse()
takes a buffer
returns a JS object
*/
exports.parse = function(sourceBuffer) {
return Buffer.isBuffer(sourceBuffer) ?
getBinaryForBuffer(sourceBuffer).parse() : null;
};
exports.getUnitTestValues = function() {
var longValue = "abc".repeat(10000);
var largeObject = { "a" : longValue, "b" : longValue, "c" : longValue };
var largeArray = [ 1, longValue, 2, longValue, 3 , longValue , 4];
var longString = "abcd_".repeat(40); // variable length is two bytes
function TestItem() {
this.a = 1;
}
TestItem.prototype.b = 2;
return [
true,
false,
null,
undefined,
0,
1,
Math.sqrt(3),
"fred",
"",
[ 1 ],
[],
[ [1,2] , ["a","b"] ],
{ },
{ "a" : 1 },
[ {}, {"b" : 2 }, null, 4, "george" ],
[ "Peter" , true , "Paul" ,
false, 1, 90000, 1 ], // mix inlined and non-inlined values
50000, // 16-bit unsigned number
70000, // 32-bit number
2147500000, // 32-bit unsigned number
-1, // signed number
-30000, // signed 16-bit number
-50000, // signed 32-bit number
new Date(0), // call toJSON() and serialize that
{ "a" : undefined , "b" : 2 }, // omit undefined properties
[ null, true, undefined, 4], // replace array gap with null
function() {}, // undefined
new TestItem(), // omit prototype properties
Math, // language-defined object
largeObject,
largeArray,
longString,
{"a":[{"a10":"a10"},{"a11":"a11"}],"name":"Name 1","number":1}
];
};
function runUnitTests() {
exports.getUnitTestValues().forEach(function(t) {
var t1, r, s, r1;
t1 = JSON.stringify(t);
s = serialize(t);
r = s.parse();
r1 = JSON.stringify(r);
if(t1 === undefined || t1.length < 40) {
console.log(t, r);
} else {
console.log(s.type, s.buffer.length);
}
assert.equal(t1, r1);
});
}
// runUnitTests();