/*
* Copyright (c) 2017 Ahmed Kh. Zamil
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Created by Ahmed Zamil on 25/07/2017.
*/
"use strict";
const ResourceComparisonResult =
{
Null: 0,
Distributed: 1,
Local: 2,
Same: 3
};
const StructureComparisonResult =
{
Null: 0,
Structure: 1,
StructureSameKeys: 2,
StructureSameTypes: 3,
Same: 4
};
class Codec {
static parse(data, offset, sizeObject, connection, dataType = DataType.Unspecified) {
var size;
var reply = new AsyncReply();
var isArray;
var t;
if (dataType == DataType.Unspecified) {
size = 1;
dataType = data[offset++];
}
else
size = 0;
t = dataType & 0x7F;
isArray = (dataType & 0x80) == 0x80;
var payloadSize = DataType.sizeOf(dataType);
var contentLength = 0;
// check if we have the enough data
if (payloadSize == -1) {
contentLength = data.getUint32(offset);
offset += 4;
size += 4 + contentLength;
}
else
size += payloadSize;
sizeObject.size = size;
if (isArray) {
switch (t) {
// VarArray ?
case DataType.Void:
return Codec.parseVarArray(data, offset, contentLength, connection);
case DataType.Bool:
return new AsyncReply(data.getBooleanArray(offset, contentLength));
case DataType.UInt8:
return new AsyncReply(data.getUint8Array(offset, contentLength));
case DataType.Int8:
return new AsyncReply(data.getInt8Array(offset, contentLength));
case DataType.Char:
return new AsyncReply(data.getCharArray(offset, contentLength));
case DataType.Int16:
return new AsyncReply(data.getInt16Array(offset, contentLength));
case DataType.UInt16:
return new AsyncReply(data.getUint16Array(offset, contentLength));
case DataType.Int32:
return new AsyncReply(data.getInt32Array(offset, contentLength));
case DataType.UInt32:
return new AsyncReply(data.getUint32Array(offset, contentLength));
case DataType.Int64:
return new AsyncReply(data.getInt64Array(offset, contentLength));
case DataType.UInt64:
return new AsyncReply(data.getUint64Array(offset, contentLength));
case DataType.Float32:
return new AsyncReply(data.getFloat32Array(offset, contentLength));
case DataType.Float64:
return new AsyncReply(data.getFloat64Array(offset, contentLength));
case DataType.String:
return new AsyncReply(data.getStringArray(offset, contentLength));
case DataType.Resource:
case DataType.DistributedResource:
return Codec.parseResourceArray(data, offset, contentLength, connection);
case DataType.DateTime:
return new AsyncReply(data.getDateTimeArray(offset, contentLength));
case DataType.Structure:
return Codec.parseStructureArray(data, offset, contentLength, connection);
}
}
else {
switch (t) {
case DataType.NotModified:
return new AsyncReply(new NotModified());
case DataType.Void:
return new AsyncReply(null);
case DataType.Bool:
return new AsyncReply(data.getBoolean(offset));
case DataType.UInt8:
return new AsyncReply(data[offset]);
case DataType.Int8:
return new AsyncReply(data.getInt8(offset));
case DataType.Char:
return new AsyncReply(data.getChar(offset));
case DataType.Int16:
return new AsyncReply(data.getInt16(offset));
case DataType.UInt16:
return new AsyncReply(data.getUint16(offset));
case DataType.Int32:
return new AsyncReply(data.getInt32(offset));
case DataType.UInt32:
return new AsyncReply(data.getUint32(offset));
case DataType.Int64:
return new AsyncReply(data.getInt64(offset));
case DataType.UInt64:
return new AsyncReply(data.getUint64(offset));
case DataType.Float32:
return new AsyncReply(data.getFloat32(offset));
case DataType.Float64:
return new AsyncReply(data.getFloat64(offset));
case DataType.String:
return new AsyncReply(data.getString(offset, contentLength));
case DataType.Resource:
return Codec.parseResource(data, offset);
case DataType.DistributedResource:
return Codec.parseDistributedResource(data, offset, connection);
case DataType.DateTime:
return new AsyncReply(data.getDateTime(offset));
case DataType.Structure:
return Codec.parseStructure(data, offset, contentLength, connection);
}
}
return null;
}
static parseResource(data, offset) {
return Warehouse.get(data.getUint32(offset));
}
static parseDistributedResource(data, offset, connection) {
//var g = data.getGuid(offset);
//offset += 16;
// find the object
var iid = data.getUint32(offset);
return connection.fetch(iid);// Warehouse.Get(iid);
}
///
/// Parse an array of bytes into array of resources
///
/// Array of bytes.
/// Number of bytes to parse.
/// Zero-indexed offset.
/// DistributedConnection is required to fetch resources.
/// Array of resources.
static parseResourceArray(data, offset, length, connection)
{
var reply = new AsyncBag();
if (length == 0)
{
reply.seal();
return reply;
}
var end = offset + length;
//
var result = data[offset++];
var previous = null;
if (result == ResourceComparisonResult.Null)
previous = new AsyncReply(null);
else if (result == ResourceComparisonResult.Local)
{
previous = Warehouse.get(data.getUint32(offset));
offset += 4;
}
else if (result == ResourceComparisonResult.Distributed)
{
previous = connection.fetch(data.getUint32(offset));
offset += 4;
}
reply.add(previous);
while (offset < end)
{
result = data[offset++];
var current = null;
if (result == ResourceComparisonResult.Null)
{
current = new AsyncReply(null);
}
else if (result == ResourceComparisonResult.Same)
{
current = previous;
}
else if (result == ResourceComparisonResult.Local)
{
current = Warehouse.get(data.getUint32(offset));
offset += 4;
}
else if (result == ResourceComparisonResult.Distributed)
{
current = connection.fetch(data.getUint32(offset));
offset += 4;
}
reply.add(current);
previous = current;
}
reply.seal();
return reply;
}
///
/// Compose an array of property values.
///
/// PropertyValue array.
/// DistributedConnection is required to check locality.
/// If True, prepend the length as UInt32 at the beginning of the output.
/// Array of bytes in the network byte order.
static composePropertyValueArray(array, connection, prependLength = false)
{
var rt = BL();
for (var i = 0; i < array.Length; i++)
rt.addUint8Array(Codec.composePropertyValue(array[i], connection));
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
///
/// Compose a property value.
///
/// Property value
/// DistributedConnection is required to check locality.
/// Array of bytes in the network byte order.
static composePropertyValue(propertyValue, connection)
{
// age, date, value
return BL().addUint64(propertyValue.age)
.addDateTime(propertyValue.date)
.addUint8Array(Codec.compose(propertyValue.value, connection))
.toArray();
}
///
/// Parse property value.
///
/// Array of bytes.
/// Zero-indexed offset.
/// DistributedConnection is required to fetch resources.
/// Output content size.
/// PropertyValue.
static parsePropertyValue(data, offset, sizeObject, connection)
{
var reply = new AsyncReply();
var age = data.getUint64(offset);
offset += 8;
var date = data.getDateTime(offset);
offset += 8;
var cs = {};
Codec.parse(data, offset, cs, connection).then(function(value)
{
reply.trigger(new PropertyValue(value, age, date));
});
sizeObject.size = 16 + cs.size;
return reply;
}
///
/// Parse resource history
///
/// Array of bytes.
/// Zero-indexed offset.
/// Number of bytes to parse.
/// Resource
/// Starting age.
/// Ending age.
/// DistributedConnection is required to fetch resources.
///
static parseHistory(data, offset, length, resource, connection)
{
var list = new KeyList();
var reply = new AsyncReply();
var bagOfBags = new AsyncBag();
var ends = offset + length;
while (offset < ends)
{
var index = data[offset++];
var pt = resource.instance.template.getPropertyTemplateByIndex(index);
list.add(pt, null);
var cs = data.getUint32(offset);
offset += 4;
bagOfBags.add(Codec.parsePropertyValueArray(data, offset, cs, connection));
offset += cs;
}
bagOfBags.seal();
bagOfBags.then(x =>
{
for(var i = 0; i < list.length; i++)
list.values[i] = x[i];
reply.trigger(list);
});
return reply;
}
///
/// Compose resource history
///
/// History
/// DistributedConnection is required to fetch resources.
///
static composeHistory(history, connection, prependLength = false)
{
var rt = new BinaryList();
for (var i = 0; i < history.length; i++)
rt.addUint8(history.keys[i].index).addUint8Array(Codec.composePropertyValueArray(history.values[i], connection, true));
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
///
/// Parse an array of ProperyValue.
///
/// Array of bytes.
/// Zero-indexed offset.
/// Number of bytes to parse.
/// DistributedConnection is required to fetch resources.
///
static parsePropertyValueArray(data, offset, length, connection)
{
var rt = new AsyncBag();
while (length > 0)
{
var cs = {};
rt.add(Codec.parsePropertyValue(data, offset, cs, connection));
if (cs.size > 0)
{
offset += cs.size;
length -= cs.size;
}
else
throw new Exception("Error while parsing ValueInfo structured data");
}
rt.seal();
return rt;
}
static parseStructure(data, offset, contentLength, connection, metadata = null, keys = null, types = null)
{
var reply = new AsyncReply();
var bag = new AsyncBag();
var keylist = [];
var typelist = [];
if (keys == null) {
while (contentLength > 0) {
var len = data[offset++];
keylist.push(data.getString(offset, len));
offset += len;
typelist.push(data[offset]);
var rt = {};
bag.add(Codec.parse(data, offset, rt, connection));
contentLength -= rt.size + len + 1;
offset += rt.size;
}
}
else if (types == null) {
for (var i = 0; i < keys.length; i++)
keylist.push(keys[i]);
while (contentLength > 0) {
typelist.push(data[offset]);
var rt = {};
bag.add(Codec.parse(data, offset, rt, connection));
contentLength -= rt.size;
offset += rt.size;
}
}
else {
for (var i = 0; i < keys.length; i++) {
keylist.push(keys[i]);
typelist.push(types[i]);
}
var i = 0;
while (contentLength > 0) {
var rt = {};
bag.add(Codec.parse(data, offset, rt, connection, types[i]));
contentLength -= rt.size;
offset += rt.size;
i++;
}
}
bag.seal();
bag.then(function (res) {
// compose the list
var s = new Structure();
for (var i = 0; i < keylist.length; i++)
s[keylist[i]] = res[i];
reply.trigger(s);
});
if (metadata != null)
{
metadata.keys = keylist;
metadata.types = typelist;
}
return reply;
}
static parseVarArray(data, offset, contentLength, connection) {
var rt = new AsyncBag();
while (contentLength > 0) {
var cs = {};
rt.add(Codec.parse(data, offset, cs, connection));
if (cs.size > 0) {
offset += cs.size;
contentLength -= cs.size;
}
else
throw new Exception("Error while parsing structured data");
}
rt.seal();
return rt;
}
static compose(value, connection, prependType = true) {
if (value instanceof Function)
value = value(connection);
else if (value instanceof DistributedPropertyContext)
value = value.method(this);
var type = Codec.getDataType(value, connection);
var rt = new BinaryList();
switch (type) {
case DataType.Void:
// nothing to do;
break;
case DataType.String:
var st = DC.stringToBytes(value);
rt.addUint32(st.length).addUint8Array(st);
break;
case DataType.Resource:
rt.addUint32(value._p.instanceId);
break;
case DataType.DistributedResource:
// rt.addUint8Array(DC.stringToBytes(value.instance.template.classId)).addUint32(value.instance.id);
rt.addUint32(value.instance.id);
break;
case DataType.Structure:
rt.addUint8Array(Codec.composeStructure(value, connection, true, true, true));
break;
case DataType.VarArray:
rt.addUint8Array(Codec.composeVarArray(value, connection, true));
break;
case DataType.ResourceArray:
rt.addUint8Array(Codec.composeResourceArray(value, connection, true));
break;
case DataType.StructureArray:
rt.addUint8Array(Codec.composeStructureArray(value, connection, true));
break;
default:
rt.add({type: type, value: value});
if (DataType.isArray(type))
rt.addUint32(rt.length, 0);
break;
}
if (prependType)
rt.addUint8(type, 0);
return rt.toArray();
}
static composeVarArray(array, connection, prependLength = false) {
var rt = new BinaryList();
for (var i = 0; i < array.length; i++)
rt.addUint8Array(Codec.compose(array[i], connection));
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
static composeStructure(value, connection, includeKeys = true, includeTypes = true, prependLength = false) {
var rt = new BinaryList();
var keys = value.getKeys();
if (includeKeys) {
for (var i = 0; i < keys.length; i++) {
var key = DC.stringToBytes(keys[i]);
rt.addUint8(key.length).addUint8Array(key).addUint8Array(Codec.compose(value[keys[i]], connection));
}
}
else {
for (var i = 0; i < keys.length; i++)
rt.addUint8Array(Codec.compose(value[keys[i]], connection, includeTypes));
}
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
static composeStructureArray(structures, connection, prependLength = false) {
if (structures == null || structures.length == 0 || !(structures instanceof StructureArray))
return new DC(0);
var rt = new BinaryList();
var comparision = StructureComparisonResult.Structure;
rt.addUint8(comparision);
rt.addUint8Array(Codec.composeStructure(structures[0], connection));
for (var i = 1; i < structures.Length; i++) {
comparision = Codec.compareStructure(structures[i - 1], structures[i], connection);
rt.addUint8(comparision);
if (comparision == StructureComparisonResult.Structure)
rt.addUint8Array(Codec.composeStructure(structures[i], connection));
else if (comparision == StructureComparisonResult.StructureSameKeys)
rt.addUint8Array(Codec.composeStructure(structures[i], connection, false));
else if (comparision == StructureComparisonResult.StructureSameTypes)
rt.addUint8Array(Codec.composeStructure(structures[i], connection, false, false));
}
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
static compareStructure(previous, next, connection) {
if (next == null)
return StructureComparisonResult.Null;
if (previous == null)
return StructureComparisonResult.Structure;
if (next == previous)
return StructureComparisonResult.Same;
if (previous.length != next.length)
return StructureComparisonResult.Structure;
var previousKeys = previous.getKeys();
var nextKeys = next.getKeys();
for (var i = 0; i < previousKeys.length; i++)
if (previousKeys[i] != nextKeys[i])
return StructureComparisonResult.Structure;
var previousTypes = Codec.getStructureDateTypes(previous, connection);
var nextTypes = Codec.getStructureDateTypes(next, connection);
for (var i = 0; i < previousTypes.length; i++)
if (previousTypes[i] != nextTypes[i])
return StructureComparisonResult.StructureSameKeys;
return StructureComparisonResult.StructureSameTypes;
}
static getStructureDateTypes(structure, connection) {
var keys = structure.getKeys();
var types = [];
for (var i = 0; i < keys.length; i++)
types.push(Codec.getDataType(structure[keys[i]], connection));
return types;
}
static isLocalResource(resource, connection) {
if (resource instanceof DistributedResource)
if (resource._p.connection == connection)
return true;
return false;
}
static composeResource(resource, connection) {
if (Codec.isLocalResource(resource, connection))
return BL().addUint32(resource.id);
else {
return BL().addUint8Array(resource.instance.template.classId.value).addUint32(resource.instance.id);
}
}
static compareResource(previous, next, connection) {
if (next == null)
return ResourceComparisonResult.Null;
else if (next == previous)
return ResourceComparisonResult.Same;
else if (Codec.isLocalResource(next, connection))
return ResourceComparisonResult.Local;
else
return ResourceComparisonResult.Distributed;
}
static composeResourceArray(resources, connection, prependLength = false) {
if (resources == null || resources.length == 0)// || !(resources instanceof ResourceArray))
return prependLength ? new DC(4) : new DC(0);
var rt = new BinaryList();
var comparsion = Codec.compareResource(null, resources[0], connection);
rt.addUint8(comparsion);
if (comparsion == ResourceComparisonResult.Local)
rt.addUint32(resources[0]._p.instanceId);
else if (comparsion == ResourceComparisonResult.Distributed)
rt.addUint32(resources[0].instance.id);
for (var i = 1; i < resources.Length; i++)
{
comparsion = Codec.compareResource(resources[i - 1], resources[i], connection);
rt.addUint8(comparsion);
if (comparsion == ResourceComparisonResult.Local)
rt.addUint32(resources[i]._p.instanceId);
else if (comparsion == ResourceComparisonResult.Distributed)
rt.addUint32(resources[i].instance.id);
}
if (prependLength)
rt.addUint32(rt.length, 0);
return rt.toArray();
}
static getDataType(value) {
switch (typeof value) {
case "number":
// float or ?
if (Math.floor(value) == value) {
if (value > 0) {
// larger than byte ?
if (value > 0xFF) {
// larger than short ?
if (value > 0xFFFF) {
// larger than int ?
if (value > 0xFFFFFFFF) {
return DataType.UInt64;
}
else {
return DataType.UInt32;
}
}
else {
return DataType.UInt16;
}
}
else {
return DataType.UInt8;
}
}
else {
if (value < -128) {
if (value < -32768) {
if (value < -2147483648) {
return DataType.Int64;
}
else {
return DataType.Int32;
}
}
else {
return DataType.Int16;
}
}
else {
return DataType.Int8;
}
}
}
else {
// float or double
return DataType.Float64;
}
break;
case "string":
return DataType.String;
case "boolean":
return DataType.Bool;
case "object":
if (value instanceof Array) {
return DataType.VarArray;
}
else if (value instanceof IResource) {
return Codec.isLocalResource(value, connection) ? DataType.Resource : DataType.DistributedResource;
}
else if (value instanceof Date) {
return DataType.DateTime;
}
else if (value instanceof Uint8Array
|| value instanceof ArrayBuffer) {
return DataType.UInt8Array;
}
else if (value instanceof Number) {
// JS numbers are always 64-bit float
return DataType.Float64;
}
else if (value instanceof Structure) {
return DataType.Structure;
}
else {
return DataType.Void
}
break;
default:
return DataType.Void;
}
}
///
/// Parse an array of structures
///
/// Bytes array
/// Zero-indexed offset
/// Number of bytes to parse
/// DistributedConnection is required in case a structure in the array holds items at the other end
/// Array of structures
static parseStructureArray(data, offset, length, connection)
{
var reply = new AsyncBag();
if (length == 0)
{
reply.seal();
return reply;
}
var end = offset + length;
var result = data[offset++];
var previous = null;
//var previousKeys = [];
//var previousTypes = [];
var metadata = {keys: null, types: null};
if (result == StructureComparisonResult.Null)
previous = new AsyncReply(null);
else if (result == StructureComparisonResult.Structure)
{
var cs = data.getUint32(offset);
offset += 4;
previous = this.parseStructure(data, offset, cs, connection, metadata);
offset += cs;
}
reply.add(previous);
while (offset < end)
{
result = data[offset++];
if (result == StructureComparisonResult.Null)
previous = new AsyncReply(null);
else if (result == StructureComparisonResult.Structure)
{
var cs = data.getUint32(offset);
offset += 4;
previous = this.parseStructure(data, offset, cs, connection, metadata);
offset += cs;
}
else if (result == StructureComparisonResult.StructureSameKeys)
{
var cs = data.getUint32(offset);
offset += 4;
previous = this.parseStructure(data, offset, cs, connection, metadata, metadata.keys);
offset += cs;
}
else if (result == StructureComparisonResult.StructureSameTypes)
{
var cs = data.getUint32(offset);
offset += 4;
previous = this.parseStructure(data, offset, cs, connection, metadata, metadata.keys, metadata.types);
offset += cs;
}
reply.add(previous);
}
reply.seal();
return reply;
}
}