Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.4.0 #13

Merged
merged 6 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions example/bitmap/bmp.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -56,16 +53,14 @@ export class Bitmap {
bitmap_header_size: number

@Choice('bitmap_header_size', {
12: OS21XBITMAPHEADER,
16: OS22XBITMAPCOREHEADER,
64: OS22XBITMAPHEADER,
40: BITMAPINFOHEADER,
52: BITMAPV2INFOHEADER,
56: BITMAPV3INFOHEADER,
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
Expand All @@ -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', {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
45 changes: 41 additions & 4 deletions src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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'
import { BinaryReader, BinaryCursorEndianness } from '../cursor'
import { CtxGet, CtxSet } from '../decorators/context'

function expectReadTest<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ...args: any[]) {
return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ...args))
function expectReadTest<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ctx = {}, ...args: any[]) {
return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ctx, ...args))
}

function expectReadTestToThrow<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>) {
Expand Down Expand Up @@ -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,
})
})
Expand Down Expand Up @@ -637,6 +638,42 @@ 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 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', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/__tests__/condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
72 changes: 72 additions & 0 deletions src/decorators/__tests__/context.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
12 changes: 0 additions & 12 deletions src/decorators/__tests__/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 39 additions & 3 deletions src/decorators/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata:
}
}

type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
TObj[TKey] extends any[]
? `${TKey}`
: TObj[TKey] extends object
? `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
: `${TKey}`
}[keyof TObj & (string | number)]

type NumericalString = `${number}`

type RecursiveKeyWithOperations<TObj extends object, T extends string, U extends string = RecursiveKeyOf<TObj> | NumericalString> =
T extends U
? T
: T extends `${U} + ${infer R}`
? T extends `${infer F} + ${R}`
? `${F} + ${RecursiveKeyWithOperations<TObj, R, Exclude<U, F>>}`
: never
: T extends `${U} - ${infer R}`
? T extends `${infer F} - ${R}`
? `${F} - ${RecursiveKeyWithOperations<TObj, R, Exclude<U, F>>}`
: never
: U

type CommaSeparetedRecursiveKey<TObj extends object, T extends string, U extends string = RecursiveKeyOf<TObj>> =
T extends U
? T
: T extends `${U},${infer R}`
? T extends `${infer F},${R}`
? `${F},${CommaSeparetedRecursiveKey<TObj, R, Exclude<U, F>>}`
: never
: U

export type StringFormattedRecursiveKeyOf<T extends object, Args extends string> = Args extends RecursiveKeyWithOperations<T, Args> ? Args : RecursiveKeyWithOperations<T, Args>
export type StringFormattedCommaSepRecursiveKeyOf<T extends object, Args extends string> = Args extends CommaSeparetedRecursiveKey<T, Args> ? Args : CommaSeparetedRecursiveKey<T, Args>

/**
* Chained accessor to get the value of `expr` for `obj`.
*
Expand All @@ -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<T extends object, Args extends string> (obj: T, expr: RecursiveKeyWithOperations<T, Args>): any {
const _isOperation = (x: string): boolean => ['+', '-'].includes(x)

const _get = (path: string): any => path.split('.').reduce((acc: any, key: string) => {
Expand Down Expand Up @@ -109,7 +145,7 @@ export function recursiveGet (obj: any, expr: string): any {
}
}

export function commaSeparetedRecursiveGet (obj: any, args: string): any[] {
export function commaSeparetedRecursiveGet<T extends object, Args extends string> (obj: T, args: CommaSeparetedRecursiveKey<T, Args>): any[] {
const keys = args.split(',')
return keys.map(key => recursiveGet(obj, key.trim()))
return keys.map(key => recursiveGet(obj, key.trim() as RecursiveKeyWithOperations<T, typeof key>))
}
14 changes: 7 additions & 7 deletions src/decorators/condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -87,7 +87,7 @@ export interface Condition<This> extends PropertyMetaDescriptor<This> {
*
* @category Advanced Use
*/
export function conditionDecoratorFactory<This, Target, Value> (name: string, cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function conditionDecoratorFactory<This extends object, Target, Value, Args extends string> (name: string, cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return function (_: undefined, context: Context<This, Value>) {
const propertyName = context.name as keyof This
function createRelation (relationOrPrimitive: Primitive<Target>): PrimitiveTypeProperty<This> | RelationTypeProperty<This, Target> {
Expand Down Expand Up @@ -125,7 +125,7 @@ export function conditionDecoratorFactory<This, Target, Value> (name: string, co
*
* @category Advanced Use
*/
export function dynamicConditionDecoratorFactory<This, Target, Value> (name: string, func: DynamicGetterFunction<This, Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function dynamicConditionDecoratorFactory<This extends object, Target, Value, Args extends string> (name: string, func: DynamicGetterFunction<This, Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return function (_: undefined, context: Context<This, Value>) {
const propertyName = context.name as keyof This
function createRelation (relationOrPrimitive: Primitive<Target>): PrimitiveTypeProperty<This> | RelationTypeProperty<This, Target> {
Expand Down Expand Up @@ -204,7 +204,7 @@ export function dynamicConditionDecoratorFactory<This, Target, Value> (name: str
*
* @category Decorators
*/
export function IfThen<This, Target, Value> (cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function IfThen<This extends object, Target, Value, Args extends string> (cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return conditionDecoratorFactory('ifthen', cond, then, args)
}

Expand Down Expand Up @@ -251,7 +251,7 @@ export function IfThen<This, Target, Value> (cond: ConditionFunction<This>, then
*
* @category Decorators
*/
export function Else<This, Target, Value> (then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Else<This extends object, Target, Value, Args extends string> (then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return conditionDecoratorFactory('else', () => true, then, args)
}

Expand Down Expand Up @@ -430,7 +430,7 @@ export function Else<This, Target, Value> (then?: Primitive<Target>, args?: Rela
*
* @category Decorators
*/
export function Choice<This, Value> (cmp: string | ((targetInstance: This) => any), match: Record<any, Primitive<any> | [Primitive<any>, RelationParameters<This>] | undefined>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Choice<This extends object, Value, Args extends string> (cmp: StringFormattedRecursiveKeyOf<This, Args> | ((targetInstance: This) => any), match: Record<any, Primitive<any> | [Primitive<any>, RelationParameters<This, Args>] | undefined>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
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) => {
Expand Down Expand Up @@ -500,7 +500,7 @@ export function Choice<This, Value> (cmp: string | ((targetInstance: This) => an
* @returns {DecoratorType<This, Value>} The property decorator function.
* @category Decorators
*/
export function Select<This, Value> (getter: ((targetInstance: This) => Primitive<any>), args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Select<This extends object, Value, Args extends string> (getter: ((targetInstance: This) => Primitive<any>), args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return dynamicConditionDecoratorFactory('select', getter, args)
}

Expand Down
Loading
Loading