From 998d5410b6d135f11a6b5f1db087cbee080891b6 Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Sun, 8 Dec 2024 10:55:22 +0100 Subject: [PATCH 1/5] refactor: move `ExecutionScope` into the common type --- src/__tests__/writer.test.ts | 8 ++++---- src/decorators/transformer.ts | 28 +++++++++------------------- src/types.ts | 10 ++++++++++ src/writer.ts | 6 +++--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/__tests__/writer.test.ts b/src/__tests__/writer.test.ts index aad56eb..3249a12 100644 --- a/src/__tests__/writer.test.ts +++ b/src/__tests__/writer.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from '@jest/globals' -import { Bitfield, Relation, Choice, Count, Matrix, Peek, Offset, Endian, NullTerminatedString, TransformScale, TransformOffset, TransformerExecutionScope, Transform, Until } from '../decorators' -import { InstantiableObject, PrimitiveSymbol, EOF } from '../types' +import { Bitfield, Relation, Choice, Count, Matrix, Peek, Offset, Endian, NullTerminatedString, TransformScale, TransformOffset, Transform, Until } from '../decorators' +import { ExecutionScope, InstantiableObject, PrimitiveSymbol, EOF } from '../types' import { binwrite } from '../writer' import { binread } from '../reader' import { BinaryReader, BinaryWriter, BinaryCursorEndianness } from '../cursor' @@ -305,11 +305,11 @@ describe('Writing binary definition with Transformer decorators', () => { @Transform((value: number[]) => { const buf = new Uint8Array(value) return new TextDecoder().decode(buf) - }, { scope: TransformerExecutionScope.OnRead }) + }, { scope: ExecutionScope.OnRead }) @Transform((value: string) => { const buf = new TextEncoder().encode(value) return Array.from(buf) - }, { scope: TransformerExecutionScope.OnWrite }) + }, { scope: ExecutionScope.OnWrite }) @Until(EOF) @Relation(PrimitiveSymbol.u8) decodedString: string diff --git a/src/decorators/transformer.ts b/src/decorators/transformer.ts index 3f48800..fcf2625 100644 --- a/src/decorators/transformer.ts +++ b/src/decorators/transformer.ts @@ -32,7 +32,7 @@ * * By default, custom transformers are applied only during the reading phase. * To support binary encoding (writing phase), define an additional transformer - * with a {@link TransformerExecutionScope} set to `OnWrite` or `OnBoth` via + * with a {@link ExecutionScope} set to `OnWrite` or `OnBoth` via * {@link TransformerOptions}. * * The {@link Transformer} category define various decorators to perform @@ -49,21 +49,11 @@ */ import { createPropertyMetaDescriptor, type PropertyMetaDescriptor } from './common' import { relationExistsOrThrow } from '../error' -import { type DecoratorType, type Context } from '../types' +import { ExecutionScope, type DecoratorType, type Context } from '../types' import Meta from '../metadatas' export const TransformerSymbol = Symbol('transformer') -/** - * The execution scope defines in which part of the binary processing (read, - * write or both) the transformer is applied. - */ -export enum TransformerExecutionScope { - OnRead = 0x01, - OnWrite = 0x02, - OnBoth = 0x03, -} - /** * TransformerOptions. */ @@ -80,13 +70,13 @@ export interface TransformerOptions { * Specifies whether the transformer function should be executed during * the read phase, the write phase, or both. */ - scope: TransformerExecutionScope + scope: ExecutionScope } export const TransformerOptionsDefault = { primitiveCheck: true, each: false, - scope: TransformerExecutionScope.OnRead, + scope: ExecutionScope.OnRead, } /** @@ -187,11 +177,11 @@ export function transformerDecoratorFactory (name: string, func: Tr * @Transform((value: number[]) => { * const buf = new Uint8Array(value) * return new TextDecoder().decode(buf) - * }, { scope: TransformerExecutionScope.OnRead }) + * }, { scope: ExecutionScope.OnRead }) * @Transform((value: string) => { * const buf = new TextEncoder().encode(value) * return Array.from(buf) - * }, { scope: TransformerExecutionScope.OnWrite }) + * }, { scope: ExecutionScope.OnWrite }) * @Until(EOF) * @Relation(PrimitiveSymbol.u8) * decodedString: string; @@ -238,7 +228,7 @@ export function Transform (transformFunction: TransformerFunction (scale: number, opt?: Partial): DecoratorType { return function (_: any, context: Context) { transformerDecoratorFactory('transform-scale', x => x * scale, opt)(_, context) - transformerDecoratorFactory('transform-scale', x => x / scale, { ...opt, scope: TransformerExecutionScope.OnWrite })(_, context) + transformerDecoratorFactory('transform-scale', x => x / scale, { ...opt, scope: ExecutionScope.OnWrite })(_, context) } } @@ -260,7 +250,7 @@ export function TransformScale (scale: number, opt?: Partial (off: number, opt?: Partial): DecoratorType { return function (_: any, context: Context) { transformerDecoratorFactory('transform-offset', x => x + off, opt)(_, context) - transformerDecoratorFactory('transform-offset', x => x - off, { ...opt, scope: TransformerExecutionScope.OnWrite })(_, context) + transformerDecoratorFactory('transform-offset', x => x - off, { ...opt, scope: ExecutionScope.OnWrite })(_, context) } } @@ -277,7 +267,7 @@ export function TransformOffset (off: number, opt?: Partial (transformers: Array>, propertyValue: any, targetInstance: This, scope = TransformerExecutionScope.OnRead): any { +export function useTransformer (transformers: Array>, propertyValue: any, targetInstance: This, scope = ExecutionScope.OnRead): any { return transformers.reduce((transformedTmpValue, transformer) => { if ((transformer.options.scope & scope) > 0) { if (Array.isArray(transformedTmpValue) && transformer.options.each) { diff --git a/src/types.ts b/src/types.ts index 2b6e8eb..a4b566a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,16 @@ export enum PrimitiveSymbol { char, } +/** + * The execution scope defines in which part of the binary processing (read, + * write or both). + */ +export enum ExecutionScope { + OnRead = 0x01, + OnWrite = 0x02, + OnBoth = 0x03, +} + /** * isPrimitiveSymbol. * diff --git a/src/writer.ts b/src/writer.ts index 0daaa21..05c8c37 100644 --- a/src/writer.ts +++ b/src/writer.ts @@ -14,10 +14,10 @@ import { isPrimitiveRelation, type PropertyType, } from './decorators/primitive' -import { type InstantiableObject } from './types' +import { ExecutionScope, type InstantiableObject } from './types' import { usePrePost } from './decorators/prepost' import { useConditions } from './decorators/condition' -import { useTransformer, TransformerExecutionScope } from './decorators/transformer' +import { useTransformer } from './decorators/transformer' import { writeBitField } from './decorators/bitfield' /** @@ -72,7 +72,7 @@ export function binwrite (cursor: BinaryWriter, ObjectDefinition: Instan // Condition don't need to be used since the object are already in here. const transformers = Meta.getTransformers(metadata, field.propertyName) const reversedTransformers = transformers.slice().reverse() - const transformedValue = useTransformer(reversedTransformers, instance[field.propertyName], instance, TransformerExecutionScope.OnWrite) + const transformedValue = useTransformer(reversedTransformers, instance[field.propertyName], instance, ExecutionScope.OnWrite) binWrite(finalRelationField, transformedValue) // TODO Some controller should include instruction on how to normalize the data From c2a11dd66e57dedfc935101a0732cb8a594b8c26 Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Sun, 8 Dec 2024 11:22:28 +0100 Subject: [PATCH 2/5] feat(PrePost): filter with execution scope Add the possibility to only execute PrePost decorator during the reading or writing phase or both. --- src/decorators/prepost.ts | 14 +++++++++++--- src/reader.ts | 10 +++++----- src/writer.ts | 8 ++++++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/decorators/prepost.ts b/src/decorators/prepost.ts index 30f9660..9c3af1d 100644 --- a/src/decorators/prepost.ts +++ b/src/decorators/prepost.ts @@ -49,7 +49,7 @@ */ import { ClassMetaDescriptor, type PropertyMetaDescriptor, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' import { relationExistsOrThrow } from '../error' -import { type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext } from '../types' +import { ExecutionScope, type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext } from '../types' import { type Cursor, type BinaryCursorEndianness, BinaryCursor } from '../cursor' import Meta from '../metadatas' @@ -73,11 +73,17 @@ export interface PrePostOptions { * Removes the decorator from metadata after its function is executed. */ once: boolean + /** + * Specifies whether the prepost function should be executed during + * the read phase, the write phase, or both. + */ + scope: ExecutionScope } export const PrePostOptionsDefault = { primitiveCheck: true, once: false, + scope: ExecutionScope.OnBoth, } /** @@ -632,8 +638,10 @@ export function Endian (endianness: BinaryCursorEndianness | ((instance: T * * @category Advanced Use */ -export function usePrePost (prepost: Array> | Array>, targetInstance: This, cursor: Cursor): void { +export function usePrePost (prepost: Array> | Array>, targetInstance: This, cursor: Cursor, scope = ExecutionScope.OnBoth): void { prepost.forEach((x) => { - x.func(targetInstance, cursor) + if ((x.options.scope & scope) > 0) { + x.func(targetInstance, cursor) + } }) } diff --git a/src/reader.ts b/src/reader.ts index 5603b8f..913b4ab 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -14,7 +14,7 @@ import { isUnknownProperty, type PropertyType, } from './decorators/primitive' -import { EOF, type InstantiableObject } from './types' +import { EOF, ExecutionScope, type InstantiableObject } from './types' import { useController, type ControllerReader } from './decorators/controller' import { useTransformer } from './decorators/transformer' import { useValidators } from './decorators/validator' @@ -100,10 +100,10 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable return useBitField(bitfields, instance, content) } - usePrePost(Meta.getClassPre(metadata), instance, content) + usePrePost(Meta.getClassPre(metadata), instance, content, ExecutionScope.OnRead) Meta.getFields(metadata).forEach((field) => { - usePrePost(Meta.getPre(metadata, field.propertyName), instance, content) + usePrePost(Meta.getPre(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) // 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 @@ -128,10 +128,10 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable instance[field.propertyName] = transformedValue } - usePrePost(Meta.getPost(metadata, field.propertyName), instance, content) + usePrePost(Meta.getPost(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) }) - usePrePost(Meta.getClassPost(metadata), instance, content) + usePrePost(Meta.getClassPost(metadata), instance, content, ExecutionScope.OnRead) return instance } diff --git a/src/writer.ts b/src/writer.ts index 05c8c37..905cf46 100644 --- a/src/writer.ts +++ b/src/writer.ts @@ -64,8 +64,10 @@ export function binwrite (cursor: BinaryWriter, ObjectDefinition: Instan return cursor } + usePrePost(Meta.getClassPre(metadata), instance, cursor, ExecutionScope.OnWrite) + Meta.getFields(metadata).forEach((field) => { - usePrePost(Meta.getPre(metadata, field.propertyName), instance, cursor) + usePrePost(Meta.getPre(metadata, field.propertyName), instance, cursor, ExecutionScope.OnWrite) const finalRelationField = isUnknownProperty(field) ? useConditions(Meta.getConditions(field.metadata, field.propertyName), instance) : field if (finalRelationField !== undefined) { @@ -80,9 +82,11 @@ export function binwrite (cursor: BinaryWriter, ObjectDefinition: Instan // NullString should add back the \0 // targetType sin } - usePrePost(Meta.getPost(metadata, field.propertyName), instance, cursor) + usePrePost(Meta.getPost(metadata, field.propertyName), instance, cursor, ExecutionScope.OnWrite) }) + usePrePost(Meta.getClassPost(metadata), instance, cursor, ExecutionScope.OnWrite) + return cursor } From 99f4c8c5fb279bdc628e582a5d2f322752c8ab4f Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Sun, 8 Dec 2024 21:01:23 +0100 Subject: [PATCH 3/5] fix: TransformScale & TransformOffset should also be applied for every member of an array --- src/decorators/transformer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/decorators/transformer.ts b/src/decorators/transformer.ts index fcf2625..4dc7814 100644 --- a/src/decorators/transformer.ts +++ b/src/decorators/transformer.ts @@ -227,8 +227,8 @@ export function Transform (transformFunction: TransformerFunction (scale: number, opt?: Partial): DecoratorType { return function (_: any, context: Context) { - transformerDecoratorFactory('transform-scale', x => x * scale, opt)(_, context) - transformerDecoratorFactory('transform-scale', x => x / scale, { ...opt, scope: ExecutionScope.OnWrite })(_, context) + transformerDecoratorFactory('transform-scale', x => x * scale, { ...opt, each: true })(_, context) + transformerDecoratorFactory('transform-scale', x => x / scale, { ...opt, each: true, scope: ExecutionScope.OnWrite })(_, context) } } @@ -249,8 +249,8 @@ export function TransformScale (scale: number, opt?: Partial (off: number, opt?: Partial): DecoratorType { return function (_: any, context: Context) { - transformerDecoratorFactory('transform-offset', x => x + off, opt)(_, context) - transformerDecoratorFactory('transform-offset', x => x - off, { ...opt, scope: ExecutionScope.OnWrite })(_, context) + transformerDecoratorFactory('transform-offset', x => x + off, { ...opt, each: true })(_, context) + transformerDecoratorFactory('transform-offset', x => x - off, { ...opt, each: true, scope: ExecutionScope.OnWrite })(_, context) } } From eb745812ba8f9d575aef1b3045dd7255e000100f Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Sun, 8 Dec 2024 21:38:38 +0100 Subject: [PATCH 4/5] fix(Condition): do not throw if no condition matched This allow to create property that won't be read but will still execute the PrePost operations. --- src/decorators/__tests__/condition.test.ts | 17 ----------------- src/decorators/condition.ts | 5 ++++- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/decorators/__tests__/condition.test.ts b/src/decorators/__tests__/condition.test.ts index 1773f3b..323a99a 100644 --- a/src/decorators/__tests__/condition.test.ts +++ b/src/decorators/__tests__/condition.test.ts @@ -1,7 +1,6 @@ import { describe, expect } from '@jest/globals' import { useConditions, IfThen, Else, Choice } from '../condition' import { type RelationTypeProperty, type PrimitiveTypeProperty } from '../primitive' -import { NoConditionMatched } from '../../error' import Meta from '../../metadatas' function testCondition (TargetClass: new (...args: any) => This, relation: any, post?: (relation: PrimitiveTypeProperty | RelationTypeProperty | undefined, instance: This) => void, field: keyof This = 'field' as keyof This) { @@ -119,19 +118,3 @@ describe('@Condition: basic testing', () => { }) }) }) - -describe('@Condition: error testing', () => { - it('NoConditionMatched: should alway define a valid option', () => { - class TestClass { - @IfThen(_ => false, Number) - field: number - } - - const instance = new TestClass() - const conditions = Meta.getConditions(TestClass[Symbol.metadata] as DecoratorMetadataObject, 'field') - - expect(() => { - useConditions(conditions, instance) - }).toThrow(NoConditionMatched) - }) -}) diff --git a/src/decorators/condition.ts b/src/decorators/condition.ts index 9989562..5b0bee8 100644 --- a/src/decorators/condition.ts +++ b/src/decorators/condition.ts @@ -531,7 +531,10 @@ export function useConditions (conditions: Array>, // - cursor backtrace to show the position in the file // - current instance to print. The condition are mostly done on the current instance // - If creating a `@Choice` it's probably more easy to debug. - throw new NoConditionMatched() + // It's still unclear if I need to throw an error or not. Right now usecases + // seems to indicate it's better to not throw. + // throw new NoConditionMatched() + return undefined } return cond.relation From f588d03a2bad20d82ba3e709c81a3e5b64b96ad0 Mon Sep 17 00:00:00 2001 From: Thomas Perale Date: Sun, 8 Dec 2024 21:39:39 +0100 Subject: [PATCH 5/5] 0.3.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 04850da..91cb9e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "binspector", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "binspector", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.14.0", diff --git a/package.json b/package.json index 8a842c7..7e3d096 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.2.1", + "version": "0.3.0", "main": "dist/binspector.js", "types": "dist/index.d.ts", "files": [