304 lines
7.9 KiB
JavaScript
304 lines
7.9 KiB
JavaScript
const encoder = new TextEncoder();
|
|
|
|
export default class Schema {
|
|
|
|
$$size = 0;
|
|
|
|
specification;
|
|
|
|
static viewGetMethods = {
|
|
uint8: 'getUint8',
|
|
int8: 'getInt8',
|
|
uint16: 'getUint16',
|
|
int16: 'getInt16',
|
|
uint32: 'getUint32',
|
|
int32: 'getInt32',
|
|
float32: 'getFloat32',
|
|
float64: 'getFloat64',
|
|
int64: 'getBigInt64',
|
|
uint64: 'getBigUint64',
|
|
};
|
|
|
|
static viewSetMethods = {
|
|
uint8: 'setUint8',
|
|
int8: 'setInt8',
|
|
uint16: 'setUint16',
|
|
int16: 'setInt16',
|
|
uint32: 'setUint32',
|
|
int32: 'setInt32',
|
|
float32: 'setFloat32',
|
|
float64: 'setFloat64',
|
|
int64: 'setBigInt64',
|
|
uint64: 'setBigUint64',
|
|
};
|
|
|
|
constructor(specification) {
|
|
this.specification = this.constructor.normalize(specification);
|
|
}
|
|
|
|
static deserialize(view, offset, specification) {
|
|
const viewGetMethod = this.viewGetMethods[specification.type];
|
|
if (viewGetMethod) {
|
|
const value = view[viewGetMethod](offset.value, true);
|
|
offset.value += this.size(specification);
|
|
return value;
|
|
}
|
|
switch (specification.type) {
|
|
case 'array': {
|
|
const length = view.getUint32(offset.value, true);
|
|
offset.value += 4;
|
|
const value = [];
|
|
for (let i = 0; i < length; ++i) {
|
|
value.push(this.deserialize(view, offset, specification.subtype));
|
|
}
|
|
return value;
|
|
}
|
|
case 'map': {
|
|
const length = view.getUint32(offset.value, true);
|
|
offset.value += 4;
|
|
const value = {};
|
|
for (let i = 0; i < length; ++i) {
|
|
const key = this.deserialize(view, offset, {type: 'string'});
|
|
value[key] = this.deserialize(view, offset, specification.value);
|
|
}
|
|
return value;
|
|
}
|
|
case 'object': {
|
|
const value = {};
|
|
for (const key in specification.properties) {
|
|
value[key] = this.deserialize(view, offset, specification.properties[key]);
|
|
}
|
|
return value;
|
|
}
|
|
case 'string': {
|
|
const length = view.getUint32(offset.value, true);
|
|
offset.value += 4;
|
|
const {buffer, byteOffset} = view;
|
|
const decoder = new TextDecoder();
|
|
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
|
|
offset.value += length;
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
|
|
deserialize(view, offset = 0) {
|
|
const wrapped = {value: offset};
|
|
return this.constructor.deserialize(view, wrapped, this.specification);
|
|
}
|
|
|
|
static defaultValue(specification) {
|
|
if (specification.defaultValue) {
|
|
return specification.defaultValue;
|
|
}
|
|
switch (specification.type) {
|
|
case 'uint8': case 'int8':
|
|
case 'uint16': case 'int16':
|
|
case 'uint32': case 'int32':
|
|
case 'uint64': case 'int64':
|
|
case 'float32': case 'float64': {
|
|
return 0;
|
|
}
|
|
case 'array': {
|
|
return [];
|
|
}
|
|
case 'map': {
|
|
return {};
|
|
}
|
|
case 'object': {
|
|
const object = {};
|
|
for (const key in specification.properties) {
|
|
object[key] = this.defaultValue(specification.properties[key]);
|
|
}
|
|
return object;
|
|
}
|
|
case 'string': {
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
|
|
defaultValue() {
|
|
return this.constructor.defaultValue(this.specification);
|
|
}
|
|
|
|
static normalize(specification) {
|
|
let normalized = specification;
|
|
switch (specification.type) {
|
|
case 'array': {
|
|
normalized.subtype = this.normalize(specification.subtype);
|
|
break;
|
|
}
|
|
case 'map': {
|
|
normalized.value = this.normalize(specification.value);
|
|
break;
|
|
}
|
|
case 'object': {
|
|
for (const key in specification.properties) {
|
|
normalized.properties[key] = this.normalize(specification.properties[key])
|
|
}
|
|
break;
|
|
}
|
|
case 'uint8':
|
|
case 'int8':
|
|
case 'uint16':
|
|
case 'int16':
|
|
case 'uint32':
|
|
case 'int32':
|
|
case 'uint64':
|
|
case 'int64':
|
|
case 'float32':
|
|
case 'float64':
|
|
case 'string': {
|
|
break;
|
|
}
|
|
/* v8 ignore next 2 */
|
|
default:
|
|
throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`);
|
|
}
|
|
return {...normalized, defaultValue: this.defaultValue(normalized)};
|
|
}
|
|
|
|
static serialize(source, view, offset, specification) {
|
|
const viewSetMethod = this.viewSetMethods[specification.type];
|
|
if (viewSetMethod) {
|
|
view[viewSetMethod](offset, source, true);
|
|
return this.size(specification);
|
|
}
|
|
switch (specification.type) {
|
|
case 'array': {
|
|
view.setUint32(offset, source.length, true);
|
|
let arraySize = 4;
|
|
for (const element of source) {
|
|
arraySize += this.serialize(
|
|
element,
|
|
view,
|
|
offset + arraySize,
|
|
specification.subtype,
|
|
);
|
|
}
|
|
return arraySize;
|
|
}
|
|
case 'map': {
|
|
const keys = Object.keys(source);
|
|
view.setUint32(offset, keys.length, true);
|
|
let mapSize = 4;
|
|
for (const key of keys) {
|
|
mapSize += this.serialize(
|
|
key,
|
|
view,
|
|
offset + mapSize,
|
|
{type: 'string'},
|
|
);
|
|
mapSize += this.serialize(
|
|
source[key],
|
|
view,
|
|
offset + mapSize,
|
|
specification.value,
|
|
);
|
|
}
|
|
return mapSize;
|
|
}
|
|
case 'object': {
|
|
let objectSize = 0;
|
|
for (const key in specification.properties) {
|
|
objectSize += this.serialize(
|
|
source[key],
|
|
view,
|
|
offset + objectSize,
|
|
specification.properties[key],
|
|
);
|
|
}
|
|
return objectSize;
|
|
}
|
|
case 'string': {
|
|
const encoder = new TextEncoder();
|
|
const bytes = encoder.encode(source);
|
|
view.setUint32(offset, bytes.length, true);
|
|
offset += 4;
|
|
for (let i = 0; i < bytes.length; ++i) {
|
|
view.setUint8(offset++, bytes[i]);
|
|
}
|
|
return 4 + bytes.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
serialize(source, view, offset = 0) {
|
|
this.constructor.serialize(source, view, offset, this.specification);
|
|
}
|
|
|
|
static sizeOf(concrete, specification) {
|
|
const size = this.size(specification);
|
|
if (size > 0) {
|
|
return size;
|
|
}
|
|
let fullSize = 0;
|
|
const {type} = specification;
|
|
switch (type) {
|
|
case 'array': {
|
|
fullSize += 4;
|
|
for (const element of concrete) {
|
|
fullSize += this.sizeOf(element, specification.subtype);
|
|
}
|
|
break;
|
|
}
|
|
case 'map': {
|
|
fullSize += 4;
|
|
for (const key in concrete) {
|
|
fullSize += this.sizeOf(key, {type: 'string'});
|
|
fullSize += this.sizeOf(concrete[key], specification.value);
|
|
}
|
|
break;
|
|
}
|
|
case 'object': {
|
|
for (const key in specification.properties) {
|
|
fullSize += this.sizeOf(concrete[key], specification.properties[key]);
|
|
}
|
|
break;
|
|
}
|
|
case 'string':
|
|
fullSize += 4;
|
|
fullSize += (encoder.encode(concrete)).length;
|
|
break;
|
|
}
|
|
return fullSize;
|
|
}
|
|
|
|
sizeOf(concrete) {
|
|
return this.constructor.sizeOf(concrete, this.specification);
|
|
}
|
|
|
|
static size(specification) {
|
|
switch (specification.type) {
|
|
case 'array': return 0;
|
|
case 'map': return 0; // TODO could be fixed-size w/ fixed-size key
|
|
case 'object': {
|
|
let size = 0;
|
|
for (const key in specification.properties) {
|
|
const propertySize = this.size(specification.properties[key]);
|
|
if (0 === propertySize) {
|
|
return 0;
|
|
}
|
|
size += propertySize;
|
|
}
|
|
return size;
|
|
}
|
|
case 'uint8': case 'int8': {
|
|
return 1;
|
|
}
|
|
case 'uint16': case 'int16': {
|
|
return 2;
|
|
}
|
|
case 'uint32': case 'int32': case 'float32': {
|
|
return 4;
|
|
}
|
|
case 'uint64': case 'int64': case 'float64': {
|
|
return 8;
|
|
}
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
}
|