diff --git a/src/Sequence.js b/src/Sequence.js new file mode 100644 index 0000000..98c3a16 --- /dev/null +++ b/src/Sequence.js @@ -0,0 +1,91 @@ +import { isHeadless, parse, serialize, sizeof } from '.' +import { Type } from './Type' + +export function Sequence(types) { + const obj = { _types: types, _headless: false, _sizeof: 0 } + Object.setPrototypeOf(obj, Sequence.prototype) + obj.new(Sequence, [ + types.map((arg) => { + if (typeof arg == 'function') { + return arg.name + } else { + return arg + } + }), + ]) + + for (let i = 0; i < types.length; i++) { + if (!Array.isArray(types[i])) { + types[i] = [types[i]] + } + obj._headless |= isHeadless(...types[i]) + if (obj._headless) { + obj._sizeof = null + } else { + obj._sizeof += sizeof(...types[i]) + } + } + + return obj +} +Sequence.prototype._srcValidate = function (src) { + if (!Array.isArray(src) || src.length != this._types.length) { + throw new Error( + this._name + + ' source object should be array with length ' + + this._types.length, + ) + } +} +Sequence.prototype.serialize = function (dv, src) { + if (dv.byteLength < this.sizeof(src)) { + throw new Error(this._name + ' too small buffer') + } + this._srcValidate(src) + + let offset = 0 + for (let i = 0; i < src.length; i++) { + const item = src[i] + const item_types = this._types[i] + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + serialize(item_frame, item, ...item_types) + offset += sizeof(item, ...item_types) + } +} +Sequence.prototype.parse = function (dv) { + const res = new Array(this._types.length) + let offset = 0 + + for (let i = 0; i < res.length; i++) { + const item_types = this._types[i] + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + res[i] = parse(item_frame, ...item_types) + offset += sizeof(res[i], ...item_types) + } + + return res +} +Sequence.prototype.isHeadless = function () { + return this._headless +} +Sequence.prototype.sizeof = function (src) { + if (this._headless) { + if (src == undefined) { + throw new Error('unknown sizeof ' + this._name) + } + this._srcValidate(src) + + let size = 0 + for (let i = 0; i < src.length; i++) { + const item = src[i] + const item_types = this._types[i] + size += sizeof(item, ...item_types) + } + + return size + } else { + return this._sizeof + } +} +Object.setPrototypeOf(Sequence.prototype, Type.prototype) +Object.freeze(Sequence.prototype) diff --git a/src/Type.js b/src/Type.js index 3c08dd3..97331a9 100644 --- a/src/Type.js +++ b/src/Type.js @@ -6,13 +6,20 @@ export function Type() { Type.prototype.new = function (func, args) { this._name = func.name if (args !== undefined) { - const str_args = Array.from(args).map((arg) => JSON.stringify(arg)) + const str_args = Array.from(args).map((arg) => { + if (typeof arg == 'function') { + return arg.name + } else { + return JSON.stringify(arg) + } + }) this._name += '(' + str_args.join(', ') + ')' } } Type.prototype.toString = function () { return this._name } +Type.prototype.toJSON = Type.prototype.toString Type.prototype.serialize = function () { throw new Error('should be overloaded') } diff --git a/src/index.js b/src/index.js index ef67d83..3fd4eb1 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import { Int } from './Int' import { ConstString } from './ConstString' import { ConstArray } from './ConstArray' import { ConstDataView } from './ConstDataView' +import { Sequence } from './Sequence' import { Struct } from './Struct' export { @@ -15,6 +16,7 @@ export { ConstString, ConstArray, ConstDataView, + Sequence, Struct, } diff --git a/test/Sequence.test.js b/test/Sequence.test.js new file mode 100644 index 0000000..4213455 --- /dev/null +++ b/test/Sequence.test.js @@ -0,0 +1,43 @@ +import { describe, expect, test } from "vitest"; +import { ConstString, Int, isHeadless, parse, Sequence, serialize, sizeof } from "../src"; +import { expectDataViewEqual, filledDataView, sizedDataView } from '.' + +describe(Sequence.name, () => { + const Type = Sequence([Int(8, 'unsigned'), String, Boolean]) + const type = [255, 'hello', true] + const type_dv = filledDataView([ + 0xFF, + 0x00, 0x00, 0x00, 0x05, + 0x68, 0x65, 0x6C, 0x6C, 0x6F, + 0x01, + ]) + + test('serialize', () => { + let actual = sizedDataView(10) + expect(() => serialize(actual, type, Type)).toThrow() + + actual = sizedDataView(sizeof(type, Type)) + serialize(actual, type, Type) + + expectDataViewEqual(actual, type_dv) + }) + + test('parse', () => { + expect(parse(type_dv, Type)).toEqual(type) + }) + + test('isHeadless', () => { + expect(isHeadless(Sequence([Number, String]))).toBeTruthy() + expect(isHeadless(Sequence([Number, ConstString(4)]))).toBeFalsy() + }) + + test('sizeof', () => { + expect(sizeof(Sequence([Number, ConstString(4)]))).toEqual(12) + + expect(() => sizeof(Sequence([Number, String]))).toThrow() + expect(() => sizeof([1], Sequence([Number, String]))).toThrow() + expect(() => sizeof([1, 'hello', 3], Sequence([Number, String]))).toThrow() + + expect(sizeof([1, 'hello'], Sequence([Number, String]))).toEqual(17) + }) +}) diff --git a/types/Sequence.d.ts b/types/Sequence.d.ts new file mode 100644 index 0000000..831161a --- /dev/null +++ b/types/Sequence.d.ts @@ -0,0 +1,7 @@ +import { SerializableType, Type } from '.' + +/** + * constructs type of heterogeneous array of arbitrary types. Even if item type is headless, it will be stored directly in the of sequence + * @returns {Type} + */ +export function Sequence(types: (SerializableType | SerializableType[])[]): Type diff --git a/types/index.d.ts b/types/index.d.ts index 9cfe277..da7770e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,6 +5,7 @@ import { Int } from './Int' import { ConstString } from './ConstString' import { ConstArray } from './ConstArray' import { ConstDataView } from './ConstDataView' +import { Sequence } from './Sequence' import { Struct } from './Struct' export { @@ -15,6 +16,7 @@ export { ConstString, ConstArray, ConstDataView, + Sequence, Struct, }