Skip to content

Latest commit

 

History

History
386 lines (297 loc) · 9.4 KB

File metadata and controls

386 lines (297 loc) · 9.4 KB

Binpat

GitHub Action Codecov License File size

Binpat simplifies parsing binary data in JavaScript by allowing you to define the data structure using declarative patterns.

  • Declarative: Define what your data looks like, no more manual DataView operations and offsets.
  • Readable: Patterns often closely resemble the desired output object structure.
  • Type Safe: Built with TypeScript, providing inferred return types based on your patterns.

Features

  • Common types: u8...u64, i8...i64, f16...f64, bool, string, bitfield and array(pattern, size).
  • Conditional parsing with ternary(condition, truthy, falsy).
  • Transform parsed values using convert(pattern, fn).
  • Control parsing offset with skip(offset), seek(offset) and peek(offset, pattern).
  • Modify output structure with omit() (exclude fields) and spread() (flatten fields).

Installation

Install from NPM Version

npm i binpat

Install from JSR

deno add jsr:@aho/binpat

Import from CDN jsDelivr or

import Binpat from 'https://cdn.jsdelivr.net/npm/binpat/dist/binpat.js';
import Binpat from 'https://unpkg.com/binpat/dist/binpat.js';

Usage

import Binpat, { u8, u16, string, array } from 'binpat';

const filePattern = {
  fileType: string(4),  // e.g., 'DATA'
  version: u8(),        // e.g., 1
  numRecords: u16(),    // e.g., 2 records
  // Read 'numRecords' count of { id: u8, value: u8 }
  records: array({ id: u8(), value: u8() }, (ctx) => ctx.data.numRecords)
};

const binpat = new Binpat(filePattern);

const sampleData = new Uint8Array([
  0x44, 0x41, 0x54, 0x41, // 'DATA'
  0x01,                   // version 1
  0x00, 0x02,             // numRecords 2 (Big Endian)
  0x01, 0x64,             // Record 1: id=1, value=100
  0x02, 0xC8              // Record 2: id=2, value=200
]);

const result = binpat.exec(sampleData.buffer);

console.log(result);
/*
{
  fileType: 'DATA',
  version: 1,
  numRecords: 2,
  records: [
    { id: 1, value: 100 },
    { id: 2, value: 200 },
  ],
}
*/

Find more complex examples in the examples directory.

API

Find the full API details in the docs.

new Binpat(pattern[, option])

pattern can be native object, native array or binpat functions.

new Binpat({ foo: u8() });
new Binpat([u8(), u8()]);
new Binpat(array(u8(), 10));

option can set the global endianness:

{
  // The global endianness.
  // 'big' | 'little'
  endian: 'big', // default
}

u8, u16, u32, u64, i8, i16, i32, i64, f16, f32, f64

All these functions except u8 and i8 accept a boolean param to set endian. They will use the global endianness by default.

import Binpat, { u8, u16 } from 'binpat';

const binpat = new Binpat({
  a: u8(),
  b: u16(),
  // use little endian
  c: u16(true),
});

bool()

import Binpat, { bool } from 'binpat';

const binpat = new Binpat({
  flag: bool(),
});

string(size[, encoding])

You can parse binary to string with a specific text encoding:

const utf8 = Uint8Array.from(new TextEncoder().encode('binpat'));
console.log(new Binpat(string(6)).exec(utf8.buffer));
// 'binpat'

const gbk = new Uint8Array([214, 208, 206, 196]);
console.log(new Binpat(string(4, 'gbk')).exec(gbk.buffer));
// '中文'

And the string size can be read from context dynamicly:

new Binpat({
  size: u8(),
  text: string((ctx) => ctx.data.size),
});

bitfield(layout[, option])

Define layout with native object:

import Binpat, { bitfield, omit } from 'binpat';

const binpat = new Binpat(bitfield({
  a: 3, // unsigned, 3 bits
  b: bitfield.u(3), // unsigned, 3 bits
  [omit('padding')]: 4, // padding, 4 bits
  c: bitfield.i(5), // signed, 5 bits
  d: bitfield.bool(), // boolean, 1 bit
}));
//                                   aaabbbpp    ppcccccd
const { buffer } = new Uint8Array([0b01010101, 0b11001100]);
console.log(binpat.exec(buffer));
// {
//   a: 2,     // 0b010
//   b: 5,     // 0b101
//             // 0b0111 (padding)
//   c: 6,     // 0b00110
//   d: false, // 0b0
// }

option can set endian and Bit numbering:

