From 504bfbeae8c54a47a67f8f1f0aff627b1222ff3e Mon Sep 17 00:00:00 2001 From: SEK1RO Date: Mon, 4 Aug 2025 15:33:12 +0300 Subject: [PATCH] feat(DataView, ConstDataView) --- src/ConstArray.js | 9 ++------- src/ConstDataView.js | 31 +++++++++++++++++++++++++++++ src/index.js | 35 ++++++++++++++++++++++++++++++--- test/ConstArray.test.js | 6 +++--- test/ConstDataView.test.js | 31 +++++++++++++++++++++++++++++ test/ConstString.test.js | 6 +++--- test/Struct.test.js | 14 ++++++------- test/index.js | 25 ++++++++++++++++++++++-- test/index.test.js | 40 +++++++++++++++++++++++++++++++------- types/ConstDataView.d.ts | 8 ++++++++ types/index.d.ts | 11 ++++++----- 11 files changed, 179 insertions(+), 37 deletions(-) create mode 100644 src/ConstDataView.js create mode 100644 test/ConstDataView.test.js create mode 100644 types/ConstDataView.d.ts diff --git a/src/ConstArray.js b/src/ConstArray.js index 543dc48..3b5abd6 100644 --- a/src/ConstArray.js +++ b/src/ConstArray.js @@ -1,4 +1,4 @@ -import { limits, parse, serialize, sizeofHead, Type } from "." +import { parse, serialize, sizeofHead, Type } from "." export function ConstArray(size) { const obj = { _size: size } @@ -8,16 +8,11 @@ export function ConstArray(size) { } ConstArray.prototype.serialize = function(dv, src, ...inner_types) { const item_size = sizeofHead(src[0]) - let size = this._size + const size = this._size if (dv.byteLength < size * item_size) { throw new Error('too small buffer') } - if (size > limits.u32.MAX_VALUE) { - throw new Error('array is too long') - } - - dv.setUint32(0, size) for (let i = 0; i < size; i++) { const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i) diff --git a/src/ConstDataView.js b/src/ConstDataView.js new file mode 100644 index 0000000..422846d --- /dev/null +++ b/src/ConstDataView.js @@ -0,0 +1,31 @@ +import { memcpy, Type } from "." + +export function ConstDataView(size) { + const obj = { _size: size } + Object.setPrototypeOf(obj, ConstDataView.prototype) + obj.new(ConstDataView, arguments) + return obj +} +ConstDataView.prototype.serialize = function(dv, src) { + if (dv.byteLength < this._size) { + throw new Error('too small buffer') + } + memcpy(dv, src) + return +} +ConstDataView.prototype.parse = function(dv) { + const res_buffer = new ArrayBuffer(this._size) + const res_dv = new DataView(res_buffer) + + memcpy(res_dv, dv) + + return res_dv +} +ConstDataView.prototype.isHeadless = function() { + return false +} +ConstDataView.prototype.sizeof = function() { + return this._size +} +Object.setPrototypeOf(ConstDataView.prototype, Type.prototype) +Object.freeze(ConstDataView.prototype) diff --git a/src/index.js b/src/index.js index 89599c9..416a5c7 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,11 @@ import { limits } from "./limits" import { memcpy } from "./mem" import { Type } from "./Type" import { ConstString } from './ConstString' -import { ConstArray } from "./ConstArray" +import { ConstArray } from "./ConstArray" +import { ConstDataView } from './ConstDataView' import { Struct } from './Struct' -export { limits, memcpy, Type, ConstString, ConstArray, Struct } +export { limits, memcpy, Type, ConstString, ConstArray, ConstDataView, Struct } export function serialize(dv, src, ...types) { const [type, ...inner_types] = types @@ -54,6 +55,19 @@ export function serialize(dv, src, ...types) { } return } + if (type == DataView && src instanceof DataView) { + if (dv.byteLength < 4 + src.byteLength) { + throw new Error('too small buffer') + } + if (src.byteLength > limits.u32.MAX_VALUE) { + throw new Error('data view is too long') + } + + dv.setUint32(0, src.byteLength) + + const frame = new DataView(dv.buffer, dv.byteOffset + 4) + memcpy(frame, src) + } if (type instanceof Type) { type.serialize(dv, src, ...inner_types) return @@ -84,6 +98,16 @@ export function parse(dv, ...types) { return array } + if (type == DataView) { + const size = dv.getUint32(0) + const res_buffer = new ArrayBuffer(size) + const res_dv = new DataView(res_buffer) + + const frame = new DataView(dv.buffer, dv.byteOffset + 4) + memcpy(res_dv, frame) + + return res_dv + } if (type instanceof Type) { return type.parse(dv, ...inner_types) } @@ -96,8 +120,10 @@ export function isHeadless(...args) { } return arg == Array || arg == String || + arg == DataView || Array.isArray(arg) || - typeof arg == 'string' + typeof arg == 'string' || + arg instanceof DataView } export function sizeofHead(...args) { @@ -121,6 +147,9 @@ export function sizeof(...args) { if (Array.isArray(arg)) { return 4 + sizeofHead(arg[0]) * arg.length } + if (arg instanceof DataView) { + return 4 + arg.byteLength + } if (arg instanceof Type) { return arg.sizeof(...inner_args) } diff --git a/test/ConstArray.test.js b/test/ConstArray.test.js index e3900de..afbb704 100644 --- a/test/ConstArray.test.js +++ b/test/ConstArray.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { ConstArray, parse, serialize, sizeof, sizeofHead } from "../src"; -import { filledDataView, sizedDataView } from "."; +import { expectDataViewEqual, filledDataView, sizedDataView } from "."; describe(ConstArray.name, () => { test('serialize, Number', () => { @@ -15,7 +15,7 @@ describe(ConstArray.name, () => { expect(16).toEqual(dv.byteLength) serialize(dv, [1, 2], ConstArray(2), Number) - expect(dv).toEqual(expected) + expectDataViewEqual(dv, expected) }) test('parse, Number', () => { @@ -25,7 +25,7 @@ describe(ConstArray.name, () => { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]) const actual = parse(dv, ConstArray(2), Number) - expect(actual).toEqual(expected) + expectDataViewEqual(actual, expected) }) test('sizeof', () => { diff --git a/test/ConstDataView.test.js b/test/ConstDataView.test.js new file mode 100644 index 0000000..ab02b70 --- /dev/null +++ b/test/ConstDataView.test.js @@ -0,0 +1,31 @@ +import { describe, expect, test } from "vitest"; +import { ConstDataView, parse, serialize, sizeofHead } from "../src"; +import { expectDataViewEqual, filledDataView } from "."; + +describe(ConstDataView.name, () => { + test('serialize', () => { + const expected = filledDataView([0x00, 0x00, 0x01, 0x02, 0x00, 0x00]) + const dv = filledDataView([0x01, 0x02]) + + const buffer = new ArrayBuffer(6) + let actual = new DataView(buffer, 2, 1) + expect(() => serialize(actual, dv, ConstDataView(2))).toThrow() + + actual = new DataView(buffer, 2, 2) + + serialize(actual, dv, ConstDataView(2)) + expectDataViewEqual(new DataView(actual.buffer), expected) + }) + + test('parse', () => { + const expected = filledDataView([0x01, 0x02]) + const dv = filledDataView([0x00, 0x00, 0x01, 0x02, 0x00, 0x00]) + const frame = new DataView(dv.buffer, dv.byteOffset + 2, 2) + const actual = parse(frame, ConstDataView(2)) + expectDataViewEqual(actual, expected) + }) + + test('sizeof', () => { + expect(sizeofHead(ConstDataView(2))).toEqual(2) + }) +}) diff --git a/test/ConstString.test.js b/test/ConstString.test.js index 47abfe9..93385b7 100644 --- a/test/ConstString.test.js +++ b/test/ConstString.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { ConstString, parse, serialize, sizeofHead } from "../src"; -import { filledDataView, sizedDataView } from "."; +import { expectDataViewEqual, filledDataView, sizedDataView } from "."; describe(ConstString.name, () => { test('serialize', () => { @@ -11,14 +11,14 @@ describe(ConstString.name, () => { dv = sizedDataView(5) serialize(dv, 'hello', ConstString(3)) - expect(dv).toEqual(expected) + expectDataViewEqual(dv, expected) }) test('parse', () => { const expected = 'hel' const dv = filledDataView([0x68, 0x65, 0x6C, 0x6C, 0x6F]) const actual = parse(dv, ConstString(3)) - expect(actual).toEqual(expected) + expectDataViewEqual(actual, expected) }) test('sizeof', () => { diff --git a/test/Struct.test.js b/test/Struct.test.js index cb380e4..a301dc1 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { parse, serialize, sizeof, Struct } from "../src"; -import { filledDataView, sizedDataView } from "."; +import { expectDataViewEqual, filledDataView, sizedDataView } from "."; describe(Struct.name, () => { @@ -20,11 +20,11 @@ describe(Struct.name, () => { expect(21).toEqual(dv.byteLength) serialize(dv, user, User) - expect(dv).toEqual(user_dv) + expectDataViewEqual(dv, user_dv) }) test('parse, headless', () => { - expect(parse(user_dv, User)).toEqual(user) + expectDataViewEqual(parse(user_dv, User), user) }) const Vector2 = Struct({ x: Number, y: Number }) @@ -39,11 +39,11 @@ describe(Struct.name, () => { expect(16).toEqual(dv.byteLength) serialize(dv, vector2, Vector2) - expect(dv).toEqual(vector2_dv) + expectDataViewEqual(dv, vector2_dv) }) test('parse, non-headless', () => { - expect(parse(vector2_dv, Vector2)).toEqual(vector2) + expectDataViewEqual(parse(vector2_dv, Vector2), vector2) }) const Nested = Struct({ user: User, vector2: [Vector2] }) @@ -61,10 +61,10 @@ describe(Struct.name, () => { test('serialize, nested', () => { const dv = sizedDataView(sizeof(nested, Nested)) serialize(dv, nested, Nested) - expect(dv).toEqual(nested_dv) + expectDataViewEqual(dv, nested_dv) }) test('parse, nested', () => { - expect(parse(nested_dv, Nested)).toEqual(nested) + expectDataViewEqual(parse(nested_dv, Nested), nested) }) }) diff --git a/test/index.js b/test/index.js index 8f47e28..e47ea82 100644 --- a/test/index.js +++ b/test/index.js @@ -1,11 +1,13 @@ +import { expect } from "vitest" + export function filledDataView(bytes) { const ab = new ArrayBuffer(bytes.length) const dv = new DataView(ab) - + for (let i = 0; i < bytes.length; i++) { dv.setInt8(i, bytes[i]) } - + return dv } @@ -13,3 +15,22 @@ export function sizedDataView(length) { const ab = new ArrayBuffer(length) return new DataView(ab) } + +export function expectDataViewEqual(actual, expected) { + const actualBytes = new Uint8Array(actual.buffer, actual.byteOffset, actual.byteLength) + const expectedBytes = new Uint8Array(expected.buffer, expected.byteOffset, expected.byteLength) + + if (actualBytes.length !== expectedBytes.length) { + throw new Error(`DataView length mismatch: ${actualBytes.length} !== ${expectedBytes.length}`) + } + + for (let i = 0; i < actualBytes.length; i++) { + if (actualBytes[i] !== expectedBytes[i]) { + throw new Error( + `Byte mismatch at offset ${i}: 0x${actualBytes[i].toString(16)} !== 0x${expectedBytes[i].toString(16)}\n` + + `actual: [${Array.from(actualBytes).map(byte => '0x' + byte.toString(16)).join(', ')}]\n` + + `expected: [${Array.from(expectedBytes).map(byte => '0x' + byte.toString(16)).join(', ')}]` + ) + } + } +} diff --git a/test/index.test.js b/test/index.test.js index 0994429..784cd08 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest' -import { filledDataView, sizedDataView } from '.' +import { expectDataViewEqual, filledDataView, sizedDataView } from '.' import { parse, serialize, sizeof, sizeofHead } from '../src' describe('serialize', () => { @@ -12,7 +12,7 @@ describe('serialize', () => { expect(8).toEqual(dv.byteLength) serialize(dv, 1532.625, Number) - expect(dv).toEqual(expected) + expectDataViewEqual(dv, expected) }) test('String', () => { const expected = filledDataView([ @@ -26,7 +26,7 @@ describe('serialize', () => { expect(9).toEqual(dv.byteLength) serialize(dv, 'hello', String) - expect(dv).toEqual(expected) + expectDataViewEqual(dv, expected) }) test('Array, Number', () => { const expected = filledDataView([ @@ -41,7 +41,22 @@ describe('serialize', () => { expect(20).toEqual(dv.byteLength) serialize(dv, [1, 2], Array, Number) - expect(dv).toEqual(expected) + expectDataViewEqual(dv, expected) + }) + test('DataView', () => { + const expected = filledDataView([ + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, 0x02, 0x03, + ]) + const dv = filledDataView([ + 0x00, 0x01, 0x02, 0x03, + ]) + let actual = sizedDataView(7) + expect(() => serialize(actual, dv, DataView)).toThrow() + + actual = sizedDataView(8) + serialize(actual, dv, DataView) + expectDataViewEqual(actual, expected) }) }) @@ -50,7 +65,7 @@ describe('parse', () => { const expected = 1532.625 const dv = filledDataView([0x40, 0x97, 0xF2, 0x80, 0x00, 0x00, 0x00, 0x00]) const actual = parse(dv, Number) - expect(actual).toEqual(expected) + expectDataViewEqual(actual, expected) }) test('String', () => { const expected = 'hello' @@ -59,7 +74,7 @@ describe('parse', () => { 0x68, 0x65, 0x6C, 0x6C, 0x6F ]) const actual = parse(dv, String) - expect(actual).toEqual(expected) + expectDataViewEqual(actual, expected) }) test('Array, Number', () => { const expected = [1, 2] @@ -69,7 +84,18 @@ describe('parse', () => { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]) const actual = parse(dv, Array, Number) - expect(actual).toEqual(expected) + expectDataViewEqual(actual, expected) + }) + test('DataView', () => { + const expected = filledDataView([ + 0x00, 0x01, 0x02, 0x03, + ]) + const dv = filledDataView([ + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, 0x02, 0x03, + ]) + const actual = parse(dv, DataView) + expectDataViewEqual(actual, expected) }) }) diff --git a/types/ConstDataView.d.ts b/types/ConstDataView.d.ts new file mode 100644 index 0000000..399da99 --- /dev/null +++ b/types/ConstDataView.d.ts @@ -0,0 +1,8 @@ +import { Type } from "."; + +/** + * constructs type of data view with constant byte size + * @param {number} byte_size max byte size + * @returns {Type} + */ +export function ConstDataView(byte_size: number): Type; diff --git a/types/index.d.ts b/types/index.d.ts index 140d393..bc5d21e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,19 +3,20 @@ import { limits } from "./limits"; import { Type } from "./Type" import { ConstString } from "./ConstString" import { ConstArray } from "./ConstArray"; +import { ConstDataView } from "./ConstDataView"; import { Struct } from "./Struct"; -export { memcpy, limits, Type, ConstString, ConstArray, Struct } +export { memcpy, limits, Type, ConstString, ConstArray, ConstDataView, Struct } -export type SerializableType = NumberConstructor | StringConstructor | ArrayConstructor | Type -export type Serializable = number | string | array +export type SerializableType = NumberConstructor | StringConstructor | ArrayConstructor | DataViewConstructor | Type +export type Serializable = number | string | array | DataView /** * @param {DataView} dv destination memory * @param {Serializable} src source object * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number * @throws {Error} if too small buffer - * @throws {Error} if array|string size is higher than limits.u32.MAX_VALUE + * @throws {Error} if array|string|DataView size is higher than limits.u32.MAX_VALUE */ export function serialize(dv: DataView, src: Serializable, ...types: SerializableType[]): void; @@ -66,6 +67,6 @@ export function sizeof(obj: Serializable, ...types: SerializableType[]): number; /** * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number * @returns {number} - * @throws {Error} if passed Array or String type (unknown sizeof) + * @throws {Error} if passed Array, String, DataView type (unknown sizeof) */ export function sizeof(...types: SerializableType[]): number;