feat(Sequence)

This commit is contained in:
2025-08-08 11:28:49 +03:00
parent ebe15d2d05
commit c5c0c17db2
6 changed files with 153 additions and 1 deletions

91
src/Sequence.js Normal file
View File

@ -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)

View File

@ -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')
}

View File

@ -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,
}

43
test/Sequence.test.js Normal file
View File

@ -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)
})
})

7
types/Sequence.d.ts vendored Normal file
View File

@ -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

2
types/index.d.ts vendored
View File

@ -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,
}