diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f0aa14a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Ignore artifacts: +package-lock.json +dist +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ba4e1a9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true +} + diff --git a/package-lock.json b/package-lock.json index d7c7a02..e248e3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@vitest/coverage-v8": "^3.2.4", "esbuild": "^0.25.8", + "prettier": "^3.6.2", "rollup": "^4.46.2", "rollup-plugin-dts": "^6.2.1", "vitest": "^3.2.4" @@ -1718,6 +1719,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", diff --git a/package.json b/package.json index f7e4446..a0bc308 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ "scripts": { "build": "npx esbuild src/index.js --bundle --minify --outfile=dist/index.js --format=esm && npx rollup -c", "test": "vitest", - "cover": "vitest --coverage" + "cover": "vitest --coverage", + "format": "prettier . --write" }, "devDependencies": { "@vitest/coverage-v8": "^3.2.4", "esbuild": "^0.25.8", + "prettier": "^3.6.2", "rollup": "^4.46.2", "rollup-plugin-dts": "^6.2.1", "vitest": "^3.2.4" diff --git a/src/ConstArray.js b/src/ConstArray.js index 3008616..d29d030 100644 --- a/src/ConstArray.js +++ b/src/ConstArray.js @@ -1,4 +1,4 @@ -import { parse, serialize, sizeofHead, Type } from "." +import { isHeadless, isSerializableType, parse, serialize, sizeof, sizeofHead, Type } from "." export function ConstArray(size) { const obj = { _size: size } @@ -7,39 +7,82 @@ export function ConstArray(size) { return obj } ConstArray.prototype.serialize = function(dv, src, ...inner_types) { - const item_size = sizeofHead(src[0]) + const item_headless = isHeadless(...inner_types) + const item_head_size = sizeofHead(src[0], ...inner_types) const size = this._size - if (dv.byteLength < size * item_size) { - throw new Error('too small buffer') + if (dv.byteLength < this.sizeof(src, ...inner_types)) { + throw new Error(this.name_+ ', ' + inner_types.join(', ') + ' too small buffer') } if (src.length != size) { - throw new Error('array should be ' + size + ' elements length') + throw new Error(this.name_+ ', ' + inner_types.join(', ') + ' should be ' + size + ' elements length') } + let offset = item_head_size * size + for (let i = 0; i < size; i++) { - const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i) - serialize(item_frame, src[i], ...inner_types) + const item_head_frame = new DataView(dv.buffer, dv.byteOffset + item_head_size * i) + if (item_headless) { + item_head_frame.setUint32(0, offset) + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + serialize(item_frame, src[i], ...inner_types) + offset += sizeof(src[i], ...inner_types) + } else { + serialize(item_head_frame, src[i], ...inner_types) + } } return } -ConstArray.prototype.parse = function(dv, ...inner_types) { +ConstArray.prototype.parse = function(dv, ...inner_types) { + const item_headless = isHeadless(...inner_types) + const item_head_size = sizeofHead(...inner_types) const size = this._size - const item_size = sizeofHead(...inner_types) const array = Array(size) for (let i = 0; i < size; i++) { - const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i) - array[i] = parse(item_frame, ...inner_types) + const item_head_frame = new DataView(dv.buffer, dv.byteOffset + item_head_size * i) + if (item_headless) { + const offset = item_head_frame.getUint32(0) + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + array[i] = parse(item_frame, ...inner_types) + } else { + array[i] = parse(item_head_frame, ...inner_types) + } } return array } -ConstArray.prototype.isHeadless = function() { - return false +ConstArray.prototype.isHeadless = function(...inner_types) { + return isHeadless(...inner_types) } -ConstArray.prototype.sizeof = function(...inner_types) { - return sizeofHead(...inner_types) * this._size +ConstArray.prototype.sizeof = function(arg, ...args) { + let inner_types + let src + + if (isSerializableType(arg)) { + src = undefined + inner_types = [arg, ...args] + } else { + src = arg + inner_types = args + } + + const fixed_size = sizeofHead(...inner_types) * this._size + + if (isHeadless(...inner_types)) { + if (src == undefined) { + throw new Error('unknown sizeof ' + this._name + ', ' + inner_types.join(',')) + } + + let variable_size = 0 + for (const item of src) { + variable_size += sizeof(item, ...inner_types) + } + + return fixed_size + variable_size + } else { + return fixed_size + } } Object.setPrototypeOf(ConstArray.prototype, Type.prototype) Object.freeze(ConstArray.prototype) diff --git a/src/ConstDataView.js b/src/ConstDataView.js index 430b90e..b8d381e 100644 --- a/src/ConstDataView.js +++ b/src/ConstDataView.js @@ -8,10 +8,10 @@ export function ConstDataView(size) { } ConstDataView.prototype.serialize = function(dv, src) { if (dv.byteLength < this._size) { - throw new Error('too small buffer') + throw new Error(this._name + ' too small buffer') } if (src.byteLength != this._size) { - throw new Error('buffer should be ' + this._size + ' bytes length') + throw new Error(this._name + ' should be ' + this._size + ' bytes length') } memcpy(dv, src) return diff --git a/src/ConstString.js b/src/ConstString.js index db9e28c..33ae5ce 100644 --- a/src/ConstString.js +++ b/src/ConstString.js @@ -12,10 +12,10 @@ ConstString.prototype.serialize = function(dv, src) { const encoded = new DataView(encoder.encode(src).buffer, 0, this._size) if (dv.byteLength < encoded.byteLength) { - throw new Error('too small buffer') + throw new Error(this._name + ' too small buffer') } if (src.length != this._size) { - throw new Error('string should be ' + this._size + ' symbols length') + throw new Error(this._name + ' should be ' + this._size + ' symbols length') } memcpy(dv, encoded) diff --git a/src/Int.js b/src/Int.js index 669c4e3..7d2cf47 100644 --- a/src/Int.js +++ b/src/Int.js @@ -27,7 +27,7 @@ export function Int(bits, sign) { break default: - throw new Error('incorrect bits ' + bits) + throw new Error(obj._name + ' incorrect bits ' + bits) } break @@ -52,25 +52,25 @@ export function Int(bits, sign) { break default: - throw new Error('incorrect bits ' + bits) + throw new Error(obj._name + ' incorrect bits ' + bits) } break default: - throw new Error('incorrect sign ' + sign) + throw new Error(obj._name + ' incorrect sign ' + sign) } return obj } Int.prototype.serialize = function (dv, src) { if (dv.byteLength < this._size) { - throw new Error('buffer is too small') + throw new Error(this._name + ' buffer is too small') } if (src > this._limits.MAX_VALUE) { - throw new Error(`number should be less or equal than ` + this._limits.MAX_VALUE) + throw new Error(this._name + ` should be less or equal than ` + this._limits.MAX_VALUE) } if (src < this._limits.MIN_VALUE) { - throw new Error(`number should be more or equal than ` + this._limits.MIN_VALUE) + throw new Error(this._name + ` should be more or equal than ` + this._limits.MIN_VALUE) } this._dv_set.call(dv, 0, src) } diff --git a/src/index.js b/src/index.js index 5198741..b1f76c4 100644 --- a/src/index.js +++ b/src/index.js @@ -14,14 +14,14 @@ export function serialize(dv, src, ...types) { if (type == Boolean && typeof src == 'boolean') { if (dv.byteLength < 1) { - throw new Error('too small buffer') + throw new Error('Boolean too small buffer') } dv.setUint8(0, src ? 1 : 0) return } if (type == Number && typeof src == 'number') { if (dv.byteLength < 8) { - throw new Error('too small buffer') + throw new Error('Number too small buffer') } dv.setFloat64(0, src) return @@ -31,10 +31,10 @@ export function serialize(dv, src, ...types) { let encoded = new DataView(encoder.encode(src).buffer) if (dv.byteLength < 4 + encoded.byteLength) { - throw new Error('too small buffer') + throw new Error('String too small buffer') } if (encoded.byteLength > limits.u32.MAX_VALUE) { - throw new Error('string is too long') + throw new Error('String is too long') } dv.setUint32(0, encoded.byteLength) @@ -44,31 +44,40 @@ export function serialize(dv, src, ...types) { return } if (type == Array && Array.isArray(src)) { - - const item_size = sizeofHead(src[0]) + const item_headless = isHeadless(...inner_types) + const item_head_size = sizeofHead(...inner_types) const size = src.length - if (dv.byteLength < 4 + size * item_size) { - throw new Error('too small buffer') + if (dv.byteLength < sizeof(src, ...types)) { + throw new Error('Array, ' + inner_types.join(', ') + ' too small buffer') } if (size > limits.u32.MAX_VALUE) { - throw new Error('array is too long') + throw new Error('Array, ' + inner_types.join(', ') + ' is too long') } dv.setUint32(0, size) + let offset = 4 + item_head_size * size + for (let i = 0; i < size; i++) { - const item_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_size * i) - serialize(item_frame, src[i], ...inner_types) + const item_head_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_head_size * i) + if (item_headless) { + item_head_frame.setUint32(0, offset) + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + serialize(item_frame, src[i], ...inner_types) + offset += sizeof(src[i], ...inner_types) + } else { + serialize(item_head_frame, src[i], ...inner_types) + } } return } if (type == DataView && src instanceof DataView) { if (dv.byteLength < 4 + src.byteLength) { - throw new Error('too small buffer') + throw new Error('DataView too small buffer') } if (src.byteLength > limits.u32.MAX_VALUE) { - throw new Error('data view is too long') + throw new Error('DataView data is too long') } dv.setUint32(0, src.byteLength) @@ -99,13 +108,20 @@ export function parse(dv, ...types) { return decoder.decode(frame) } if (type == Array) { + const item_headless = isHeadless(...inner_types) + const item_head_size = sizeofHead(...inner_types) const size = dv.getUint32(0) - const item_size = sizeofHead(inner_types[0]) const array = Array(size) for (let i = 0; i < size; i++) { - const item_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_size * i) - array[i] = parse(item_frame, ...inner_types) + const item_head_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_head_size * i) + if (item_headless) { + const offset = item_head_frame.getUint32(0) + const item_frame = new DataView(dv.buffer, dv.byteOffset + offset) + array[i] = parse(item_frame, ...inner_types) + } else { + array[i] = parse(item_head_frame, ...inner_types) + } } return array @@ -148,29 +164,50 @@ export function sizeofHead(...args) { export function sizeof(...args) { const [arg, ...inner_args] = args + const [arg2, ...inner_args2] = inner_args - if (arg == Boolean || typeof arg == 'boolean') { + if (arg == Boolean || arg2 == Boolean && typeof arg == 'boolean') { return 1 } - if (arg == Number || typeof arg == 'number') { + if (arg == Number || arg2 == Number && typeof arg == 'number') { return 8 } - if (typeof arg == 'string') { + if (arg2 == String && typeof arg == 'string') { const encoder = new TextEncoder('utf-8') return 4 + encoder.encode(arg).byteLength } - if (Array.isArray(arg)) { - return 4 + sizeofHead(arg[0]) * arg.length + if (arg2 == Array && Array.isArray(arg)) { + + const fixed_size = 4 + sizeofHead(...inner_args2) * arg.length + if (isHeadless(...inner_args2)) { + + let variable_size = 0 + for (const item of arg) { + variable_size += sizeof(item, ...inner_args2) + } + + return fixed_size + variable_size + } else { + return fixed_size + } } - if (arg instanceof DataView) { + if (arg2 == DataView && arg instanceof DataView) { return 4 + arg.byteLength } if (arg instanceof Type) { return arg.sizeof(...inner_args) } - const [arg2, ...inner_args2] = inner_args if (arg2 instanceof Type) { return arg2.sizeof(arg, ...inner_args2) } - throw new Error('unknown size of ' + arg) + throw new Error('unknown size of ' + args) +} + +export function isSerializableType(type) { + return type == Boolean || + type == Number || + type == String || + type == Array || + type == DataView || + type instanceof Type } diff --git a/test/ConstArray.test.js b/test/ConstArray.test.js index 5e6459f..43e61a9 100644 --- a/test/ConstArray.test.js +++ b/test/ConstArray.test.js @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { ConstArray, parse, serialize, sizeof, sizeofHead } from "../src"; +import { ConstArray, isHeadless, parse, serialize, sizeof, sizeofHead } from "../src"; import { expectDataViewEqual, filledDataView, sizedDataView } from "."; describe(ConstArray.name, () => { @@ -33,4 +33,41 @@ describe(ConstArray.name, () => { test('sizeof', () => { expect(sizeofHead(ConstArray(2), Number)).toEqual(16) }) + + const array_dv = filledDataView([ + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x11, + // hello + 0x00, 0x00, 0x00, 0x05, + 0x68, 0x65, 0x6C, 0x6C, 0x6F, + // sek1ro + 0x00, 0x00, 0x00, 0x06, + 0x73, 0x65, 0x6b, 0x31, 0x72, 0x6f, + ]) + const array = ['hello', 'sek1ro'] + + test('serialize, String', () => { + + const actual = sizedDataView(sizeof(array, ConstArray(2), String)) + + expect(() => sizeof(ConstArray(2), String)).toThrow() + + serialize(actual, array, ConstArray(2), String) + expectDataViewEqual(actual, array_dv) + }) + + test('parse, String', () => { + expect(parse(array_dv, ConstArray(2), String)).toEqual(array) + }) + + test('isHeadless', () => { + expect(isHeadless(ConstArray(2), Number)).toBeFalsy() + expect(isHeadless(ConstArray(2), String)).toBeTruthy() + }) + + test('sizeof', () => { + expect(sizeof(ConstArray(2), Number)).toBe(16) + expect(() => sizeof(ConstArray(2), String)).toThrow() + expect(sizeof(['hello', 'sek1ro'], ConstArray(2), String)).toBe(27) + }) }) diff --git a/test/index.test.js b/test/index.test.js index 2fe8c86..c5de580 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest' import { expectDataViewEqual, filledDataView, sizedDataView } from '.' -import { parse, serialize, sizeof, sizeofHead } from '../src' +import { isHeadless, parse, serialize, sizeof, sizeofHead } from '../src' describe('serialize', () => { test('Boolean', () => { @@ -32,7 +32,7 @@ describe('serialize', () => { let dv = sizedDataView(8) expect(() => serialize(dv, 'hello', String)).toThrow() - dv = sizedDataView(sizeof('hello')) + dv = sizedDataView(sizeof('hello', String)) expect(9).toEqual(dv.byteLength) serialize(dv, 'hello', String) @@ -47,12 +47,30 @@ describe('serialize', () => { let dv = sizedDataView(19) expect(() => serialize(dv, [1, 2], Array, Number)).toThrow() - dv = sizedDataView(sizeof([1, 2])) + dv = sizedDataView(sizeof([1, 2], Array, Number)) expect(20).toEqual(dv.byteLength) serialize(dv, [1, 2], Array, Number) expectDataViewEqual(dv, expected) }) + test('Array, String', () => { + const expected = filledDataView([ + 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x15, + // hello + 0x00, 0x00, 0x00, 0x05, + 0x68, 0x65, 0x6C, 0x6C, 0x6F, + // sek1ro + 0x00, 0x00, 0x00, 0x06, + 0x73, 0x65, 0x6b, 0x31, 0x72, 0x6f, + ]) + const array = ['hello', 'sek1ro'] + const actual = sizedDataView(sizeof(array, Array, String)) + + serialize(actual, array, Array, String) + expectDataViewEqual(actual, expected) + }) test('DataView', () => { const expected = filledDataView([ 0x00, 0x00, 0x00, 0x04, @@ -100,6 +118,22 @@ describe('parse', () => { const actual = parse(dv, Array, Number) expect(actual).toEqual(expected) }) + test('Array, String', () => { + const expected = ['hello', 'sek1ro'] + const dv = filledDataView([ + 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x15, + // hello + 0x00, 0x00, 0x00, 0x05, + 0x68, 0x65, 0x6C, 0x6C, 0x6F, + // sek1ro + 0x00, 0x00, 0x00, 0x06, + 0x73, 0x65, 0x6b, 0x31, 0x72, 0x6f, + ]) + const actual = parse(dv, Array, String) + expect(actual).toEqual(expected) + }) test('DataView', () => { const expected = filledDataView([ 0x00, 0x01, 0x02, 0x03, @@ -113,22 +147,28 @@ describe('parse', () => { }) }) +describe('isHeadless', () => { + test('Array', () => { + expect(isHeadless(Array, Number)).toBeTruthy() + expect(isHeadless(Array, String)).toBeTruthy() + }) +}) + describe('sizeofHead', () => { + test('Boolean', () => { + expect(sizeofHead(Boolean)).toEqual(1) + }) test('Number', () => { expect(sizeofHead(Number)).toEqual(8) - expect(sizeofHead(1, Number)).toEqual(8) }) test('String', () => { expect(sizeofHead(String)).toEqual(4) - expect(sizeofHead('s', String)).toEqual(4) }) test('Array', () => { expect(sizeofHead(Array, Number)).toEqual(4) - expect(sizeofHead([1], Array, Number)).toEqual(4) }) test('DataView', () => { expect(sizeofHead(DataView)).toEqual(4) - expect(sizeofHead(filledDataView([0x01]), DataView)).toEqual(4) }) }) diff --git a/types/index.d.ts b/types/index.d.ts index 30106e9..df65d0a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -28,14 +28,6 @@ export function serialize(dv: DataView, src: Serializable, ...types: Serializabl */ export function parse(dv: DataView, ...types: SerializableType[]): Serializable; -/** - * some types, like Array, String, has no fixed size. So in Structure they are stored as u32 offset, which points to their beginning - * @param {Serializable} obj to check - * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number - * @returns {boolean} - */ -export function isHeadless(obj: Serializable, ...types: SerializableType[]): boolean; - /** * some types, like Array, String, has no fixed size. So in Structure they are stored as u32 offset, which points to their beginning * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number @@ -43,14 +35,6 @@ export function isHeadless(obj: Serializable, ...types: SerializableType[]): boo */ export function isHeadless(...types: SerializableType[]): boolean; -/** - * if obj has no fixed size, return 4 (sizeof u32 offset) - * @param {Serializable} obj to check - * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number - * @returns {number} - */ -export function sizeofHead(obj: Serializable, ...types: SerializableType[]): number; - /** * if obj has no fixed size, return 4 (sizeof u32 offset) * @param {SerializableType[]} ...types primary and inner types. eg: Array, Number @@ -71,3 +55,5 @@ export function sizeof(obj: Serializable, ...types: SerializableType[]): number; * @throws {Error} if passed Array, String, DataView type (unknown sizeof) */ export function sizeof(...types: SerializableType[]): number; + +export function isSerializableType(type: unknown): boolean;