{
  // The endianness of the bitfield.
  // Allow values: 'big' | 'little'
  // If not specified, it will use the global endianness.
  endian: 'big',
  // Which bit comes first, allow values:
  // + 'MSb': Most Significant Bit (left-to-right).
  // + 'LSb': Least Significant Bit (right-to-left).
  // If not specified:
  // + When endian is 'big', the first bit is 'MSb'.
  // + When endian is 'little', the first bit is 'LSb'.
  first: 'MSb',
}

array(pattern, size)

pattern can be native object or binpat functions.

If the pattern is a primitive type, it will return a TypedArray instance.

import Binpat, { array, u16, u8 } from 'binpat';

const binpat = new Binpat({
  // pattern can be object
  bar: array({ x: u8(), y: u8() }, 4),
  // It will return Uint16Array
  foo: array(u16(), 4),
});

And the array size can be read from context dynamicly:

new Binpat({
  count: u32(),
  items: array(u8(), (ctx) => ctx.data.count),
});

ternary(condition, truthy[, falsy])

It works like condition ? truthy : falsy

import Binpat, { ternary, bool, u16, u8 } from 'binpat';

const binpat = new Binpat({
  flag: bool(),
  value: ternary(
    (ctx) => ctx.data.flag,
    [u8(), u8()],
    [u16()],
  ),
});
console.log(binpat.exec(new Uint8Array([1, 0, 0]).buffer));
// { flag: true, value: [0, 0] }
console.log(binpat.exec(new Uint8Array([0, 0, 0]).buffer));
// { flag: false, value: [0] }

convert(pattern, fn)

Convert the result value with custom function:

import Binpat, { convert, u16 } from 'binpat';

const binpat = new Binpat({
  type: convert(u16(), (value) => ['', 'ico', 'cur'][value] || 'unknown'),
});
console.log(binpat.exec(new Uint8Array([0, 1]).buffer));
// { type: 'ico' }
console.log(binpat.exec(new Uint8Array([0, 2]).buffer));
// { type: 'cur' }
console.log(binpat.exec(new Uint8Array([0, 0]).buffer));
// { type: 'unknown' }

seek(offset)

Move current offset to the given offset.

offset can be number or a function returns a number.

import Binpat, { seek, omit, u8 } from 'binpat';

const binpat = new Binpat({
  foo: u8(),
  [omit('padding')]: seek((ctx) => ctx.offset + 4),
  bar: u8(),
});
const { buffer } = new Uint8Array([1, 0, 0, 0, 0, 2]);
console.log(binpat.exec(buffer));
// { foo: 1, bar: 2 }

peek(offset, pattern)

Reads pattern from the given offset, and doesn't move current offset.

offset can be number or a function returns a number.

import Binpat, { array, peek, u8 } from 'binpat';

const binpat = new Binpat(array({
  size: u32(),
  address: u32(),
  data: peek(
    (ctx) => ctx.data.address,
    array(u8(), (ctx) => ctx.data.size),
  ),
}, 4));

skip(offset)

Move forward with the given offset.

skip(x) is same as seek((ctx) => ctx.offset + x)

import Binpat, { skip, omit, u8 } from 'binpat';

const binpat = new Binpat({
  foo: u8(),
  [omit('padding')]: skip(4),
  bar: u8(),
});
const { buffer } = new Uint8Array([1, 0, 0, 0, 0, 2]);
console.log(binpat.exec(buffer));
// { foo: 1, bar: 2 }

omit(comment?)

Omit the key-value in result.

comment can be any value.

import Binpat, { omit, u16 } from 'binpat';

const binpat = new Binpat({
  [omit('reserved')]: u16(),
  type: u16(),
  count: u16(),
});
const { buffer } = new Uint8Array([0, 0, 0, 1, 0, 1]);
console.log(binpat.exec(buffer));
// { type: 1, count: 1 }

Due to a TypeScript bug, you'll get wrong type with omit(). To solve it, you can use string literal starts with // as object key:

const binpat = new Binpat({
  '// reserved': u16(),
  type: u16(),
  count: u16(),
  // and you should avoid duplicate key
  '// another reserved': u16(),
});

spread(comment?)

It works like spread syntax ..., and usually be used with ternary().

import Binpat, { spread, ternary, bool, u8 } from 'binpat';

const binpat = new Binpat({
  flag: bool(),
  [spread()]: ternary(
    (ctx) => ctx.data.flag,
    { truthy: u8() },
    { falsy: u8() },
  ),
});
console.log(binpat.exec(new Uint8Array([1, 0]).buffer));
// { flag: true, truthy: 0 }
console.log(binpat.exec(new Uint8Array([0, 0]).buffer));
// { flag: false, falsy: 0 }

And like omit(), you can use string literal starts with ... as object key to get correct type inference:

const binpat = new Binpat({
  flag: bool(),
  '...foo': ternary(
    (ctx) => ctx.data.flag,
    { truthy: u8() },
    { falsy: u8() },
  ),
});