From 64b74097e826578d61d3b354fa6692bdb5ab73c0 Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Tue, 10 Dec 2024 22:15:59 +0100 Subject: [PATCH 1/6] feat: add `ValueSet` to PrePost decorators `@ValueSet` decorator set the value of the decorated property based on a function passed as a parameter. This decorator don't read anything from the binary file and is just used to add more context to a class during the reading. In the following example `@ValueSet` is used to fetch the protocol type name based on an id read in by the binary definition. The `protocol_name` will just appear when the object is serialized and will gave the object more context. ```typescript const ID_TO_NAME = { 1: "Record", ... } class Protocol { @Relation(PrimitiveSymbol.u8) protocol_id: number @ValueSet(_ => ID_TO_NAME[_.protocol_id] || 'UNKNOWN') protocol_name: string } ``` --- src/__tests__/parser.test.ts | 15 +++++++++- src/decorators/prepost.ts | 55 +++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 7946808..01b1aac 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from '@jest/globals' -import { Relation, While, Count, Until, MapTo, Match, Enum, IfThen, Else, Choice, Bitfield, Offset, Endian, Peek, Post, Pre } from '../decorators' +import { Relation, While, Count, Until, MapTo, Match, Enum, IfThen, Else, Choice, Bitfield, Offset, Endian, Peek, Post, Pre, ValueSet } from '../decorators' import { EOF, PrimitiveSymbol, type InstantiableObject } from '../types' import { binread } from '../reader' import { withBinspectorContext } from '../context' @@ -637,6 +637,19 @@ describe('Reading binary definition with PrePost decorators', () => { }) expect(curr.offset()).toStrictEqual(0) }) + it('should set a value into the decorated property without reading anything or declaring ', () => { + class Protocol { + @ValueSet(_ => 0xFF) + value: number + } + + const header = new Uint8Array([0x01, 0x02, 0x03, 0x04]).buffer + const curr = new BinaryReader(header) + expect(binread(curr, Protocol)).toMatchObject({ + value: 0xFF, + }) + expect(curr.offset()).toStrictEqual(0) + }) }) describe('Reading a relation to an empty definition', () => { diff --git a/src/decorators/prepost.ts b/src/decorators/prepost.ts index 9c3af1d..6073e93 100644 --- a/src/decorators/prepost.ts +++ b/src/decorators/prepost.ts @@ -49,9 +49,10 @@ */ import { ClassMetaDescriptor, type PropertyMetaDescriptor, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' import { relationExistsOrThrow } from '../error' -import { ExecutionScope, type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext } from '../types' +import { ExecutionScope, type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext, type DecoratorType, type Context } from '../types' import { type Cursor, type BinaryCursorEndianness, BinaryCursor } from '../cursor' import Meta from '../metadatas' +import { Relation } from './primitive' export const PreFunctionSymbol = Symbol('pre-function') export const PostFunctionSymbol = Symbol('post-function') @@ -628,6 +629,58 @@ export function Endian (endianness: BinaryCursorEndianness | ((instance: T } } +type ValueSetFunction = (instance: This) => Value + +/** + * `@ValueSet` decorator set the value of the decorated property based on + * a function passed as a parameter. + * This decorator don't read anything from the binary file and is just used + * to add more context to a class during the reading. + * + * @example + * + * In the following example `@ValueSet` is used to fetch the protocol type name + * based on an id read in by the binary definition. + * The `protocol_name` will just appear when the object is serialized and will + * gave the object more context. + * + * ```typescript + * const ID_TO_NAME = { + * 1: "Record", + * ... + * } + * + * class Protocol { + * @Relation(PrimitiveSymbol.u8) + * protocol_id: number + * + * @ValueSet(_ => ID_TO_NAME[_.protocol_id] || 'UNKNOWN') + * protocol_name: string + * } + * ``` + * + * @typeParam This The type of the class the decorator is applied to. + * + * @param {ValueSetFunction} setter Function that will store the return value in the decorated property. + * @param {Partial} [opt] Optional configution. + * @returns {DecoratorType} The class or property decorator function. + * + * @throws {@link Primitive.RelationAlreadyDefinedError} if a relation metadata is found. + */ +export function ValueSet (setter: ValueSetFunction, opt?: Partial): DecoratorType { + return function (_: any, context: Context) { + const propertyName = context.name as keyof This + if (!Meta.isFieldDecorated(context.metadata, propertyName)) { + // Create an empty relation that wont be read. + Relation()(_, context) + } + + postFunctionDecoratorFactory ('value-set', (targetInstance) => { + targetInstance[propertyName] = setter(targetInstance) + }, { ...opt, scope: ExecutionScope.OnRead })(_, context) + } +} + /** * usePrePost execute an array of `PrePost` decorator metadata on a target instance. * From b1c92a081730791d1bd90971f4396171d74b0728 Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Tue, 10 Dec 2024 22:18:32 +0100 Subject: [PATCH 2/6] fix: include get/set/accessor as well --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index a4b566a..188502b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,7 +48,7 @@ export type Context = * DecoratorType. */ export type DecoratorType = (target: undefined, context: Context) => void -export type ClassAndPropertyDecoratorContext = ClassDecoratorContext This> | ClassFieldDecoratorContext +export type ClassAndPropertyDecoratorContext = ClassDecoratorContext This> | Context export type ClassAndPropertyDecoratorType = (target: any, context: ClassAndPropertyDecoratorContext) => void /** From 5c1b19986e6d26031707d3cb07f88731da2dc64b Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Wed, 11 Dec 2024 19:46:37 +0100 Subject: [PATCH 3/6] fix(example): drop support for older header format --- example/bitmap/bmp.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/example/bitmap/bmp.ts b/example/bitmap/bmp.ts index af9e336..a1970e8 100644 --- a/example/bitmap/bmp.ts +++ b/example/bitmap/bmp.ts @@ -1,11 +1,8 @@ import { PrimitiveSymbol, Relation, Count, Enum, IfThen, Else, Choice, Matrix, Offset } from '../../src' import { - OS21XBITMAPHEADER, OS22XBITMAPCOREHEADER, OS22XBITMAPHEADER, BITMAPINFOHEADER, BITMAPV2INFOHEADER, BITMAPV3INFOHEADER, BITMAPV4INFOHEADER, BITMAPV5INFOHEADER, + OS22XBITMAPHEADER, BITMAPINFOHEADER, BITMAPV2INFOHEADER, BITMAPV3INFOHEADER, BITMAPV4INFOHEADER, BITMAPV5INFOHEADER, } from './header' import { printColour } from './renderer' -// import { -// BitmapCompression -// } from './compression' enum BitmapHeaderTypes { BM = 'BM', @@ -56,8 +53,6 @@ export class Bitmap { bitmap_header_size: number @Choice('bitmap_header_size', { - 12: OS21XBITMAPHEADER, - 16: OS22XBITMAPCOREHEADER, 64: OS22XBITMAPHEADER, 40: BITMAPINFOHEADER, 52: BITMAPV2INFOHEADER, @@ -65,7 +60,7 @@ export class Bitmap { 108: BITMAPV4INFOHEADER, 124: BITMAPV5INFOHEADER, }) - bitmap_header: OS21XBITMAPHEADER | OS22XBITMAPHEADER | OS22XBITMAPCOREHEADER | BITMAPINFOHEADER | BITMAPV2INFOHEADER | BITMAPV3INFOHEADER | BITMAPV4INFOHEADER | BITMAPV5INFOHEADER + bitmap_header: OS22XBITMAPHEADER | BITMAPINFOHEADER | BITMAPV2INFOHEADER | BITMAPV3INFOHEADER | BITMAPV4INFOHEADER | BITMAPV5INFOHEADER /* Present only in case the DIB header is the BITMAPINFOHEADER * and the Compression Method member is set to either BI_BITFIELDS @@ -80,7 +75,6 @@ export class Bitmap { /* The gap size depend on the offset found in the BitmapFileHeader */ /* Just use the `@Pre` decorator to move the cursor to the correct place */ - @Offset('file_header.offset') @Matrix('bitmap_header.width', 'bitmap_header.height', { alignment: 4 }) @Choice('bitmap_header.bits_per_pixels', { From 306192862423dda3961cc27ef50f90a37b25ed3d Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Wed, 11 Dec 2024 19:48:28 +0100 Subject: [PATCH 4/6] fix: better type checking for strings arguments of decorators --- src/decorators/__tests__/condition.test.ts | 2 +- src/decorators/__tests__/controller.test.ts | 12 ------ src/decorators/common.ts | 42 +++++++++++++++++++-- src/decorators/condition.ts | 14 +++---- src/decorators/controller.ts | 14 ++++--- src/decorators/prepost.ts | 8 ++-- src/decorators/primitive.ts | 11 +++--- 7 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/decorators/__tests__/condition.test.ts b/src/decorators/__tests__/condition.test.ts index 323a99a..4e0f350 100644 --- a/src/decorators/__tests__/condition.test.ts +++ b/src/decorators/__tests__/condition.test.ts @@ -101,7 +101,7 @@ describe('@Condition: basic testing', () => { foo: number = 1 bar: number = 2 @Choice('foo', { - 1: [TestArg, 'foo, bar'], + 1: [TestArg, 'foo,bar'], }) field: TestArg } diff --git a/src/decorators/__tests__/controller.test.ts b/src/decorators/__tests__/controller.test.ts index e858658..87dd4f5 100644 --- a/src/decorators/__tests__/controller.test.ts +++ b/src/decorators/__tests__/controller.test.ts @@ -347,18 +347,6 @@ describe('@Controller: errors', () => { testControllerCursor(TestClass, 'field', () => cur.read(PrimitiveSymbol.u8), [], cur) }).toThrow(EOFError) }) - it('@Count: recursiveGet should throw an error for non existing property', () => { - expect(() => { - class TestClass { - child = { count: 2 } - - @Count('child.x', { primitiveCheck: false }) - field: number - } - - testController(TestClass, 'field', () => 1, [1, 1]) - }).toThrow(ReferenceError) - }) it('@Count: recursiveGet should throw an error when referencing a string in an arithmetic expression', () => { expect(() => { class TestClass { diff --git a/src/decorators/common.ts b/src/decorators/common.ts index 9e5093e..928c487 100644 --- a/src/decorators/common.ts +++ b/src/decorators/common.ts @@ -67,6 +67,42 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata: } } +type RecursiveKeyOf = { + [TKey in keyof TObj & (string | number)]: + TObj[TKey] extends any[] + ? `${TKey}` + : TObj[TKey] extends object + ? `${TKey}.${RecursiveKeyOf}` + : `${TKey}` +}[keyof TObj & (string | number)] + +type NumericalString = `${number}` + +type RecursiveKeyWithOperations | NumericalString> = + T extends U + ? T + : T extends `${U} + ${infer R}` + ? T extends `${infer F} + ${R}` + ? `${F} + ${RecursiveKeyWithOperations>}` + : never + : T extends `${U} - ${infer R}` + ? T extends `${infer F} - ${R}` + ? `${F} - ${RecursiveKeyWithOperations>}` + : never + : U + +type CommaSeparetedRecursiveKey> = + T extends U + ? T + : T extends `${U},${infer R}` + ? T extends `${infer F},${R}` + ? `${F},${CommaSeparetedRecursiveKey>}` + : never + : U + +export type StringFormattedRecursiveKeyOf = Args extends RecursiveKeyWithOperations ? Args : RecursiveKeyWithOperations +export type StringFormattedCommaSepRecursiveKeyOf = Args extends CommaSeparetedRecursiveKey ? Args : CommaSeparetedRecursiveKey + /** * Chained accessor to get the value of `expr` for `obj`. * @@ -77,7 +113,7 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata: * @throws {ReferenceError} if you attempt to access a non existing property. * */ -export function recursiveGet (obj: any, expr: string): any { +export function recursiveGet (obj: T, expr: RecursiveKeyWithOperations): any { const _isOperation = (x: string): boolean => ['+', '-'].includes(x) const _get = (path: string): any => path.split('.').reduce((acc: any, key: string) => { @@ -109,7 +145,7 @@ export function recursiveGet (obj: any, expr: string): any { } } -export function commaSeparetedRecursiveGet (obj: any, args: string): any[] { +export function commaSeparetedRecursiveGet (obj: T, args: CommaSeparetedRecursiveKey): any[] { const keys = args.split(',') - return keys.map(key => recursiveGet(obj, key.trim())) + return keys.map(key => recursiveGet(obj, key.trim() as RecursiveKeyWithOperations)) } diff --git a/src/decorators/condition.ts b/src/decorators/condition.ts index 5b0bee8..77a8155 100644 --- a/src/decorators/condition.ts +++ b/src/decorators/condition.ts @@ -40,7 +40,7 @@ * * @module Condition */ -import { recursiveGet, type PropertyMetaDescriptor, createPropertyMetaDescriptor } from './common' +import { recursiveGet, type PropertyMetaDescriptor, createPropertyMetaDescriptor, StringFormattedRecursiveKeyOf } from './common' import { type PrimitiveTypeProperty, type RelationTypeProperty, type RelationParameters, Relation, createPrimitiveTypeProperty, createRelationTypeProperty } from './primitive' import { isPrimitiveSymbol, type DecoratorType, type Primitive, type Context } from '../types' import { NoConditionMatched } from '../error' @@ -87,7 +87,7 @@ export interface Condition extends PropertyMetaDescriptor { * * @category Advanced Use */ -export function conditionDecoratorFactory (name: string, cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { +export function conditionDecoratorFactory (name: string, cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context) { const propertyName = context.name as keyof This function createRelation (relationOrPrimitive: Primitive): PrimitiveTypeProperty | RelationTypeProperty { @@ -125,7 +125,7 @@ export function conditionDecoratorFactory (name: string, co * * @category Advanced Use */ -export function dynamicConditionDecoratorFactory (name: string, func: DynamicGetterFunction, args?: RelationParameters): DecoratorType { +export function dynamicConditionDecoratorFactory (name: string, func: DynamicGetterFunction, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context) { const propertyName = context.name as keyof This function createRelation (relationOrPrimitive: Primitive): PrimitiveTypeProperty | RelationTypeProperty { @@ -204,7 +204,7 @@ export function dynamicConditionDecoratorFactory (name: str * * @category Decorators */ -export function IfThen (cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { +export function IfThen (cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { return conditionDecoratorFactory('ifthen', cond, then, args) } @@ -251,7 +251,7 @@ export function IfThen (cond: ConditionFunction, then * * @category Decorators */ -export function Else (then?: Primitive, args?: RelationParameters): DecoratorType { +export function Else (then?: Primitive, args?: RelationParameters): DecoratorType { return conditionDecoratorFactory('else', () => true, then, args) } @@ -430,7 +430,7 @@ export function Else (then?: Primitive, args?: Rela * * @category Decorators */ -export function Choice (cmp: string | ((targetInstance: This) => any), match: Record | [Primitive, RelationParameters] | undefined>, args?: RelationParameters): DecoratorType { +export function Choice (cmp: StringFormattedRecursiveKeyOf | ((targetInstance: This) => any), match: Record | [Primitive, RelationParameters] | undefined>, args?: RelationParameters): DecoratorType { const valueToCompare = typeof cmp === 'string' ? (targetInstance: This) => recursiveGet(targetInstance, cmp) : cmp // Mandatory to cast to String because the key is always a string even though you declare it as a number const decorators = Object.keys(match).map((key: keyof typeof match) => { @@ -500,7 +500,7 @@ export function Choice (cmp: string | ((targetInstance: This) => an * @returns {DecoratorType} The property decorator function. * @category Decorators */ -export function Select (getter: ((targetInstance: This) => Primitive), args?: RelationParameters): DecoratorType { +export function Select (getter: ((targetInstance: This) => Primitive), args?: RelationParameters): DecoratorType { return dynamicConditionDecoratorFactory('select', getter, args) } diff --git a/src/decorators/controller.ts b/src/decorators/controller.ts index c54fc2e..e04bac7 100644 --- a/src/decorators/controller.ts +++ b/src/decorators/controller.ts @@ -50,7 +50,7 @@ * * @module Controller */ -import { createPropertyMetaDescriptor, type PropertyMetaDescriptor, recursiveGet } from './common' +import { createPropertyMetaDescriptor, type PropertyMetaDescriptor, recursiveGet, StringFormattedRecursiveKeyOf } from './common' import { type Cursor } from '../cursor' import { EOF, type DecoratorType, type InstantiableObject, type Context } from '../types' import { relationExistsOrThrow, EOFError } from '../error' @@ -98,6 +98,8 @@ export const ControllerOptionsDefault = { export type ControllerFunction = (targetInstance: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => any export type OptionlessControllerFunction = (targetInstance: any, cursor: Cursor, read: ControllerReader) => any +type NumberOrRecursiveKey = number | StringFormattedRecursiveKeyOf + /** * Controller type interface structure definition. * @@ -500,7 +502,7 @@ export function NullTerminatedString (opt?: Partial (arg: number | string, opt?: Partial): DecoratorType { +export function Count (arg: NumberOrRecursiveKey, opt?: Partial): DecoratorType { function countController ( currStateObject: This, cursor: Cursor, @@ -542,14 +544,14 @@ export function Count (arg: number | string, opt?: Partial (width: number | string, height: number | string, opt?: Partial): DecoratorType { +export function Matrix (width: NumberOrRecursiveKey, height: NumberOrRecursiveKey, opt?: Partial): DecoratorType { function matrixController ( currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions, ): any { - const getArg = (x: number | string): number => typeof x === 'string' + const getArg = (x: NumberOrRecursiveKey): number => typeof x === 'string' ? recursiveGet(currStateObject, x) : x const finalWidth = getArg(width) @@ -614,7 +616,7 @@ export function Matrix (width: number | string, height: number | st * * @category Decorators */ -export function Size (size: number | string, opt?: Partial): DecoratorType { +export function Size (size: NumberOrRecursiveKey, opt?: Partial): DecoratorType { return controllerDecoratorFactory( 'size', (currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => { @@ -669,7 +671,7 @@ export function Size (size: number | string, opt?: Partial (arr: string | any[] | ((_: This) => any[]), opt?: Partial): DecoratorType { +export function MapTo (arr: StringFormattedRecursiveKeyOf | any[] | ((_: This) => any[]), opt?: Partial): DecoratorType { return controllerDecoratorFactory( 'map', (currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => { diff --git a/src/decorators/prepost.ts b/src/decorators/prepost.ts index 6073e93..b3b6559 100644 --- a/src/decorators/prepost.ts +++ b/src/decorators/prepost.ts @@ -47,7 +47,7 @@ * * @module PrePost */ -import { ClassMetaDescriptor, type PropertyMetaDescriptor, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' +import { ClassMetaDescriptor, type PropertyMetaDescriptor, StringFormattedRecursiveKeyOf, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' import { relationExistsOrThrow } from '../error' import { ExecutionScope, type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext, type DecoratorType, type Context } from '../types' import { type Cursor, type BinaryCursorEndianness, BinaryCursor } from '../cursor' @@ -400,7 +400,7 @@ export function Post (func: PrePostFunction, opt?: Partial (offset: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { +export function Offset (offset: number | StringFormattedRecursiveKeyOf | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { return preFunctionDecoratorFactory('offset', (targetInstance, cursor) => { const offCompute = typeof offset === 'string' ? recursiveGet(targetInstance, offset) as number @@ -483,7 +483,7 @@ export function Offset (offset: number | string | ((instance: This, cursor * * @category Decorators */ -export function Peek (offset?: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { +export function Peek (offset?: number | StringFormattedRecursiveKeyOf | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { return function (_: undefined, context: ClassAndPropertyDecoratorContext) { preFunctionDecoratorFactory('pre-peek', (targetInstance, cursor) => { const preOff = cursor.offset() @@ -667,7 +667,7 @@ type ValueSetFunction = (instance: This) => Value * * @throws {@link Primitive.RelationAlreadyDefinedError} if a relation metadata is found. */ -export function ValueSet (setter: ValueSetFunction, opt?: Partial): DecoratorType { +export function ValueSet (setter: ValueSetFunction, opt?: Partial): DecoratorType { return function (_: any, context: Context) { const propertyName = context.name as keyof This if (!Meta.isFieldDecorated(context.metadata, propertyName)) { diff --git a/src/decorators/primitive.ts b/src/decorators/primitive.ts index 3980628..18b1a81 100644 --- a/src/decorators/primitive.ts +++ b/src/decorators/primitive.ts @@ -33,7 +33,7 @@ * * @module Primitive */ -import { type PropertyMetaDescriptor, createPropertyMetaDescriptor, commaSeparetedRecursiveGet } from './common' +import { type PropertyMetaDescriptor, createPropertyMetaDescriptor, commaSeparetedRecursiveGet, StringFormattedCommaSepRecursiveKeyOf } from './common' import { RelationAlreadyDefinedError, WrongBitfieldClassImplementation } from '../error' import Meta from '../metadatas' import { type PrimitiveSymbol, isPrimitiveSymbol, type InstantiableObject, type DecoratorType, type Context, type DecoratorMetadataObject } from '../types' @@ -111,8 +111,7 @@ export interface PrimitiveTypeProperty extends PropertyMetaDescriptor = ((targetInstance: This) => any[]) | string - +export type RelationParameters = ((targetInstance: This) => any[]) | StringFormattedCommaSepRecursiveKeyOf /** * `RelationTypeProperty` are primitive type that holds information about * another binary type definition. @@ -225,7 +224,7 @@ export function createPrimitiveTypeProperty (metadata: DecoratorMetadataOb * * @category Advanced Use */ -export function createRelationTypeProperty (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject, args?: RelationParameters): RelationTypeProperty { +export function createRelationTypeProperty (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject, args?: RelationParameters): RelationTypeProperty { const argsFunc = typeof args === 'string' ? (targetInstance: This) => commaSeparetedRecursiveGet(targetInstance, args) : args as ((targetInstance: This) => any[]) | undefined @@ -344,7 +343,7 @@ export function createRelationTypeProperty (metadata: DecoratorMet * * @category Decorators */ -export function Relation (relation?: InstantiableObject | PrimitiveSymbol, args?: RelationParameters): DecoratorType { +export function Relation (relation?: InstantiableObject | PrimitiveSymbol, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context): void { if (Meta.getBitFields(context.metadata).length > 0) { throw new WrongBitfieldClassImplementation(String(context.name)) @@ -360,7 +359,7 @@ export function Relation (relation?: InstantiableObject(context.metadata, propertyName, relation)) } else { - Meta.setField(context.metadata, createRelationTypeProperty(context.metadata, propertyName, relation, args)) + Meta.setField(context.metadata, createRelationTypeProperty(context.metadata, propertyName, relation, args)) } } } From b450ed2c1e6f639c4dca4e9bcb128ea7342839ca Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Wed, 11 Dec 2024 22:22:18 +0100 Subject: [PATCH 5/6] feat: basic support for `Ctx{Get,Set}` decorators `Ctx` decorators are a way to share informations across objects during the reading of the binary file definition. In the following example, a streaming protocol that receives records of arbitrary length is defined. Records have two different type a 'definition' or a 'data' both use an 'id' to identify themself. The definition defines the size of the data message they define by using `CtxSet` to store that size into the context. The data message uses `CtxGet` to fetch its size defined previously by the definition. ```typescript class RecordDefinition { @Relation(PrimitiveSymbol.u8) id: number @CtxSet(_ => `Definition.${_.id}`) @Relation(PrimitiveSymbol.u8) size: number } class RecordMessage { @Relation(PrimitiveSymbol.u8) id: number @CtxGet(_ => `Definition.${_.id}`) _size: number @Count('_size') @Relation(PrimitiveSymbol.u8) data } class Record { @Relation(PrimitiveSymbol.u8) type: number @Choice('type', { 0x00: RecordDefinition, 0x01: RecordMessage, }) message: RecordDefinition | RecordMessage } class Protocol { @Until(EOF) records: Record[] } ``` --- src/__tests__/parser.test.ts | 30 ++- src/decorators/__tests__/context.test.ts | 72 +++++++ src/decorators/context.ts | 264 +++++++++++++++++++++++ src/decorators/index.ts | 1 + src/metadatas.ts | 19 ++ src/reader.ts | 14 +- 6 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 src/decorators/__tests__/context.test.ts create mode 100644 src/decorators/context.ts diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 01b1aac..5b0ff41 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -4,9 +4,10 @@ import { EOF, PrimitiveSymbol, type InstantiableObject } from '../types' import { binread } from '../reader' import { withBinspectorContext } from '../context' import { BinaryReader, BinaryCursorEndianness } from '../cursor' +import { CtxGet, CtxSet } from '../decorators/context' -function expectReadTest (buffer: Array, ObjectDefinition: InstantiableObject, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ...args: any[]) { - return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ...args)) +function expectReadTest (buffer: Array, ObjectDefinition: InstantiableObject, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ctx = {}, ...args: any[]) { + return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ctx, ...args)) } function expectReadTestToThrow (buffer: Array, ObjectDefinition: InstantiableObject) { @@ -536,7 +537,7 @@ describe('Reading binary definition with PrePost decorators', () => { } } - expectReadTest([0x01, 0x02, 0x03, 0x04], Protocol, BinaryCursorEndianness.BigEndian, 2).toMatchObject({ + expectReadTest([0x01, 0x02, 0x03, 0x04], Protocol, BinaryCursorEndianness.BigEndian, {}, 2).toMatchObject({ value: 0x03, }) }) @@ -652,6 +653,29 @@ describe('Reading binary definition with PrePost decorators', () => { }) }) +describe('Reading binary definition with Ctx decorators', () => { + it('should ', () => { + class Protocol { + @CtxGet('Settings.Count') + data_type: number + + @CtxSet('Settings.Value') + @Count('data_type') + @Relation(PrimitiveSymbol.u8) + foo: number + } + + const ctx = { Settings: { Count: 3 } } + + expectReadTest([0x01, 0x02, 0x03], Protocol, BinaryCursorEndianness.LittleEndian, ctx).toMatchObject({ + data_type: 3, + foo: [1, 2, 3], + }) + + expect(ctx).toMatchObject({ Settings: { Count: 3, Value: [1, 2, 3] } }) + }) +}) + describe('Reading a relation to an empty definition', () => { it('should throw an error', () => { class Header { diff --git a/src/decorators/__tests__/context.test.ts b/src/decorators/__tests__/context.test.ts new file mode 100644 index 0000000..c484672 --- /dev/null +++ b/src/decorators/__tests__/context.test.ts @@ -0,0 +1,72 @@ +import { describe, expect } from '@jest/globals' +import { CtxSet, CtxGet, useContextGet, useContextSet } from '../context' +import Meta from '../../metadatas' + +function testContextGet (TargetClass: new () => any, field: string, ctx: object, expected: any) { + const instance = new TargetClass() + + const contexts = Meta.getContext(TargetClass[Symbol.metadata] as DecoratorMetadataObject, field) + expect(useContextGet(contexts, instance, ctx)).toEqual(expected) +} + +function testContextSet (TargetClass: new () => any, field: string, value: any, ctx: object) { + const instance = new TargetClass() + + const contexts = Meta.getContext(TargetClass[Symbol.metadata] as DecoratorMetadataObject, field) + useContextSet(contexts, value, instance, ctx) +} + +describe('@Ctx: functions', () => { + it('should modify the ctx object', () => { + class TestClass { + @CtxSet('test') + data: number + } + const ctx: any = {} + const VALUE = 1 + testContextSet(TestClass, 'data', VALUE, ctx) + expect(ctx.test).toEqual(VALUE) + }) + it('should modify the ctx object with recursive accessors', () => { + class TestClass { + @CtxSet('foo.bar.1') + data: number + + @CtxSet('foo.bar.2') + data_2: number + } + const ctx: any = {} + const VALUE = 1 + testContextSet(TestClass, 'data', VALUE, ctx) + testContextSet(TestClass, 'data_2', VALUE, ctx) + expect(ctx.foo.bar[1]).toEqual(VALUE) + expect(ctx.foo.bar[2]).toEqual(VALUE) + }) + it('should get value from the ctx object', () => { + class TestClass { + @CtxGet('test') + data: number + } + const VALUE = 1 + const ctx = { test: VALUE } + testContextGet(TestClass, 'data', ctx, VALUE) + }) + it('should get value from the ctx object with recursive accessors', () => { + class TestClass { + @CtxGet('foo.bar') + data: number + } + const VALUE = 1 + const ctx = { foo: { bar: VALUE } } + testContextGet(TestClass, 'data', ctx, VALUE) + }) + it('should throw when accessing non existing properties', () => { + class TestClass { + @CtxGet('test.foo') + data: number + } + const VALUE = 1 + const ctx = { test: VALUE } + expect(() => testContextGet(TestClass, 'data', ctx, VALUE)).toThrow() + }) +}) diff --git a/src/decorators/context.ts b/src/decorators/context.ts new file mode 100644 index 0000000..423e308 --- /dev/null +++ b/src/decorators/context.ts @@ -0,0 +1,264 @@ +/** + * Module definition of {@link Context} property decorators. + * + * The {@link Context} decorators provides functions to read/write the context + * shared among the binary objects during the reading phase. + * + * @module Context + */ +import { Context, DecoratorType } from '../types' +import { createPropertyMetaDescriptor, PropertyMetaDescriptor, recursiveGet, StringFormattedRecursiveKeyOf } from './common' +import { Relation } from './primitive' +import Meta from '../metadatas' + +export const CtxSymbol = Symbol('context') + +export type GlobalCtx = object + +export type CtxKeyFunction = (targetInstance: This) => string + +export enum CtxType { + CtxGetter, + CtxSetter, +} + +export interface CtxOptions { + /** + * Ensures that a relation exists before defining the Transformer decorator. + */ + base_type: 'array' | undefined +} + +export const CtxOptionsDefault = { + base_type: undefined, +} + +/** + * Ctx metadata type definition. + * + * @extends {PropertyMetaDescriptor} + */ +export interface Ctx extends PropertyMetaDescriptor { + options: CtxOptions + /** + * Function that retrieve the key to access the context + */ + keyGetter: CtxKeyFunction + /** + * Context type: retrieve a value or set a value + */ + func_type: CtxType +} + +function ctxPropertyFunctionDecoratorFactory (name: string, func_type: CtxType, keyGetter: string | CtxKeyFunction, opt: Partial = CtxOptionsDefault): DecoratorType { + const options = { ...CtxOptionsDefault, ...opt } + + return function (_: any, context: Context) { + const propertyKey = context.name as keyof This + if (!Meta.isFieldDecorated(context.metadata, propertyKey)) { + // Create an empty relation that wont be read. + Relation()(_, context) + } + const ctx: Ctx = { + ...createPropertyMetaDescriptor(CtxSymbol, name, context.metadata, propertyKey), + func_type, + options, + keyGetter: typeof keyGetter === 'string' ? () => keyGetter : keyGetter, + } + + Meta.setContext(context.metadata, propertyKey, ctx) + } +} + +/** + * `@CtxGet` decorator retrieve a value based on the key passed as argument + * from a 'context' shared during the reading phase. + * + * @example + * + * In the following example, a streaming protocol that receives records + * of arbitrary length is defined. + * Records have two different type a 'definition' or a 'data' both use an + * 'id' to identify themself. The definition defines the size of the data + * message they define by using `CtxSet` to store that size into the + * context. The data message uses `CtxGet` to fetch its size defined + * previously by the definition. + * + * ```typescript + * class RecordDefinition { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxSet(_ => `Definition.${_.id}`) + * @Relation(PrimitiveSymbol.u8) + * size: number + * } + * + * class RecordMessage { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxGet(_ => `Definition.${_.id}`) + * _size: number + * + * @Count('_size') + * @Relation(PrimitiveSymbol.u8) + * data + * } + * + * class Record { + * @Relation(PrimitiveSymbol.u8) + * type: number + * + * @Choice('type', { + * 0x00: RecordDefinition, + * 0x01: RecordMessage, + * }) + * message: RecordDefinition | RecordMessage + * } + * + * class Protocol { + * @Until(EOF) + * records: Record[] + * } + * ``` + * + * @param {CtxKeyFunction} keyGetter Either a string formatted as + * recursive key or a function that returns that string based on the + * instance value. + * @param {Partial} [opt] Optional configuration. + * @returns {DecoratorType} The property decorator function. + * + * @category Decorators + */ +export function CtxGet (keyGetter: CtxKeyFunction | string, opt?: Partial): DecoratorType { + return ctxPropertyFunctionDecoratorFactory ('ctx-get', CtxType.CtxGetter, keyGetter, opt) +} + +/** + * `@CtxSet` decorator set the value of the decorated property into a + * shared 'context' during the reading phase. + * + * @example + * + * In the following example, a streaming protocol that receives records + * of arbitrary length is defined. + * Records have two different type a 'definition' or a 'data' both use an + * 'id' to identify themself. The definition defines the size of the data + * message they define by using `CtxSet` to store that size into the + * context. The data message uses `CtxGet` to fetch its size defined + * previously by the definition. + * + * ```typescript + * class RecordDefinition { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxSet(_ => `Definition.${_.id}`) + * @Relation(PrimitiveSymbol.u8) + * size: number + * } + * + * class RecordMessage { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxGet(_ => `Definition.${_.id}`) + * _size: number + * + * @Count('_size') + * @Relation(PrimitiveSymbol.u8) + * data + * } + * + * class Record { + * @Relation(PrimitiveSymbol.u8) + * type: number + * + * @Choice('type', { + * 0x00: RecordDefinition, + * 0x01: RecordMessage, + * }) + * message: RecordDefinition | RecordMessage + * } + * + * class Protocol { + * @Until(EOF) + * records: Record[] + * } + * ``` + * + * @param {CtxKeyFunction} keyGetter Either a string formatted as + * recursive key or a function that returns that string based on the + * instance value. + * @param {Partial} [opt] Optional configuration. + * @returns {DecoratorType} The property decorator function. + * + * @category Decorators + */ +export function CtxSet (keyGetter: CtxKeyFunction | string, opt?: Partial): DecoratorType { + return ctxPropertyFunctionDecoratorFactory ('ctx-set', CtxType.CtxSetter, keyGetter, opt) +} + +/** + * useContextGet execute an array of `Ctx` decorator metadata. + * + * @typeParam This The type of the class the decorator is applied to. + * + * @param {Array>} metaCtx An array of context function to apply. + * @param {This} targetInstance The target class instance containing the property. + * @param {GlobalCtx} ctx The shared context reference. + * @returns {any} The context retrieved. + * + * @category Advanced Use + */ +export function useContextGet (metaCtx: Array>, targetInstance: This, ctx: GlobalCtx): any { + const values = metaCtx.filter(x => x.func_type === CtxType.CtxGetter).map((x) => { + const key = x.keyGetter(targetInstance) + + // TODO future version should pass some typing to the context + return key.split('.').reduce((acc: any, key: string) => { + if (Object.prototype.hasOwnProperty.call(acc, key) === false) { + throw new ReferenceError(`Can't retrieve key: '${key}' from ctx: ${JSON.stringify(ctx)}.`) + } + return acc[key] + }, ctx) + }) + + return values.length === 1 ? values[0] : values +} + +/** + * useContextSet execute an array of `Ctx` decorator metadata. + * + * @typeParam This The type of the class the decorator is applied to. + * + * @param {Array>} metaCtx An array of context function to apply. + * @param {This} targetInstance The target class instance containing the property. + * @param {GlobalCtx} ctx The shared context reference. + * @returns {any} The context retrieved. + * + * @category Advanced Use + */ +export function useContextSet (metaCtx: Array>, propertyValue: any, targetInstance: This, ctx: GlobalCtx): void { + metaCtx.filter(x => x.func_type === CtxType.CtxSetter).forEach((x) => { + const key = x.keyGetter(targetInstance) + const accessors = key.split('.') + const lastKey = accessors[accessors.length - 1] + const ref = accessors.slice(0, -1).reduce((acc: any, key: string) => { + if (Object.prototype.hasOwnProperty.call(acc, key) === false) { + if (x.options.base_type == 'array') { + Object.defineProperty(acc, key, { + value: [] + }) + } else { + Object.defineProperty(acc, key, { + value: {} + }) + } + } + return acc[key] + }, ctx) + ref[lastKey] = propertyValue + }) +} diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 7648078..0f77271 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -10,3 +10,4 @@ export * from './primitive' export * from './prepost' export * from './bitfield' export * from './transformer' +export * from './context' diff --git a/src/metadatas.ts b/src/metadatas.ts index 1151ac0..db1c288 100644 --- a/src/metadatas.ts +++ b/src/metadatas.ts @@ -4,6 +4,7 @@ import { type Controller, ControllerSymbol } from './decorators/controller' import { type Transformer, TransformerSymbol } from './decorators/transformer' import { type Validator, ValidatorSymbol } from './decorators/validator' import { type BitField, BitFieldSymbol } from './decorators/bitfield' +import { type Ctx, CtxSymbol } from './decorators/context' import { type PrePost, PreFunctionSymbol, PostFunctionSymbol, PreClassFunctionSymbol, PostClassFunctionSymbol, PrePostSymbols, PrePostClass } from './decorators/prepost' import { type DecoratorMetadataObject } from './types' @@ -252,6 +253,22 @@ function setBitField ( return bitfields } +function getContext (metadata: DecoratorMetadataObject, propertyKey: keyof This): Array> { + return getMetadata( + metadata, + propertyKey, + CtxSymbol, + ) +} + +function setContext ( + metadata: DecoratorMetadataObject, + propertyKey: keyof This, + ctx: Ctx, +): Array> { + return setMetadata(metadata, propertyKey, ctx.type, ctx) +} + export default { getMetadata, setMetadata, @@ -276,4 +293,6 @@ export default { getBitField, getBitFields, setBitField, + getContext, + setContext, } diff --git a/src/reader.ts b/src/reader.ts index 913b4ab..d5260dc 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -21,6 +21,7 @@ import { useValidators } from './decorators/validator' import { useConditions } from './decorators/condition' import { usePrePost } from './decorators/prepost' import { useBitField } from './decorators/bitfield' +import { useContextGet, useContextSet, CtxType } from './decorators/context' /** * binread. @@ -44,7 +45,7 @@ import { useBitField } from './decorators/bitfield' * `ObjectDefinition` passed in param. * You can create self refering field by using conditionnal decorator. */ -export function binread (content: Cursor, ObjectDefinition: InstantiableObject, ...args: any[]): Target { +export function binread (content: Cursor, ObjectDefinition: InstantiableObject, ctx = {}, ...args: any[]): Target { const ObjectDefinitionName = ObjectDefinition.name function getBinReader (field: PropertyType, instance: Target): ControllerReader { if (isPrimitiveRelation(field)) { @@ -63,7 +64,7 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable } try { - return binread(content, field.relation, ...[...finalArgs, instance]) + return binread(content, field.relation, ctx, ...[...finalArgs, instance]) } catch (error) { // We need to catch the EOF error because the binread function // can't return it so it just throw it EOFError. @@ -105,6 +106,14 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable Meta.getFields(metadata).forEach((field) => { usePrePost(Meta.getPre(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) + const metaCtx = Meta.getContext(field.metadata, field.propertyName) + + const ctxGetter = metaCtx.filter(x => x.func_type === CtxType.CtxGetter) + if (ctxGetter.length) { + instance[field.propertyName] = useContextGet(metaCtx, instance, ctx) + return + } + // TODO [Cursor] Pass the field name information to add to the namespace const finalRelationField = isUnknownProperty(field) ? useConditions(Meta.getConditions(field.metadata, field.propertyName), instance) : field if (finalRelationField !== undefined) { @@ -127,6 +136,7 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable useValidators(Meta.getValidators(metadata, field.propertyName), transformedValue, instance, content) instance[field.propertyName] = transformedValue + useContextSet(metaCtx, transformedValue, instance, ctx) } usePrePost(Meta.getPost(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) }) From b7ba9f43b272210698414d60314d8a11df2f173b Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Thu, 12 Dec 2024 22:03:48 +0100 Subject: [PATCH 6/6] 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91cb9e9..524fae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "binspector", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "binspector", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.14.0", diff --git a/package.json b/package.json index 7e3d096..ee343e0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "binspector", "description": "A _truly_ declarative library for binary file and protocol definition written in typescript. Read binary files based on class definition and decorators directly from your webapp.", - "version": "0.3.0", + "version": "0.4.0", "main": "dist/binspector.js", "types": "dist/index.d.ts", "files": [