├── src ├── common │ ├── writable.ts │ ├── constructor.ts │ ├── index.ts │ ├── incomplete-read-result.ts │ ├── buffered-writable.ts │ └── buffered-writable.test.ts ├── index.ts ├── bitstream │ ├── index.ts │ ├── string-encoding-options.ts │ ├── writer.ts │ ├── writer.test.ts │ └── reader.ts ├── elements │ ├── utils.ts │ ├── value-determinant.ts │ ├── length-determinant.ts │ ├── marker.ts │ ├── decorators.ts │ ├── field-definition.ts │ ├── variant-definition.ts │ ├── variant-options.ts │ ├── null-serializer.ts │ ├── serializer.ts │ ├── index.ts │ ├── number-options.ts │ ├── buffer-options.ts │ ├── structure-serializer.ts │ ├── string-serializer.ts │ ├── reserved.test.ts │ ├── reserved-low.test.ts │ ├── boolean-options.ts │ ├── resolve-length.ts │ ├── reserved-low.ts │ ├── boolean-serializer.ts │ ├── resolve-length.test.ts │ ├── reserved.ts │ ├── array-options.ts │ ├── number-serializer.ts │ ├── buffer-serializer.ts │ ├── variant.ts │ ├── field.ts │ ├── field-options.ts │ └── array-serializer.ts ├── test.ts ├── benchmark.ts └── benchmark-utils.ts ├── .gitignore ├── .nycrc ├── tsconfig.esm.json ├── .npmignore ├── tsconfig.json ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /src/common/writable.ts: -------------------------------------------------------------------------------- 1 | export interface Writable { 2 | write(chunk: Uint8Array): void; 3 | } -------------------------------------------------------------------------------- /src/common/constructor.ts: -------------------------------------------------------------------------------- 1 | export interface Constructor { 2 | new(...args) : T; 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bitstream'; 2 | export * from './elements'; 3 | export * from './common'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | dist.esm/ 3 | node_modules/ 4 | debug.log 5 | coverage/ 6 | .nyc_output/ 7 | scripts/ -------------------------------------------------------------------------------- /src/bitstream/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reader'; 2 | export * from './string-encoding-options'; 3 | export * from './writer'; -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "extension": [".ts", ".js"], 4 | "include": ["dist/**/*.js", "src/**/*.ts"], 5 | "reporter": [ "lcov" ] 6 | } -------------------------------------------------------------------------------- /src/bitstream/string-encoding-options.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface StringEncodingOptions { 3 | encoding? : string; 4 | nullTerminated? : boolean; 5 | } -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './buffered-writable'; 2 | export * from './constructor'; 3 | export * from './writable'; 4 | export * from './incomplete-read-result'; -------------------------------------------------------------------------------- /src/common/incomplete-read-result.ts: -------------------------------------------------------------------------------- 1 | export interface IncompleteReadResult { 2 | remaining: number; 3 | optional?: boolean; 4 | contextHint?: () => string; 5 | } -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist.esm", 5 | "module": "ES2020" 6 | }, 7 | "include": [ 8 | "./src/**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | debug.log 3 | 4 | src/test.ts 5 | src/**/*.test.ts 6 | dist/test.js 7 | dist/**/*.test.js 8 | src/benchmark.ts 9 | src/benchmark-utils.ts 10 | dist/benchmark.js 11 | dist/benchmark-utils.js 12 | scripts/** -------------------------------------------------------------------------------- /src/elements/utils.ts: -------------------------------------------------------------------------------- 1 | import { FieldDefinition } from "./field-definition"; 2 | 3 | export function summarizeField(field: FieldDefinition) { 4 | return `[${field.options.serializer.constructor.name || ''}] ${field.containingType?.name || ''}#${String(field.name)}`; 5 | } -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import "zone.js"; 3 | import "reflect-metadata"; 4 | import "source-map-support/register"; 5 | import "ts-node/register"; 6 | import { suite } from "razmin"; 7 | 8 | globalThis.BITSTREAM_TRACE = false; 9 | 10 | suite() 11 | .include(['**/*.test.js']) 12 | .run() 13 | ; -------------------------------------------------------------------------------- /src/elements/value-determinant.ts: -------------------------------------------------------------------------------- 1 | import { FieldDefinition } from "./field-definition"; 2 | 3 | /** 4 | * Determines the value of a certain field. Can be a value or a function that dynamically 5 | * determines the value based on the current context. 6 | */ 7 | export type ValueDeterminant = T | ((instance : T, f : FieldDefinition) => V); 8 | -------------------------------------------------------------------------------- /src/common/buffered-writable.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "./writable"; 2 | 3 | export class BufferedWritable implements Writable { 4 | buffer : Uint8Array = new Uint8Array(0); 5 | write(chunk : Uint8Array) { 6 | let buf = new Uint8Array(this.buffer.length + chunk.length); 7 | buf.set(this.buffer); 8 | buf.set(chunk, this.buffer.length); 9 | this.buffer = buf; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/elements/length-determinant.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { FieldDefinition } from "./field-definition"; 3 | 4 | /** 5 | * Determines the bitlength of a certain field. Can be a number or a function that dynamically 6 | * determines the value based on the current context. 7 | */ 8 | export type LengthDeterminant = number | ((instance : T, f : FieldDefinition) => number); 9 | -------------------------------------------------------------------------------- /src/elements/marker.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Field } from "./field"; 3 | 4 | /** 5 | * Used to mark a location within a BitstreamElement which can be useful when used with measure(). 6 | * Markers are always ignored (meaning they are not actually read/written to the BitstreamElement instance), and 7 | * they always have a bitlength of zero. 8 | */ 9 | export function Marker() { 10 | return Field(0, { isIgnored: true }); 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "lib": ["DOM", "ES2021"] 13 | }, 14 | "include": [ 15 | "./src/**/*.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/elements/decorators.ts: -------------------------------------------------------------------------------- 1 | 2 | export type InferredPropertyDecorator = (target: T, fieldName: K) => void; 3 | 4 | /** 5 | * Resolves to the type of the given property K on type T unless K is not 6 | * a property of T, in which case the result is U. 7 | * 8 | * Solves an inferrence case with private fields, which cannot be accessed via type, 9 | * so we want to revert to any without causing an error. 10 | */ 11 | export type PropType = K extends keyof T ? T[K] : U; -------------------------------------------------------------------------------- /src/common/buffered-writable.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { describe, it } from "razmin"; 3 | import { BufferedWritable } from "./buffered-writable"; 4 | 5 | describe('BufferedWritable', () => { 6 | it('appends written data onto its buffer', () => { 7 | let writable = new BufferedWritable(); 8 | expect(writable.buffer.length).to.equal(0); 9 | writable.write(Buffer.alloc(3)); 10 | expect(writable.buffer.length).to.equal(3); 11 | writable.write(Buffer.alloc(20)); 12 | expect(writable.buffer.length).to.equal(23); 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/elements/field-definition.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { FieldOptions } from "./field-options"; 3 | import { LengthDeterminant } from "./length-determinant"; 4 | 5 | /** 6 | * Defines the structure of a field definition within a BitstreamElement superclass. 7 | * @see Field 8 | * @see BitstreamElement 9 | */ 10 | export interface FieldDefinition { 11 | length : LengthDeterminant; 12 | name : string | symbol; 13 | containingType : Function; 14 | type : Function; 15 | options : FieldOptions; 16 | } 17 | -------------------------------------------------------------------------------- /src/elements/variant-definition.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from "../common"; 2 | import { BitstreamElement } from "./element"; 3 | import { VariantOptions } from "./variant-options"; 4 | 5 | export type VariantDiscriminant = (element : T, parent? : T['parent']) => boolean; 6 | 7 | /** 8 | * Defines the structure of a Variant subclass of a BitstreamElement superclass. 9 | * 10 | * @see Variant 11 | * @see VariantMarker 12 | */ 13 | export interface VariantDefinition { 14 | type : Constructor; 15 | discriminant : VariantDiscriminant; 16 | options : VariantOptions; 17 | } -------------------------------------------------------------------------------- /src/elements/variant-options.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Defines options for a `@Variant` subclass of a BitstreamElement superclass. 4 | */ 5 | export interface VariantOptions { 6 | /** 7 | * Determine the order in which this variant should be considered during variation. 8 | * The special values "first" and "last" are used to force the variant to be considered 9 | * before all others or after all others respectively. Otherwise the value is a number, with 10 | * lower numbers being considered before higher numbers. 11 | * 12 | * Note: If you just want a default (priority: 'last') variant, it is better to use `@DefaultVariant` 13 | */ 14 | priority? : 'first' | 'last' | number; 15 | } 16 | -------------------------------------------------------------------------------- /src/elements/null-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { BitstreamElement } from "./element"; 3 | import { Serializer } from "./serializer"; 4 | import { FieldDefinition } from "./field-definition"; 5 | import { IncompleteReadResult } from "../common"; 6 | 7 | /** 8 | * Serializes nothing to/from bitstreams. Used when the field is a no-op, such as for fields decorated with `@Marker` 9 | */ 10 | export class NullSerializer implements Serializer { 11 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 12 | } 13 | 14 | write(writer: BitstreamWriter, type : any, instance: any, field: FieldDefinition, value: any) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/elements/serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { IncompleteReadResult } from "../common"; 3 | import { BitstreamElement } from "./element"; 4 | import { FieldDefinition } from "./field-definition"; 5 | 6 | /** 7 | * The abstract interface of a value serializer used within BitstreamElement. 8 | * The library comes with a number of built-in Serializers, or you can create your own 9 | * and use them by specifying the `serializer` option of `@Field()` 10 | */ 11 | export interface Serializer { 12 | read(reader : BitstreamReader, type : Function, parent : BitstreamElement, field : FieldDefinition) : Generator; 13 | write(writer : BitstreamWriter, type : Function, parent : BitstreamElement, field : FieldDefinition, value : any); 14 | } 15 | -------------------------------------------------------------------------------- /src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array-options'; 2 | export * from './array-serializer'; 3 | export * from './boolean-options'; 4 | export * from './boolean-serializer'; 5 | export * from './buffer-options'; 6 | export * from './buffer-serializer'; 7 | export * from './element'; 8 | export * from './field-definition'; 9 | export * from './field-options'; 10 | export * from './field'; 11 | export * from './length-determinant'; 12 | export * from './null-serializer'; 13 | export * from './number-options'; 14 | export * from './number-serializer'; 15 | export * from './resolve-length'; 16 | export * from './serializer'; 17 | export * from './string-serializer'; 18 | export * from './structure-serializer'; 19 | export * from './value-determinant'; 20 | export * from './variant-definition'; 21 | export * from './variant-options'; 22 | export * from './variant'; 23 | export * from './reserved'; 24 | export * from './reserved-low'; 25 | export * from './marker'; -------------------------------------------------------------------------------- /src/elements/number-options.ts: -------------------------------------------------------------------------------- 1 | export interface NumberOptions { 2 | /** 3 | * Binary format to use for this number field. 4 | * - `unsigned`: The value is treated as an unsigned integer 5 | * - `signed`: The value is treated as a signed two's complement integer 6 | * - `float`: The value is treated as an IEEE 754 floating point number. 7 | * Only lengths of 32 (32-bit single-precision) and 64 8 | * (64-bit double-precision) bits are supported 9 | */ 10 | format? : 'unsigned' | 'signed' | 'float'; 11 | 12 | /** 13 | * Specify the byte order for this number. 14 | * - **big-endian** - Also known as network byte order, this is the default. 15 | * - **little-endian** - Least significant byte first. Only valid when the field 16 | * length is a multiple of 8 bits (ie it contains 1 or more whole bytes) 17 | */ 18 | byteOrder? : 'big-endian' | 'little-endian'; 19 | 20 | /** 21 | * Allow using the 'number' type on fields longer than 53 bits. Consider using bigint instead of this option. 22 | */ 23 | allowOversized?: boolean; 24 | } -------------------------------------------------------------------------------- /src/elements/buffer-options.ts: -------------------------------------------------------------------------------- 1 | export interface BufferOptions { 2 | /** 3 | * When true (default), the buffer will be truncated based on the calculated field length when writing. 4 | * When false, the buffer will be written out in it's entirety, regardless of the calculated field length. 5 | * Disabling truncation can be useful when an earlier field determines how many bytes to *read*, but when 6 | * writing is determined by the size of this buffer (usually via writtenValue). 7 | * The default is true. 8 | */ 9 | truncate?: boolean; 10 | 11 | /** 12 | * When false, buffers that are shorter than the calculated field length will be written as is. When set to 13 | * a number, the buffer will be expanded to the proper size with the new slots filled with the given value. 14 | * When this value is unspecified, the default behavior depends on the value of the `truncate` option- when 15 | * `truncate` is `true` this option is assumed to be `0` (ie expand and fill with zeroes). When `truncate` is 16 | * `false`, this option is assumed to be `false`. 17 | */ 18 | fill?: number | false; 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Astronaut Labs, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/elements/structure-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { Serializer } from "./serializer"; 3 | import { FieldDefinition } from "./field-definition"; 4 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 5 | import { IncompleteReadResult } from "../common"; 6 | 7 | /** 8 | * Serializes BitstreamElement instances to/from bitstreams 9 | */ 10 | export class StructureSerializer implements Serializer { 11 | *read(reader : BitstreamReader, type : typeof BitstreamElement, parent : BitstreamElement, field : FieldDefinition): Generator { 12 | let g = type.read(reader, { parent, field, context: parent.context }); 13 | while (true) { 14 | let result = g.next(); 15 | if (result.done === false) 16 | yield result.value; 17 | else 18 | return result.value; 19 | } 20 | } 21 | 22 | write(writer: BitstreamWriter, type : any, instance: any, field: FieldDefinition, value: BitstreamElement) { 23 | if (!value) 24 | throw new Error(`Cannot write ${field.type.name}#${String(field.name)}: Value is null/undefined`); 25 | value.write(writer, { skip: field.options?.skip, context: instance?.context }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/elements/string-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { BitstreamElement } from "./element"; 3 | import { resolveLength } from "./resolve-length"; 4 | import { Serializer } from "./serializer"; 5 | import { FieldDefinition } from "./field-definition"; 6 | import { IncompleteReadResult } from "../common"; 7 | import { summarizeField } from "./utils"; 8 | 9 | /** 10 | * Serializes strings to/from bitstreams 11 | */ 12 | export class StringSerializer implements Serializer { 13 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 14 | let length = resolveLength(field.length, parent, field); 15 | 16 | if (!reader.isAvailable(length*8)) 17 | yield { remaining: length*8, contextHint: () => summarizeField(field) }; 18 | 19 | return reader.readStringSync(length, field.options.string); 20 | } 21 | 22 | write(writer : BitstreamWriter, type : any, parent : BitstreamElement, field : FieldDefinition, value : any) { 23 | let length : number; 24 | try { 25 | length = resolveLength(field.length, parent, field); 26 | } catch (e) { 27 | throw new Error(`Failed to resolve length of string via 'length' determinant: ${e.message}`); 28 | } 29 | 30 | writer.writeString(length, `${value}`, field?.options?.string?.encoding || 'utf-8'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # kick: 1 2 | 3 | version: 2.1 4 | 5 | commands: 6 | run_tests: 7 | description: "Build @astronautlabs/scte35 and run the test suite" 8 | parameters: 9 | version: 10 | type: string 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | keys: 15 | - v1-dependencies-<< parameters.version >>-{{ checksum "package.json" }} 16 | - v1-dependencies-<< parameters.version >>- 17 | - run: npm install 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-<< parameters.version >>-{{ checksum "package.json" }} 22 | - run: npm test 23 | - run: npm test # perhaps this will fix coverage reporting? 24 | - run: npm run benchmark 25 | - store_artifacts: 26 | path: coverage 27 | jobs: 28 | node-15: 29 | docker: 30 | - image: circleci/node:15 31 | working_directory: ~/repo 32 | steps: 33 | - run_tests: 34 | version: "15" 35 | node-14: 36 | docker: 37 | - image: circleci/node:14 38 | working_directory: ~/repo 39 | steps: 40 | - run_tests: 41 | version: "14" 42 | node-12: 43 | docker: 44 | - image: circleci/node:12 45 | working_directory: ~/repo 46 | steps: 47 | - run_tests: 48 | version: "12" 49 | 50 | workflows: 51 | version: 2 52 | build: 53 | jobs: 54 | - node-15 55 | - node-14 56 | - node-12 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astronautlabs/bitstream", 3 | "version": "4.2.2", 4 | "description": "Utilities for packing/unpacking fields of a bitstream", 5 | "main": "dist/index.js", 6 | "module": "dist.esm/index.js", 7 | "types": "dist/index.d.ts", 8 | "private": false, 9 | "keywords": [ 10 | "bitstream", 11 | "serialization", 12 | "parsing", 13 | "binary", 14 | "protocol", 15 | "network", 16 | "syntax" 17 | ], 18 | "scripts": { 19 | "clean": "rimraf dist dist.esm", 20 | "build": "npm run clean && tsc -b && tsc -b tsconfig.esm.json", 21 | "test": "npm run build && nyc node dist/test", 22 | "test:nocov": "npm run build && node dist/test", 23 | "benchmark": "npm run build && node dist/benchmark", 24 | "prepublishOnly": "npm test" 25 | }, 26 | "repository": { 27 | "url": "git@github.com:astronautlabs/bitstream.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/astronautlabs/bitstream/issues" 31 | }, 32 | "author": "Astronaut Labs", 33 | "license": "MIT", 34 | "peerDependencies": { 35 | "reflect-metadata": "^0.1.13" 36 | }, 37 | "devDependencies": { 38 | "@types/chai": "^4.2.14", 39 | "@types/node": "^14.0.4", 40 | "@types/reflect-metadata": "^0.1.0", 41 | "chai": "^4.2.0", 42 | "nyc": "^15.1.0", 43 | "razmin": "^0.6.20", 44 | "rimraf": "^3.0.2", 45 | "source-map-support": "^0.5.21", 46 | "ts-node": "^10.4.0", 47 | "typescript": "^5.3.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/elements/reserved.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { describe } from "razmin"; 3 | import { BitstreamElement } from "./element"; 4 | import { Field } from "./field"; 5 | import { Reserved } from "./reserved"; 6 | 7 | describe('@Reserved()', it => { 8 | it('always writes high bits', () => { 9 | class CustomElement extends BitstreamElement { 10 | @Field(8) a : number; 11 | @Reserved(8) reserved : number; 12 | @Field(8) b : number; 13 | } 14 | 15 | let buf = new CustomElement().with({ a: 123, reserved: 111, b: 122 }).serialize(); 16 | 17 | expect(Array.from(buf)).to.eql([ 123, 255, 122]); 18 | }); 19 | it('supports determinants', () => { 20 | class CustomElement extends BitstreamElement { 21 | @Field(8) a : number; 22 | @Reserved(i => 8) reserved : number; 23 | @Field(8) b : number; 24 | } 25 | 26 | let buf = new CustomElement().with({ a: 123, reserved: 111, b: 122 }).serialize(); 27 | 28 | expect(Array.from(buf)).to.eql([ 123, 255, 122]); 29 | }); 30 | it('is never read', () => { 31 | class CustomElement extends BitstreamElement { 32 | @Field(8) a : number; 33 | @Reserved(8) reserved : number; 34 | @Field(8) b : number; 35 | } 36 | 37 | let element = CustomElement.deserialize(Buffer.from([ 123, 111, 122 ])); 38 | 39 | expect(element.a).to.equal(123); 40 | expect(element.reserved).to.be.undefined; 41 | expect(element.b).to.equal(122); 42 | }); 43 | }); -------------------------------------------------------------------------------- /src/elements/reserved-low.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { describe } from "razmin"; 3 | import { ReservedLow } from "./reserved-low"; 4 | import { BitstreamElement } from "./element"; 5 | import { Field } from "./field"; 6 | 7 | describe('@ReservedLow()', it => { 8 | it('always writes low bits', () => { 9 | class CustomElement extends BitstreamElement { 10 | @Field(8) a : number; 11 | @ReservedLow(8) reserved : number; 12 | @Field(8) b : number; 13 | } 14 | 15 | let buf = new CustomElement().with({ a: 123, reserved: 111, b: 122 }).serialize(); 16 | 17 | expect(Array.from(buf)).to.eql([ 123, 0, 122]); 18 | }); 19 | it('supports determinants', () => { 20 | class CustomElement extends BitstreamElement { 21 | @Field(8) a : number; 22 | @ReservedLow(i => 8) reserved : number; 23 | @Field(8) b : number; 24 | } 25 | 26 | let buf = new CustomElement().with({ a: 123, reserved: 111, b: 122 }).serialize(); 27 | 28 | expect(Array.from(buf)).to.eql([ 123, 0, 122]); 29 | }); 30 | it('is never read', () => { 31 | class CustomElement extends BitstreamElement { 32 | @Field(8) a : number; 33 | @ReservedLow(8) reserved : number; 34 | @Field(8) b : number; 35 | } 36 | 37 | let element = CustomElement.deserialize(Buffer.from([ 123, 111, 122 ])); 38 | 39 | expect(element.a).to.equal(123); 40 | expect(element.reserved).to.be.undefined; 41 | expect(element.b).to.equal(122); 42 | }); 43 | }); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Test", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}\\dist\\test.js", 15 | "preLaunchTask": "tsc: build - tsconfig.json", 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ] 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Benchmark (Sync)", 24 | "skipFiles": [ 25 | "/**" 26 | ], 27 | "program": "${workspaceFolder}\\dist\\benchmark.js", 28 | "args": ["sync"], 29 | "preLaunchTask": "tsc: build - tsconfig.json", 30 | "outFiles": [ 31 | "${workspaceFolder}/dist/**/*.js" 32 | ] 33 | }, 34 | { 35 | "type": "node", 36 | "request": "launch", 37 | "name": "Benchmark (Async)", 38 | "skipFiles": [ 39 | "/**" 40 | ], 41 | "program": "${workspaceFolder}\\dist\\benchmark.js", 42 | "preLaunchTask": "tsc: build - tsconfig.json", 43 | "outFiles": [ 44 | "${workspaceFolder}/dist/**/*.js" 45 | ] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /src/elements/boolean-options.ts: -------------------------------------------------------------------------------- 1 | export interface BooleanOptions { 2 | /** 3 | * Numeric value to use when writing true to bitstream. 4 | * Also used to interpret values read from the bitstream 5 | * according to the chosen 'mode'. The default value is 1. 6 | */ 7 | true? : number; 8 | 9 | /** 10 | * Numeric value to use when writing false to bitstream. 11 | * Also used to interpret values read from the bitstream 12 | * according to the chosen 'mode'. The default value is 0. 13 | */ 14 | false? : number; 15 | 16 | /** 17 | * Numeric value to use when writing `undefined` to bitstream. 18 | * The default value is 0. 19 | */ 20 | undefined? : number; 21 | 22 | /** 23 | * How to handle serialization of booleans. 24 | * - `true-unless`: The value is true unless the numeric 25 | * value chosen for 'false' is observed (default mode). 26 | * For example `0` is `false`, `1` is `true`, `100` is `true` 27 | * - `false-unless`: The value is false unless the numeric 28 | * value chosen for 'true' is observed. For example `0` is 29 | * `false`, `1` is `true`, `100` is `false` 30 | * - `undefined`: The value is true when the numeric value 31 | * chosen for 'true' is observed. The value is false when 32 | * the numeric value chosen for 'false' is observed. In 33 | * all other cases, the value will be serialized as `undefined`. 34 | * Note that you should choose a numeric value for 'undefined' 35 | * to ensure `undefined` values do not collapse to the 'false' 36 | * value when writing to the bitstream. For example `0` is 37 | * `false`, `1` is `true`, `100` is `undefined`. 38 | */ 39 | mode? : 'true-unless' | 'false-unless' | 'undefined'; 40 | } -------------------------------------------------------------------------------- /src/elements/resolve-length.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { LengthDeterminant } from "./length-determinant"; 3 | import { FieldDefinition } from "./field-definition"; 4 | 5 | /** 6 | * Given a LengthDeterminant function, a BitstreamElement instance (the context), and a field definition, 7 | * this function returns the number of bits that should be read. This is used to determine the actual bitlength 8 | * of fields when reading and writing BitstreamElement via BitstreamReader and BitstreamWriter. 9 | * @param determinant The length determinant 10 | * @param parent The BitstreamElement instance for context 11 | * @param field The field definition for context 12 | * @returns The bitlength of the field in this context 13 | */ 14 | export function resolveLength(determinant : LengthDeterminant, parent : T, field : FieldDefinition) { 15 | if (typeof determinant === 'number') 16 | return determinant; 17 | 18 | if (!parent) 19 | throw new Error(`Cannot resolve length without an instance!`); 20 | 21 | let length = parent.runWithFieldBeingComputed(field, () => determinant(parent, field)); 22 | 23 | if (typeof length !== 'number') 24 | throw new Error(`${field.containingType.name}#${String(field.name)}: Length determinant returned non-number value: ${length}`); 25 | 26 | if (length < 0) { 27 | let message = `${field.containingType.name}#${String(field.name)}: Length determinant returned negative value ${length} -- Value read so far: ${JSON.stringify(parent, undefined, 2)}`; 28 | 29 | console.error(message); 30 | console.error(`============= Item =============`); 31 | console.dir(parent); 32 | let debugParent = parent.parent; 33 | while (debugParent) { 34 | console.error(`============= Parent =============`); 35 | console.dir(debugParent); 36 | debugParent = debugParent.parent; 37 | } 38 | 39 | throw new Error(message); 40 | } 41 | 42 | return length; 43 | } 44 | -------------------------------------------------------------------------------- /src/elements/reserved-low.ts: -------------------------------------------------------------------------------- 1 | import { InferredPropertyDecorator } from "./decorators"; 2 | import { BitstreamElement } from "./element"; 3 | import { Field } from "./field"; 4 | import { FieldDefinition } from "./field-definition"; 5 | import { FieldOptions } from "./field-options"; 6 | import { LengthDeterminant } from "./length-determinant"; 7 | 8 | /** 9 | * Used to mark a specific field as reserved. The value in this field will be read, but will not be 10 | * copied into the BitsreamElement, and when writing the value will always be all low bits. For a 11 | * version using high bits, see `@Reserved` 12 | * 13 | * Oftentimes it is desirable to avoid naming reserved fields, especially in formats with lots of small reservation 14 | * sections. Unfortunately Typescript doesn't provide a good way to do this (computed symbol names cannot be generated by 15 | * function calls). 16 | * 17 | * However, be assured that if you reuse a reserved field name in a subclass (which is not itself an error in Typescript), 18 | * the resulting bitstream representation will still be correct. There are two reasons for this: 19 | * - Every new field declaration is a new syntax field, even if the field exists in a superclass. 20 | * - `@ReservedLow()` specifically replaces the name you specify with an anonymous symbol 21 | * 22 | * @param length The bitlength determinant 23 | * @param options Options related to this reserved field 24 | */ 25 | export function ReservedLow(length : LengthDeterminant, options : Omit, 'writtenValue' | 'isIgnored'> = {}): InferredPropertyDecorator{ 26 | return (target : T, fieldName : string | symbol) => { 27 | fieldName = Symbol(`[reserved: ${typeof length === 'number' ? `${length} bits` : `dynamic`}]`); 28 | Reflect.defineMetadata('design:type', Number, target, fieldName); 29 | return Field(length, { 30 | ...options, 31 | isIgnored: true, 32 | writtenValue: (_, field : FieldDefinition) => { 33 | if (field.type === Number) 34 | return 0; 35 | } 36 | })(target, fieldName); 37 | } 38 | } -------------------------------------------------------------------------------- /src/elements/boolean-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { BitstreamElement } from "./element"; 3 | import { resolveLength } from "./resolve-length"; 4 | import { Serializer } from "./serializer"; 5 | import { FieldDefinition } from "./field-definition"; 6 | import { IncompleteReadResult } from "../common"; 7 | import { summarizeField } from "./utils"; 8 | 9 | /** 10 | * Serializes booleans to/from bitstreams. 11 | */ 12 | export class BooleanSerializer implements Serializer { 13 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 14 | let length = resolveLength(field.length, parent, field); 15 | if (!reader.isAvailable(length)) 16 | yield { remaining: length, contextHint: () => summarizeField(field) }; 17 | 18 | const numericValue = reader.readSync(length); 19 | const trueValue = field?.options?.boolean?.true ?? 1; 20 | const falseValue = field?.options?.boolean?.false ?? 0; 21 | const mode = field?.options?.boolean?.mode ?? 'true-unless'; 22 | 23 | if (mode === 'true-unless') 24 | return numericValue !== falseValue; 25 | else if (mode === 'false-unless') 26 | return numericValue === trueValue; 27 | else if (mode === 'undefined') 28 | return numericValue === trueValue ? true : numericValue === falseValue ? false : undefined; 29 | } 30 | 31 | write(writer: BitstreamWriter, type : any, instance: any, field: FieldDefinition, value: any) { 32 | 33 | const trueValue = field?.options?.boolean?.true ?? 1; 34 | const falseValue = field?.options?.boolean?.false ?? 0; 35 | const undefinedValue = field?.options?.boolean?.undefined ?? 0; 36 | let numericValue : number; 37 | 38 | if (value === void 0) 39 | numericValue = undefinedValue; 40 | else if (value) 41 | numericValue = trueValue; 42 | else 43 | numericValue = falseValue; 44 | 45 | writer.write(resolveLength(field.length, instance, field), numericValue); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/elements/resolve-length.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { describe } from "razmin"; 3 | import { BitstreamElement } from "./element"; 4 | import { Field } from "./field"; 5 | import { resolveLength } from "./resolve-length"; 6 | 7 | describe('resolveLength()', it => { 8 | class CustomElement extends BitstreamElement { 9 | @Field(8) a : number; 10 | } 11 | class ContainerElement extends BitstreamElement { 12 | @Field() custom : CustomElement; 13 | } 14 | 15 | const element = new CustomElement().with({ a: 32 }); 16 | const container = new ContainerElement(); 17 | 18 | element.parent = container; 19 | const aField = CustomElement.syntax.find(x => x.name === 'a'); 20 | 21 | it('should execute the determinant and return its value', () => { 22 | expect(resolveLength(i => i.a, element, aField)).to.equal(32); 23 | }); 24 | it('should throw with determinant and no instance', () => { 25 | let caught; 26 | 27 | try { 28 | resolveLength(i => i.a, undefined, undefined); 29 | } catch (e) { caught = e; } 30 | 31 | expect(caught).to.exist; 32 | }); 33 | it('should throw when determinant returns negative value', () => { 34 | const consoleT = console; 35 | 36 | try { 37 | (globalThis as any).console = { 38 | log() { }, 39 | error() { }, 40 | dir() { } 41 | } 42 | 43 | let caught; 44 | try { 45 | resolveLength(i => -1, element, aField); 46 | } catch (e) { caught = e; } 47 | 48 | expect(caught).to.exist; 49 | expect(caught.message).to.contain('Length determinant returned negative value'); 50 | 51 | } finally { 52 | (globalThis as any).console = consoleT; 53 | } 54 | }); 55 | it('should recognize and return literal values', () => { 56 | expect(resolveLength(100, element, aField)).to.equal(100); 57 | }); 58 | it('should support literals even when no instance is available', () => { 59 | expect(resolveLength(100, undefined, undefined)).to.equal(100); 60 | }); 61 | }); -------------------------------------------------------------------------------- /src/elements/reserved.ts: -------------------------------------------------------------------------------- 1 | import { resolveLength } from "./resolve-length"; 2 | import { FieldDefinition } from "./field-definition"; 3 | import { FieldOptions } from "./field-options"; 4 | import { LengthDeterminant } from "./length-determinant"; 5 | import { Field } from "./field"; 6 | import { BitstreamElement } from "./element"; 7 | import { InferredPropertyDecorator } from "./decorators"; 8 | 9 | /** 10 | * Used to mark a specific field as reserved. The value in this field will be read, but will not be 11 | * copied into the BitsreamElement, and when writing the value will always be all high bits. For a 12 | * version using low bits, see `@ReservedLow`. 13 | * 14 | * Oftentimes it is desirable to avoid naming reserved fields, especially in formats with lots of small reservation 15 | * sections. Unfortunately Typescript doesn't provide a good way to do this (computed symbol names cannot be generated by 16 | * function calls). 17 | * 18 | * However, be assured that if you reuse a reserved field name in a subclass (which is not itself an error in Typescript), 19 | * the resulting bitstream representation will still be correct. There are two reasons for this: 20 | * - Every new field declaration is a new syntax field, even if the field exists in a superclass. 21 | * - `@Reserved()` specifically replaces the name you specify with an anonymous symbol 22 | * 23 | * @param length The bitlength determinant 24 | * @param options Options related to this reserved field 25 | */ 26 | 27 | export function Reserved(length : LengthDeterminant, options : Omit, 'writtenValue' | 'isIgnored'> = {}): InferredPropertyDecorator 28 | { 29 | return (target : T, fieldName : string | symbol) => { 30 | fieldName = Symbol(`[reserved: ${typeof length === 'number' ? `${length} bits` : `dynamic`}]`); 31 | Reflect.defineMetadata('design:type', Number, target, fieldName); 32 | 33 | return Field(length, { 34 | ...options, 35 | isIgnored: true, 36 | writtenValue: (instance, field : FieldDefinition) => { 37 | if (field.type === Number) { 38 | let currentLength = resolveLength(field.length, instance, field); 39 | return Math.pow(2, currentLength) - 1; 40 | } 41 | } 42 | })(target, fieldName); 43 | } 44 | } -------------------------------------------------------------------------------- /src/elements/array-options.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { LengthDeterminant } from "./length-determinant"; 3 | 4 | export type HasMore = (array : ArrayT, element : InstanceT, parent? : ParentT) => boolean; 5 | 6 | export interface ArrayOptions { 7 | /** 8 | * The length (in bits) of the count field which 9 | * precedes the data of the array. 10 | * 11 | * Only one of `count`, `countFieldLength`, `hasMore` can be specified simultaneously. 12 | */ 13 | countFieldLength? : number; 14 | 15 | /** 16 | * The number of items in the array. Can be a fixed number or 17 | * can be dependent on the fields already parsed. When writing the structure 18 | * to bitstream, this value must be less than or equal to the number of items 19 | * in the provided array. When the value is greater than the number of items in 20 | * an array, an exception is thrown. When the value is less than the number of items in 21 | * an array, the number of items written is truncated to the count. 22 | * 23 | * Note that you can specify `count` or `countFieldLength` but not both. When `count` 24 | * is specified, no length field is written as part of the array field (because it is presumed 25 | * to be an implied length, or the length is represented elsewhere in the structure). 26 | * 27 | * Only one of `count`, `countFieldLength`, `hasMore` can be specified simultaneously. 28 | */ 29 | count? : LengthDeterminant; 30 | 31 | /** 32 | * Whether to read another item. When specified, the discriminant is executed before reading each item. 33 | * If the discriminant returns true, another item is read, If false, the field is finalized and parsing continues 34 | * on. 35 | * 36 | * Only one of `count`, `countFieldLength`, `hasMore` can be specified simultaneously. 37 | */ 38 | hasMore? : HasMore; 39 | 40 | /** 41 | * The Javascript type of the elements of this array. 42 | * Can either be a class that extends from BitstreamElement 43 | * or `Number`. 44 | */ 45 | type? : Function; 46 | 47 | /** 48 | * How long each element is in bits. This is only 49 | * meaningful for array fields where `type` is 50 | * set to Number. 51 | */ 52 | elementLength? : number; 53 | } -------------------------------------------------------------------------------- /src/elements/number-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamElement } from "./element"; 2 | import { resolveLength } from "./resolve-length"; 3 | import { Serializer } from "./serializer"; 4 | import { FieldDefinition } from "./field-definition"; 5 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 6 | import { IncompleteReadResult } from "../common"; 7 | import { summarizeField } from "./utils"; 8 | 9 | /** 10 | * Serializes numbers to/from bitstreams 11 | */ 12 | export class NumberSerializer implements Serializer { 13 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 14 | let length : number; 15 | try { 16 | length = resolveLength(field.length, parent, field); 17 | } catch (e) { 18 | throw new Error(`Failed to resolve length of number via 'length' determinant: ${e.message}`); 19 | } 20 | 21 | if (!reader.isAvailable(length)) 22 | yield { remaining: length, contextHint: () => summarizeField(field) }; 23 | 24 | let format = field.options?.number?.format ?? 'unsigned'; 25 | if (format === 'unsigned') 26 | return reader.readSync(length); 27 | else if (format === 'signed') 28 | return reader.readSignedSync(length); 29 | else if (format === 'float') 30 | return reader.readFloatSync(length); 31 | else 32 | throw new TypeError(`Unsupported number format '${format}'`); 33 | } 34 | 35 | write(writer: BitstreamWriter, type : any, instance: any, field: FieldDefinition, value: any) { 36 | if (value === undefined) 37 | value = 0; 38 | 39 | let length : number; 40 | try { 41 | length = resolveLength(field.length, instance, field); 42 | } catch (e) { 43 | throw new Error(`Failed to resolve length of number via 'length' determinant: ${e.message}`); 44 | } 45 | 46 | let format = field.options?.number?.format ?? 'unsigned'; 47 | 48 | if (format === 'unsigned') 49 | writer.write(length, value); 50 | else if (format === 'signed') 51 | writer.writeSigned(length, value); 52 | else if (format === 'float') 53 | writer.writeFloat(length, value); 54 | else 55 | throw new TypeError(`Unsupported number format '${format}'`); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/elements/buffer-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { Serializer } from "./serializer"; 3 | import { FieldDefinition } from "./field-definition"; 4 | import { BitstreamElement } from "./element"; 5 | import { resolveLength } from "./resolve-length"; 6 | import { IncompleteReadResult } from "../common"; 7 | import { summarizeField } from "./utils"; 8 | 9 | /** 10 | * Serializes buffers to/from bitstreams 11 | */ 12 | export class BufferSerializer implements Serializer { 13 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 14 | let length : number; 15 | 16 | try { 17 | length = resolveLength(field.length, parent, field) / 8; 18 | } catch (e) { 19 | throw new Error(`Failed to resolve length for buffer via 'length': ${e.message}`); 20 | } 21 | 22 | let buffer : Uint8Array; 23 | 24 | if (typeof Buffer !== 'undefined' && field.type === Buffer) 25 | buffer = Buffer.alloc(length); 26 | else 27 | buffer = new Uint8Array(length); 28 | 29 | let gen = reader.readBytes(buffer); 30 | 31 | while (true) { 32 | let result = gen.next(); 33 | if (result.done === false) 34 | yield { remaining: result.value*8, contextHint: () => summarizeField(field) }; 35 | else 36 | break; 37 | } 38 | 39 | return buffer; 40 | } 41 | 42 | write(writer: BitstreamWriter, type : any, parent : BitstreamElement, field: FieldDefinition, value: Uint8Array) { 43 | let length : number; 44 | 45 | try { 46 | length = resolveLength(field.length, parent, field) / 8; 47 | } catch (e) { 48 | throw new Error(`Failed to resolve length for buffer via 'length': ${e.message}`); 49 | } 50 | 51 | let fieldLength = Math.floor(length); 52 | let truncate = field.options?.buffer?.truncate ?? true; 53 | let fill = field.options?.buffer?.fill ?? (truncate ? 0 : false); 54 | 55 | if (!value) { 56 | throw new Error(`BufferSerializer: Field ${String(field.name)}: Cannot encode a null buffer`); 57 | } 58 | 59 | if (value.length > fieldLength) { 60 | if (truncate) { 61 | writer.writeBuffer(value.subarray(0, fieldLength)); 62 | return; 63 | } 64 | } else if (value.length < fieldLength) { 65 | if (fill !== false) { 66 | let filledBuffer = new Uint8Array(fieldLength).fill(fill); 67 | 68 | for (let i = 0, max = value.length; i < max; ++i) 69 | filledBuffer[i] = value[i]; 70 | 71 | writer.writeBuffer(filledBuffer); 72 | return; 73 | } 74 | } 75 | 76 | writer.writeBuffer(value); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/elements/variant.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from "../common"; 2 | import { InferredPropertyDecorator } from "./decorators"; 3 | import { BitstreamElement } from "./element"; 4 | import { Field } from "./field"; 5 | import { VariantDefinition, VariantDiscriminant } from "./variant-definition"; 6 | import { VariantOptions } from "./variant-options"; 7 | 8 | export interface DiscriminatedVariant { 9 | variantDiscriminant?: VariantDiscriminant; 10 | } 11 | 12 | /** 13 | * Decorator which can be applied to subclasses of a BitstreamElement class which marks the subclass 14 | * as an option for "upgrading" an element being read. In order for the subclass to be considered, the 15 | * given discriminant function must be true when called passing the instance of the superclass which is 16 | * currently being read. The process of determining which variant subclass should be used is called "variation". 17 | * By default variation occurs once all fields of the superclass have been read unless the superclass has a 18 | * `@VariantMarker` decorator, in which case it is performed at the point in the structure where the variant 19 | * marker is placed. 20 | * 21 | * Note that the default type assigned to the "element" parameter of the passed variant discriminant is 22 | * actually the type that the decorator is placed on, but that's technically not correct, since the passed 23 | * value is an instance of the parent class. Right now Typescript doesn't have a way to type this, and we 24 | * figured it was better to be brief and almost correct than the alternative. 25 | * 26 | * If you want to ensure the type of the element pasesd to the discriminator is exactly correct, specify the 27 | * type parameter (ie `@Variant(i => i.parentProperty)`) 28 | * 29 | * @param discriminant A function which determines whether the Variant is valid for a given object being read 30 | * @param options A set of options that modify the applicability of the variant. @see VariantOptions 31 | */ 32 | export function Variant(discriminant : VariantDiscriminant, options? : VariantOptions) { 33 | return (type: Constructor & typeof BitstreamElement & DiscriminatedVariant) => { 34 | let parent = Object.getPrototypeOf(type.prototype).constructor; 35 | 36 | if (!(parent).hasOwnProperty('ownVariants')) 37 | Object.defineProperty(parent, 'ownVariants', { value: [] }); 38 | 39 | if (!options) 40 | options = {}; 41 | 42 | parent.ownVariants.push(>{ type, discriminant, options }); 43 | type.variantDiscriminant = discriminant; 44 | }; 45 | } 46 | 47 | /** 48 | * This decorator is a special form of `@Variant` which marks the subclass as the "least priority default" 49 | * variant subclass. 50 | * This is equivalent to `@Variant(() => true, { priority: 'last' })` 51 | */ 52 | export function DefaultVariant() { 53 | return Variant(() => true, { priority: 'last' }); 54 | } 55 | 56 | /** 57 | * A decorator which can be applied to a marker in a BitstreamElement class that indicates where 58 | * fields of a variant subclass should be read relative to the other fields of the class. This can be 59 | * used to "sandwich" subclass fields in a specific spot between two fields of the superclass. 60 | */ 61 | export function VariantMarker(): InferredPropertyDecorator { 62 | return Field(0, { isVariantMarker: true }); 63 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the maintainer at wilahti@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /src/benchmark.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import "source-map-support/register"; 3 | 4 | import { FPSCounter, Generator, runTests } from './benchmark-utils'; 5 | import { BitstreamReader } from './bitstream'; 6 | 7 | globalThis.BITSTREAM_TRACE = false; 8 | 9 | runTests([ 10 | { 11 | name: 'byteAlignedReads-8bit', 12 | unit: 'bytes', suffix: '/s', 13 | func() { 14 | let bitstream = new BitstreamReader(); 15 | let data = Buffer.alloc(500 * 1024 * 1024, 123); 16 | bitstream.addBuffer(data); 17 | 18 | let read = 0; 19 | let max = data.length; 20 | let start = Date.now(); 21 | 22 | for (; read < max; ++read) 23 | bitstream.readSync(8); 24 | let time = Date.now() - start; 25 | return read / (time / 1000); 26 | } 27 | }, 28 | { 29 | name: 'byteAlignedReads-16bit', 30 | unit: 'bytes', suffix: '/s', 31 | func() { 32 | let bitstream = new BitstreamReader(); 33 | let data = Buffer.alloc(25 * 1024 * 1024, 123); 34 | bitstream.addBuffer(data); 35 | 36 | let read = 0; 37 | let max = data.length / 2; 38 | let start = Date.now(); 39 | 40 | for (; read < max; ++read) { 41 | bitstream.readSync(16); 42 | } 43 | 44 | let time = Date.now() - start; 45 | return read / (time / 1000); 46 | } 47 | }, 48 | { 49 | name: 'byteAlignedReads-24bit', 50 | unit: 'bytes', suffix: '/s', 51 | func() { 52 | let bitstream = new BitstreamReader(); 53 | let data = Buffer.alloc(25 * 1024 * 1024, 123); 54 | bitstream.addBuffer(data); 55 | 56 | let read = 0; 57 | let max = data.length / 3 | 0; 58 | let start = Date.now(); 59 | 60 | for (; read < max; ++read) { 61 | bitstream.readSync(24); 62 | } 63 | 64 | let time = Date.now() - start; 65 | return read / (time / 1000); 66 | } 67 | }, 68 | { 69 | name: 'byteAlignedReads-32bit', 70 | unit: 'bytes', suffix: '/s', 71 | func() { 72 | let bitstream = new BitstreamReader(); 73 | let data = Buffer.alloc(200 * 1024 * 1024, 123); 74 | bitstream.addBuffer(data); 75 | 76 | let read = 0; 77 | let max = data.length / 4; 78 | let start = Date.now(); 79 | 80 | for (; read < max; ++read) { 81 | bitstream.readSync(32); 82 | } 83 | 84 | let time = Date.now() - start; 85 | return read / (time / 1000); 86 | } 87 | }, 88 | { 89 | name: 'byteOffsetReads-8bit', 90 | unit: 'bytes', suffix: '/s', 91 | func() { 92 | let bitstream = new BitstreamReader(); 93 | let data = Buffer.alloc(25 * 1024 * 1024 + 2, 123); 94 | bitstream.addBuffer(data); 95 | bitstream.read(4); 96 | 97 | let read = 0; 98 | let max = data.length - 1; 99 | let start = Date.now(); 100 | 101 | for (; read < max; ++read) 102 | bitstream.readSync(8); 103 | let time = Date.now() - start; 104 | return read / (time / 1000); 105 | } 106 | }, 107 | { 108 | name: 'halfByteReads', 109 | unit: 'bytes', suffix: '/s', 110 | func() { 111 | let bitstream = new BitstreamReader(); 112 | let data = Buffer.alloc(25 * 1024 * 1024, 123); 113 | bitstream.addBuffer(data); 114 | 115 | let read = 0; 116 | let max = data.length * 2; 117 | let start = Date.now(); 118 | 119 | for (; read < max; ++read) 120 | bitstream.readSync(4); 121 | let time = Date.now() - start; 122 | return read / (time / 1000); 123 | } 124 | }, 125 | { 126 | name: 'readBytesAligned-32', 127 | unit: 'bytes', suffix: '/s', 128 | func() { 129 | let bitstream = new BitstreamReader(); 130 | let data = Buffer.alloc(600 * 1024 * 1024, 123); 131 | bitstream.addBuffer(data); 132 | 133 | let read = 0; 134 | let max = data.length / 32; 135 | let buf = Buffer.alloc(32); 136 | let start = Date.now(); 137 | 138 | for (; read < max; ++read) 139 | bitstream.readBytesSync(buf); 140 | let time = Date.now() - start; 141 | return read * 32 / (time / 1000); 142 | } 143 | } 144 | ]); 145 | 146 | async function asyncTest() { 147 | let generator = new Generator(); 148 | let bitstream = new BitstreamReader(); 149 | let counter = new FPSCounter('hits'); 150 | 151 | counter.start(5*1000); 152 | generator.on('data', data => { 153 | bitstream.addBuffer(data); 154 | console.log(`backlog: ${bitstream.available} bits now enqueued`); 155 | }); 156 | 157 | setInterval(() => { 158 | console.log(`backlog: ${bitstream.available} bits not yet read`); 159 | }, 5*1000); 160 | 161 | while (true) { 162 | let byte = await bitstream.read(8); 163 | counter.hit(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/benchmark-utils.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Readable } from "stream"; 3 | 4 | export async function runTest(test: Test): Promise { 5 | let iterations = test.iterations ?? 5; 6 | let results: number[] = []; 7 | 8 | console.log(`### ${test.name} ###`); 9 | for (let i = 0; i < iterations; ++i) { 10 | let value = await test.func(); 11 | results.push(value); 12 | console.log(` Iteration #${i + 1}: ${formatData(value, test.unit)}${test.suffix ?? ''}`); 13 | } 14 | 15 | let average = results.reduce((s, v) => s + v, 0) / iterations; 16 | return { 17 | test, 18 | results, 19 | average 20 | }; 21 | } 22 | 23 | export interface Test { 24 | name: string, 25 | only?: boolean; 26 | func: () => number; 27 | unit: 'time' | 'bytes' | 'count'; 28 | suffix?: string; 29 | iterations?: number; 30 | } 31 | 32 | export interface TestResult { 33 | test: Test; 34 | results: number[]; 35 | average: number; 36 | } 37 | 38 | export async function runTests(tests: Test[]): Promise { 39 | let results: TestResult[] = []; 40 | let only = tests.some(x => x.only); 41 | 42 | for (let test of tests) { 43 | if (!only || test.only) 44 | results.push(await runTest(test)); 45 | } 46 | 47 | console.log(); 48 | console.log(`### RESULTS ###`); 49 | for (let result of results) { 50 | console.log(`${result.test.name}: ${formatData(result.average, result.test.unit)}${result.test.suffix ?? ''}`); 51 | } 52 | 53 | return results; 54 | } 55 | 56 | export function formatBytes(bytes: number) { 57 | if (bytes > 1024 * 1024 * 1024) { 58 | return `${Math.floor(bytes / 1024 / 1024 / 1024 * 100) / 100} GiB`; 59 | } else if (bytes > 1024 * 1024) { 60 | return `${Math.floor(bytes / 1024 / 1024 * 100) / 100} MiB`; 61 | } else if (bytes > 1024) { 62 | return `${Math.floor(bytes / 1024 * 100) / 100} KiB`; 63 | } else { 64 | return `${Math.floor(bytes * 100) / 100} B`; 65 | } 66 | } 67 | 68 | export function formatTime(time: number) { 69 | if (time > 1000*60) { 70 | return `${Math.floor(time / 1000 / 60 * 100) / 100}m`; 71 | } else if (time > 1000) { 72 | return `${Math.floor(time / 1000 * 100) / 100}s`; 73 | } else { 74 | return `${Math.floor(time * 100) / 100}ms`; 75 | } 76 | } 77 | 78 | export function formatCount(count: number) { 79 | if (count > 1000 * 1000 * 1000) { 80 | return `${Math.floor(count / 1000 / 1000 / 1000 * 100) / 100}G`; 81 | } else if (count > 1000 * 1000) { 82 | return `${Math.floor(count / 1000 / 1000 * 100) / 100}M`; 83 | } else if (count > 1000) { 84 | return `${Math.floor(count / 1000 * 100) / 100}K`; 85 | } else { 86 | return `${Math.floor(count * 100) / 100}`; 87 | } 88 | } 89 | 90 | export function formatData(value: number, units: 'time' | 'bytes' | 'count') { 91 | if (units === 'time') 92 | return formatTime(value); 93 | else if (units === 'bytes') 94 | return formatBytes(value); 95 | else 96 | return formatCount(value); 97 | } 98 | 99 | export class FPSCounter { 100 | constructor( 101 | public label : string, 102 | ) { 103 | } 104 | 105 | reportingSecond : number = 0; 106 | fps : number = 0; 107 | private interval; 108 | private reportingFps : number = 0; 109 | private _reportFrequency : number = 10*1000; 110 | private _minf2f : number = Infinity; 111 | private _maxf2f : number = 0; 112 | private _avgf2f : number = 0; 113 | private _minft : number = Infinity; 114 | private _maxft : number = 0; 115 | private _avgft : number = 0; 116 | 117 | get reportFrequency() { 118 | return this._reportFrequency; 119 | } 120 | 121 | start(freq : number = 10*1000) { 122 | this._reportFrequency = freq; 123 | this.interval = setInterval(() => this.report(), this._reportFrequency); 124 | return this; 125 | } 126 | 127 | report() { 128 | let dec = (number : number, decimals : number = 2) => { 129 | let factor = Math.pow(10, decimals); 130 | return Math.floor(number * factor) / factor; 131 | }; 132 | 133 | console.log( 134 | `${this.label}: ${this.fps}/s ` 135 | + `| time: ${dec(this._avgft)}ms [${dec(this._minft)} - ${dec(this._maxft)}], ` 136 | + `| f2f: ${dec(this._avgf2f)}ms [${dec(this._minf2f)} - ${dec(this._maxf2f)}]` 137 | ); 138 | this._minft = Infinity; 139 | this._maxft = 0; 140 | this._minf2f = Infinity; 141 | this._maxf2f = 0; 142 | } 143 | 144 | stop() { 145 | clearInterval(this.interval); 146 | } 147 | 148 | private _presentationTime; 149 | 150 | present() { 151 | this._presentationTime = Date.now(); 152 | } 153 | 154 | private _hitTime; 155 | 156 | hit() { 157 | if (this._presentationTime) { 158 | let ft = Date.now() - this._presentationTime; 159 | 160 | this._minft = Math.min(ft, this._minft); 161 | this._maxft = Math.max(ft, this._maxft); 162 | this._avgft = 0.99*this._avgft + 0.01*ft; 163 | } 164 | 165 | if (this._hitTime) { 166 | let f2f = Date.now() - this._hitTime 167 | this._minf2f = Math.min(f2f, this._minf2f); 168 | this._maxf2f = Math.max(f2f, this._maxf2f); 169 | this._avgf2f = 0.99*this._avgf2f + 0.01*f2f; 170 | } 171 | 172 | this._hitTime = Date.now(); 173 | 174 | this.reportingFps += 1; 175 | if (Date.now() > (this.reportingSecond + 1) * 1000) { 176 | let now = Math.floor(Date.now() / 1000); 177 | this.fps = this.reportingFps; 178 | this.reportingSecond = now; 179 | this.reportingFps = 0; 180 | //console.log(`[!] ${this.label}: ${this.fps}/s`); 181 | } 182 | } 183 | } 184 | 185 | export class Generator extends Readable { 186 | constructor(size = 10_000_000, interval = 10) { 187 | super(); 188 | setInterval(() => this.push(Buffer.alloc(size, 123)), interval); 189 | } 190 | 191 | _read(size : number) { 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/elements/field.ts: -------------------------------------------------------------------------------- 1 | import { ArraySerializer } from "./array-serializer"; 2 | import { BooleanSerializer } from "./boolean-serializer"; 3 | import { BufferSerializer } from "./buffer-serializer"; 4 | import { InferredPropertyDecorator, PropType } from "./decorators"; 5 | import { BitstreamElement } from "./element"; 6 | import { FieldDefinition } from "./field-definition"; 7 | import { FieldOptions } from "./field-options"; 8 | import { LengthDeterminant } from "./length-determinant"; 9 | import { NullSerializer } from "./null-serializer"; 10 | import { NumberSerializer } from "./number-serializer"; 11 | import { StringSerializer } from "./string-serializer"; 12 | import { StructureSerializer } from "./structure-serializer"; 13 | 14 | /** 15 | * Mark a property of a BitstreamElement subclass as a field that should be read from the bitstream. 16 | * @param length The length of the field, in bits (except when the field has type Buffer or String, in which case it is in bytes) 17 | * @param options 18 | */ 19 | export function Field(); 20 | 21 | export function Field(options : FieldOptions>): InferredPropertyDecorator; 22 | export function Field(length : LengthDeterminant): InferredPropertyDecorator; 23 | export function Field( 24 | length : LengthDeterminant, options : FieldOptions> 25 | ): InferredPropertyDecorator; 26 | 27 | export function Field( 28 | length : LengthDeterminant, options : FieldOptions 29 | ): PropertyDecorator; 30 | 31 | export function Field(...args: any[]): PropertyDecorator { 32 | let length: LengthDeterminant = 0; 33 | let options: FieldOptions = undefined; 34 | 35 | if (['number', 'function'].includes(typeof args[0])) 36 | length = args.shift(); 37 | 38 | if (typeof args[0] === 'object') 39 | options = args.shift(); 40 | 41 | if (!options) 42 | options = {}; 43 | 44 | return (target : T, fieldName : string | symbol) => { 45 | let containingType = target.constructor as typeof BitstreamElement; 46 | 47 | let field : FieldDefinition = { 48 | name: fieldName, 49 | containingType, 50 | type: Reflect.getMetadata('design:type', target, fieldName), 51 | length, 52 | options 53 | } 54 | 55 | let fieldDesc = `${containingType.name}#${String(field.name)}`; 56 | let BufferT = typeof Buffer !== 'undefined' ? Buffer : undefined; 57 | if ((field.type === BufferT || field.type === Uint8Array) && typeof field.length === 'number' && field.length % 8 !== 0) 58 | throw new Error(`${fieldDesc}: Length (${field.length}) must be a multiple of 8 when field type is Buffer`); 59 | 60 | if (field.type === Array) { 61 | if (!field.options.array?.type) 62 | throw new Error(`${fieldDesc}: Array field must specify option array.type`); 63 | if (!(field.options.array?.type.prototype instanceof BitstreamElement) && field.options.array?.type !== Number) 64 | throw new Error(`${fieldDesc}: Array fields can only be used with types which inherit from BitstreamElement`); 65 | if (field.options.array?.countFieldLength) { 66 | if (typeof field.options.array.countFieldLength !== 'number' || field.options.array.countFieldLength <= 0) 67 | throw new Error(`${fieldDesc}: Invalid value provided for length of count field: ${field.options.array.countFieldLength}. Must be a positive number.`); 68 | } 69 | 70 | if (field.options.array?.count) { 71 | if (typeof field.options.array.count !== 'number' && typeof field.options.array.count !== 'function') 72 | throw new Error(`${fieldDesc}: Invalid value provided for count determinant: ${field.options.array.count}. Must be a number or function`); 73 | } 74 | } 75 | 76 | if (field.options.readAhead) { 77 | if (field.options.readAhead === undefined) 78 | throw new Error(`${fieldDesc}: To use the readAhead option, you must specify readAhead.length`); 79 | if (!['number', 'function'].includes(typeof field.options.readAhead.length)) 80 | throw new Error(`${fieldDesc}: Invalid read-ahead length specified (must be a number or discriminant function)`); 81 | } 82 | 83 | if (field.type === Number) { 84 | if (typeof field.length === 'number' && field.length > 53 && field.options.number?.format !== 'float' && !field.options.number?.allowOversized) { 85 | throw new Error( 86 | `${fieldDesc}: It is not safe to use the 'number' type for fields larger than 53 bits. ` 87 | + `Consider using 'bigint' instead. ` 88 | + `If you are sure this is what you want, set option number.allowOversized.` 89 | ); 90 | } 91 | } 92 | 93 | if (!options.serializer) { 94 | if (field.type === Array) 95 | options.serializer = new ArraySerializer(); 96 | else if (field.type?.prototype instanceof BitstreamElement) 97 | options.serializer = new StructureSerializer(); 98 | else if (field.length === 0) 99 | options.serializer = new NullSerializer(); 100 | else if (field.type === Object) 101 | options.serializer = new NumberSerializer(); 102 | else if (field.type === Number) 103 | options.serializer = new NumberSerializer(); 104 | else if (field.type === Boolean) 105 | options.serializer = new BooleanSerializer(); 106 | else if (typeof Buffer !== 'undefined' && field.type === Buffer) 107 | options.serializer = new BufferSerializer(); 108 | else if (field.type === Uint8Array) 109 | options.serializer = new BufferSerializer(); 110 | else if (field.type === String) 111 | options.serializer = new StringSerializer(); 112 | else 113 | throw new Error(`${containingType.name}#${String(field.name)}: No serializer available for type ${field.type?.name || ''}`); 114 | } 115 | 116 | ([]>containingType.ownSyntax).push(field); 117 | } 118 | } -------------------------------------------------------------------------------- /src/elements/field-options.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, StringEncodingOptions } from "../bitstream"; 2 | import { ArrayOptions } from "./array-options"; 3 | import { BufferOptions } from "./buffer-options"; 4 | import { Serializer } from "./serializer"; 5 | import { ValueDeterminant } from "./value-determinant"; 6 | import { VariantDefinition } from "./variant-definition"; 7 | import { NumberOptions } from "./number-options"; 8 | import { BooleanOptions } from "./boolean-options"; 9 | import { LengthDeterminant } from "./length-determinant"; 10 | import { BitstreamElement } from "./element"; 11 | 12 | export type ReadAheadDiscriminant = (buffer: BitstreamReader, element : T) => boolean; 13 | 14 | export interface ReadAheadOptions { 15 | /** 16 | * How many bits should be read before processing this field. 17 | */ 18 | length: LengthDeterminant; 19 | 20 | /** 21 | * When specified, if the given discriminant returns true, the field is parsed. Otherwise it is skipped. 22 | * 23 | * Called after the required number of bits have been read ahead (see `length`), as long as the bitstream has 24 | * not ended. 25 | * 26 | * The bitstream reader is placed in simulation mode before calling the discriminant, so it is fine to use 27 | * the read*Sync() functions to inspect the peeked data. The state of the stream's read head will be restored to 28 | * where it was after the function completes. 29 | * 30 | * In the case where the stream ended before the requisite number of bits became available, this discriminant is 31 | * still called. It is expected that the discriminant will take care in this situation. 32 | */ 33 | presentWhen?: ReadAheadDiscriminant; 34 | 35 | /** 36 | * When specified, if the given discriminant returns true, the field is skipped. Otherwise it is parsed. 37 | * 38 | * Called after the required number of bits have been read ahead (see `length`), as long as the bitstream has 39 | * not ended. 40 | * 41 | * The bitstream reader is placed in simulation mode before calling the discriminant, so it is fine to use 42 | * the read*Sync() functions to inspect the peeked data. The state of the stream's read head will be restored to 43 | * where it was after the function completes. 44 | * 45 | * In the case where the stream ended before the requisite number of bits became available, this discriminant is 46 | * still called. It is expected that the discriminant will take care in this situation. 47 | */ 48 | excludedWhen?: ReadAheadDiscriminant; 49 | } 50 | 51 | /** 52 | * A function which returns true when the given element matches a certain condition 53 | */ 54 | export type PresenceDiscriminant = (element : T) => boolean; 55 | 56 | /** 57 | * Defines options available for properties marked with `@Field()` within BitstreamElement classes. 58 | */ 59 | export interface FieldOptions { 60 | /** 61 | * Specify a custom serializer for this field. If not specified, this option will be 62 | * filled based on the runtime type metadata available for the given field. For instance, 63 | * if the field is of type Number, it will get a NumberSerializer, if the type is a subclass 64 | * of BitstreamElement, it will get StructureSerializer, etc. 65 | */ 66 | serializer? : Serializer; 67 | 68 | /** 69 | * Specify options specific to string fields, such as text encoding and null termination. 70 | */ 71 | string? : StringEncodingOptions; 72 | 73 | /** 74 | * Specify options specific to number fields 75 | */ 76 | number? : NumberOptions; 77 | 78 | /** 79 | * Specify options specific to boolean fields 80 | */ 81 | boolean? : BooleanOptions; 82 | 83 | /** 84 | * Specify options specific to array fields, such as how the length of the array should be 85 | * determined and what element type the array is (because Typescript does not expose the type of 86 | * an array field) 87 | */ 88 | array? : ArrayOptions; 89 | 90 | /** 91 | * Specify options specific to Buffer fields 92 | */ 93 | buffer? : BufferOptions; 94 | 95 | /** 96 | * Define a group name that this field falls within. This is not used by default and is only useful 97 | * when implementing custom reading and writing code. 98 | */ 99 | group? : string; 100 | 101 | /** 102 | * Allows for reading a certain number of bits from the bitstream ahead of attempting to read this field. 103 | * This can be used to make parsing decisions on upcoming data. 104 | */ 105 | readAhead?: ReadAheadOptions; 106 | 107 | /** 108 | * Define a function that indicates when the field is present within the bitstream. This is the opposite 109 | * of `excludedWhen`. 110 | */ 111 | presentWhen? : PresenceDiscriminant; 112 | 113 | /** 114 | * Define a function that indicates when the field is absent within the bitstream. This is the opposite 115 | * of `presentWhen`. 116 | */ 117 | excludedWhen? : PresenceDiscriminant; 118 | 119 | /** 120 | * Specify a set of subclasses which should be considered when variating this field. When not specified, 121 | * all subclasses marked with `@Variant` are considered, this option lets you narrow the options in specific 122 | * cases 123 | */ 124 | variants? : (Function | VariantDefinition)[]; 125 | 126 | /** 127 | * Specify a set of subfields (by field name) that should be skipped when serializing this field. 128 | * Only relevant for subelements. For other field types, this is ignored. 129 | */ 130 | skip?: (string | symbol)[]; 131 | 132 | /** 133 | * When true, the field represents the "variant marker", which is the 134 | * location in a superclass where a variant subclass's fields are expected. 135 | * Not meant to be used directly, instead use `@VariantMarker()` 136 | */ 137 | isVariantMarker? : boolean; 138 | 139 | /** 140 | * When true, the value read for this field is thrown away, and not 141 | * applied to the serialized instance. This is used by `@Reserved` to represent 142 | * reserved bits 143 | */ 144 | isIgnored? : boolean; 145 | 146 | /** 147 | * When provided, the value written for this field will be the one listed here, 148 | * or the result of running the determinant function. Useful for ensuring that 149 | * fields with values dependent on other parts of the structure are always written 150 | * correctly without having to manually update them before writing. It is also used 151 | * by the `@Reserved()` decorator to ensure that high bits are always written. 152 | */ 153 | writtenValue?: ValueDeterminant; 154 | 155 | /** 156 | * Initializer to call when constructing new instances for this field. 157 | * The element instance that contains this field will be passed as the second parameter. 158 | * This is called after constructing the new instance but before any of its fields are 159 | * parsed. 160 | */ 161 | initializer?: (instance: any, parentElement: any) => void; 162 | } 163 | -------------------------------------------------------------------------------- /src/elements/array-serializer.ts: -------------------------------------------------------------------------------- 1 | import { BitstreamReader, BitstreamWriter } from "../bitstream"; 2 | import { BitstreamElement } from "./element"; 3 | import { resolveLength } from "./resolve-length"; 4 | import { Serializer } from "./serializer"; 5 | import { StructureSerializer } from "./structure-serializer"; 6 | import { FieldDefinition } from "./field-definition"; 7 | import { IncompleteReadResult } from "../common"; 8 | import { summarizeField } from "./utils"; 9 | 10 | /** 11 | * Serializes arrays to/from bitstreams 12 | */ 13 | export class ArraySerializer implements Serializer { 14 | *read(reader: BitstreamReader, type : any, parent : BitstreamElement, field: FieldDefinition): Generator { 15 | let count = 0; 16 | let elements = []; 17 | 18 | if (field?.options?.array?.countFieldLength) { 19 | if (!reader.isAvailable(field.options.array.countFieldLength)) 20 | yield { remaining: field.options.array.countFieldLength, contextHint: () => summarizeField(field) }; 21 | count = reader.readSync(field.options.array.countFieldLength); 22 | } else if (field?.options?.array?.count) { 23 | count = resolveLength(field.options.array.count, parent, field); 24 | } else if (field?.length) { 25 | count = resolveLength(field?.length, parent, field); 26 | } 27 | 28 | if (parent) { 29 | parent.readFields.push(field.name); 30 | parent[field.name] = []; 31 | } 32 | 33 | if (field?.options?.array?.type === Number) { 34 | // Array of numbers. Useful when the array holds a single number field, but the 35 | // bit length of the element fields is not 8 (where you would probably use a single `Buffer` field instead). 36 | // For instance, in somes IETF RFCs 10 bit words are used instead of 8 bit words (ie bytes). 37 | let elementLength = field.options.array.elementLength; 38 | let format = field?.options?.number?.format ?? 'unsigned'; 39 | let readNumber = () => { 40 | if (format === 'signed') 41 | elements.push(reader.readSignedSync(elementLength)); 42 | else if (format === 'float') 43 | elements.push(reader.readFloatSync(elementLength)); 44 | else if (format === 'unsigned') 45 | elements.push(reader.readSync(elementLength)); 46 | else 47 | throw new Error(`Unsupported number format '${format}'`); 48 | }; 49 | 50 | if (field?.options?.array?.hasMore) { 51 | do { 52 | let continued : boolean; 53 | 54 | try { 55 | parent.runWithFieldBeingComputed(field, () => 56 | continued = field.options.array.hasMore(elements, parent, parent.parent), true); 57 | } catch (e) { 58 | throw new Error(`${parent?.constructor.name || ''}#${String(field?.name || '')} Failed to determine if array has more items via 'hasMore' discriminant: ${e.message}`); 59 | } 60 | 61 | if (!continued) 62 | break; 63 | 64 | 65 | if (!reader.isAvailable(elementLength)) 66 | yield { remaining: elementLength, contextHint: () => summarizeField(field) }; 67 | 68 | readNumber(); 69 | } while (true); 70 | } else { 71 | for (let i = 0; i < count; ++i) { 72 | if (!reader.isAvailable(elementLength)) 73 | yield { remaining: elementLength, contextHint: () => summarizeField(field) }; 74 | 75 | readNumber(); 76 | } 77 | } 78 | } else { 79 | if (field.options.array.hasMore) { 80 | let i = 0; 81 | do { 82 | let continued : boolean; 83 | 84 | try { 85 | parent.runWithFieldBeingComputed(field, () => 86 | continued = field.options.array.hasMore(elements, parent, parent.parent), true); 87 | } catch (e) { 88 | throw new Error(`${parent?.constructor.name || ''}#${String(field?.name || '')} Failed to determine if array has more items via 'hasMore' discriminant: ${e.message}`); 89 | } 90 | 91 | if (!continued) 92 | break; 93 | 94 | let element : BitstreamElement; 95 | let serializer = new StructureSerializer(); 96 | let gen = serializer.read(reader, field.options.array.type, parent, field); 97 | 98 | while (true) { 99 | let result = gen.next(); 100 | if (result.done === false) { 101 | yield result.value; 102 | } else { 103 | element = result.value; 104 | break; 105 | } 106 | } 107 | 108 | elements.push(element); 109 | parent[field.name].push(element); 110 | 111 | } while (true); 112 | } else { 113 | for (let i = 0; i < count; ++i) { 114 | let element : BitstreamElement; 115 | let serializer = new StructureSerializer(); 116 | let g = serializer.read(reader, field.options.array.type, parent, field); 117 | 118 | while (true) { 119 | let result = g.next(); 120 | if (result.done === false) { 121 | yield result.value; 122 | } else { 123 | element = result.value; 124 | break; 125 | } 126 | } 127 | 128 | elements.push(element); 129 | parent[field.name].push(element); 130 | } 131 | } 132 | } 133 | 134 | return elements; 135 | } 136 | 137 | write(writer : BitstreamWriter, type : any, parent : BitstreamElement, field : FieldDefinition, value : any[]) { 138 | if (!value) { 139 | throw new Error(`${parent?.constructor.name || ''}#${String(field?.name) || ''}: Cannot serialize a null array!`); 140 | } 141 | 142 | let length = value.length; 143 | 144 | if (field.options.array.countFieldLength) { 145 | let countFieldLength = field.options.array.countFieldLength; 146 | 147 | if (length >= Math.pow(2, countFieldLength)) { 148 | length = Math.pow(2, countFieldLength) - 1; 149 | } 150 | 151 | writer.write(field.options.array.countFieldLength, value.length); 152 | } else if (field.options.array.count) { 153 | try { 154 | length = resolveLength(field.options.array.count, parent, field); 155 | } catch (e) { 156 | throw new Error(`Failed to resolve length for array via 'count': ${e.message}`); 157 | } 158 | 159 | if (length > value.length) { 160 | throw new Error( 161 | `${field.containingType.name}#${String(field.name)}: ` 162 | + `Array field's count determinant specified ${length} elements should be written ` 163 | + `but array only contains ${value.length} elements. ` 164 | + `Ensure that the value of the count determinant is compatible with the number of elements in ` 165 | + `the provided array.` 166 | ); 167 | } 168 | } 169 | 170 | for (let i = 0; i < length; ++i) { 171 | if (field?.options?.array?.type === Number) { 172 | let type = field.options?.number?.format ?? 'unsigned'; 173 | 174 | if (type === 'unsigned') 175 | writer.write(field.options.array.elementLength, value[i]); 176 | else if (type === 'signed') 177 | writer.writeSigned(field.options.array.elementLength, value[i]); 178 | else if (type === 'float') 179 | writer.writeFloat(field.options.array.elementLength, value[i]); 180 | 181 | } else { 182 | (value[i] as BitstreamElement).write(writer, { context: parent?.context }); 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ⏩ vNext 2 | 3 | # v4.2.2 4 | - Fix an issue where the new 53-bit oversize error is emitted when the `float` format is selected, even though it is 5 | valid and safe to use size `64` for a float. 6 | 7 | # v4.2.1 8 | - Add inference based type safety in more places. 9 | - It is no longer necessary to specify a length determinant if the length is inferred (such as when declaring 10 | a field of type BitstreamElement) 11 | 12 | # v4.2.0 13 | 14 | - Type safety has been dramatically increased thanks to new inference available for decorators in Typescript 5. 15 | Note that this library does not use the new ES decorator standard as it relies on Typescript's 16 | `--emitDecoratorMetadata` functionality which has no equivalent yet when using standardized decorators. 17 | If you are using Typescript 4 you may experience issues around discriminators in fields and variants as the 18 | types of element values gets upgraded to `BitstreamElement` from `any`, but not all the way to the specific 19 | subclass you are applying the decorator to as it does in Typescript 5. 20 | - The `BitstreamRequest` interface, which is used internally to track pending reads, is no longer exported. This 21 | interface previously had no use for consumers of the library and was exported accidentally, so this is not 22 | considered a breaking change. 23 | - `BitstreamReader` now supports the concept of ending the stream using `end()`. When a reader ends, a blocking read is 24 | rejected. 25 | - `BitstreamReader.assure()` now rejects if the stream ends before the requested number of bits is satisfied. Previously, 26 | since there was no way to end a stream, it would remain unresolved forever, until a buffer satisfied it. Pass 27 | `true` as the `optional` parameter to have the returned promise resolve even if not all bits could be read. You can 28 | pair this with reading `available` directly to check if all requested bits have been read. 29 | - `BitstreamReader.addBuffer()` throws if called on an ended reader 30 | - `BitstreamReader.reset()` has been added to reset a reader to a clean state, including causing the reader's ended 31 | flag to get reset. 32 | - `BitstreamReader.simulate()` and `BitstreamReader.simulateSync()` have been added to provide a convenient way to 33 | run a function and reset the read head of the reader back to where it was before the function was called. This is 34 | equivalent to enabling the `retainBuffers` setting, noting the offset before executing the function, and setting 35 | the offset back to where it was before the simulation. 36 | - Added read ahead capability to `BitstreamElement`. See the `readAhead` field options for more information. 37 | - Made discriminant types more specific. Previously a single `Discriminant` type was used for both variant discriminants 38 | and field presence discriminants, but the `parent` parameter was never passed to field presence discriminants. Now 39 | these are represented with their own distinct types (`VariantDiscriminant` and `FieldPresenceDiscriminant` 40 | respectively). 41 | 42 | # v4.1.3 43 | - Performance: Substantial optimizations around byte-aligned reads, partial byte-aligned reads, byte array reads, and 44 | other scenarios. In some scenarios, performance can be 10x faster, and in the case of reading byte-aligned byte arrays, 45 | the improvement is somewhere in the realm of 500x faster. This is a common scenario in data-heavy parsers such as 46 | audio/video formats. 47 | 48 | # v4.1.2 49 | - Performance: `serialize()` now uses a 1KB buffer by default, previously the default buffer size was 1 byte. This yields a 50 | massive performance increase when writing large objects using `serialize()` 51 | - Performance: `serialize()` will now recycle its `BitstreamWriter` objects in order to eliminate object allocations, 52 | providing an additional performance increase. 53 | 54 | # v4.1.1 55 | - Fix: Calling flush early resulted in writing entire buffer size instead 56 | - Added fast path for BitstreamWriter#writeBytes() when the stream is byte-aligned 57 | 58 | # v4.1.0 59 | - Add support for initializers 60 | 61 | # v4.0.1 62 | - Guard when BufferSerializer is trying to write a null buffer 63 | 64 | # v4.0.0 65 | - Changed generators to return IncompleteReadResult to enable better context information during buffer exhaustion 66 | 67 | # v3.1.1 68 | 69 | - Fix: Crash during `serialize()` on element with no fields 70 | - Fix: backwards-compat: Keep providing `SerializeOptions` for compatibility 71 | 72 | # v3.1.0 73 | 74 | - Add `allowExhaustion` option to `deserialize()` 75 | 76 | # v3.0.4 77 | 78 | - Add `BitstreamReader#readBytes*()` and `BitstreamWriter#writeBytes()` for reading/writing a number 79 | of bytes into a buffer or typed array. 80 | - Deprecated `BitstreamWriter#writeBuffer()` in favor of `writeBytes()` 81 | 82 | # v3.0.3 83 | 84 | - Add `WritableStream#offset` and `WritableStream#byteOffset` 85 | - Eliminate use of `BitstreamElement#measure()` within `BitstreamElement#serialize()`. This produces more predictable 86 | behavior for lifecycle hooks, is more performant and ensures only one `write()` call is needed per `serialize()` 87 | call 88 | 89 | # v3.0.2 90 | 91 | - Context is now shared between owner of array and array elements during write operations 92 | 93 | # v3.0.1 94 | 95 | - The current reader offset is now reported when the buffer becomes exhausted during deserialization 96 | - Context is now set up correctly during write operations 97 | 98 | # v3.0.0 99 | 100 | Features: 101 | - Ability to read/write IEEE 754 floating point and signed (two's complement) integers 102 | - Added support for lifecycle operations on elements 103 | - Elements and all nested sub-elements now have a shared "context" object for elements during (de)serialization 104 | - Documentation improvements 105 | 106 | Breaking changes: 107 | - Removed the `BitstreamReader.unread()` method which has been deprecated since `v1.0.1` released March 28, 2021. 108 | - The `BitstreamElement.read()` family of operations now accepts an options bag instead of positional parameters 109 | - An exception will now be thrown when trying to serialize (+/-) `Infinity` to an integer number field (either signed 110 | or unsigned) 111 | - `BitstreamElement.deserialize()` no longer returns a Promise. The operation has not been asynchronous since the 112 | generator rewrite in 2.0, so promises are not required here and unnecessarily complicate efficient deserialization. 113 | - In previous versions the length parameter of `@Field()` was ignored by `ArraySerializer` even though it was documented 114 | as a valid way to specify the item count of the array. This version makes `ArraySerializer` respect this value when 115 | `options.array.count` is not specified. This matches the design intention, but since the default count was zero, this 116 | is technically a breaking change since, for example, `@Field(3, { array: { elementSize: 8 }}) items : number[]` 117 | previously would read zero numbers and now it will read three. 118 | - hasMore() now accepts the `array` being built so that analyzing the values read prior can influence the result. The 119 | `instance` and `parent` parameters are still present and follow the `array`. 120 | 121 | # v2.1.1 122 | - Fix: `@ReservedLow` should actually emit low bits 123 | 124 | # v2.1.0 125 | - Add `@ReservedLow` decorator for cases where bits must be low instead of high (as in `@Reserved`) 126 | 127 | # v2.0.4 128 | - Fix: Do not crash when `Buffer` is unavailable 129 | 130 | # v2.0.3 131 | - Improved documentation, particularly performance guidance 132 | - Eliminated dependency on `streambuffers` 133 | - `Uint8Array` can now be used instead of `Buffer` to allow the library to be used on the web 134 | 135 | # v2.0.2 136 | - Fixed a bug where BitstreamElement.deserialize() would return a promise resolved with a generator instead of 137 | the final element instance 138 | 139 | # v2.0.1 140 | - Much improved documentation 141 | 142 | # v2.0.0 143 | 144 | - Use generators instead of promises to implement blocking reads. This allows us to defer the strategy for handling 145 | bitstream underruns to the top level including support for continuation (as generators are really just coroutines). 146 | The core `BitstreamElement.read()` method now returns a generator. 147 | Several new reading strategies built upon generators are provided, including `readSync()` (try to complete the 148 | operation, and fail if there are not enough bits available), `tryRead()` (try to complete the operation, and return 149 | undefined if not enough bits are available, undoing all pending reads from the bitstream), `readBlocking()` (read the 150 | object and wait for bits to become available as necessary by awaiting `BitstreamReader.assure()`) 151 | 152 | # v1.1.0 153 | 154 | - Adds support for retaining buffers in BitstreamReader as well as "seeking" to a previous buffer if it has been retained. 155 | This can be useful for implementing a "try read" pattern which resets the read head back to the correct position if there 156 | are not enough bits available to complete the operation. See `BitstreamReader#retainBuffers` and `BitstreamReader#offset` 157 | for details -------------------------------------------------------------------------------- /src/bitstream/writer.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "../common"; 2 | 3 | /** 4 | * A class for writing numbers of varying bitlengths to a Node.js Writable. 5 | * All data is written in big-endian (network) byte order. 6 | */ 7 | export class BitstreamWriter { 8 | /** 9 | * Create a new writer 10 | * @param stream The writable stream to write to 11 | * @param bufferSize The number of bytes to buffer before flushing onto the writable 12 | */ 13 | constructor(public stream : Writable, readonly bufferSize = 1) { 14 | this.buffer = new Uint8Array(bufferSize); 15 | } 16 | 17 | private pendingByte : bigint = BigInt(0); 18 | private pendingBits : number = 0; 19 | private buffer : Uint8Array; 20 | private bufferedBytes = 0; 21 | private _offset = 0; 22 | 23 | /** 24 | * How many bits have been written via this writer in total 25 | */ 26 | get offset() { 27 | return this._offset; 28 | } 29 | 30 | /** 31 | * How many bits into the current byte is the write cursor. 32 | * If this value is zero, then we are currently byte-aligned. 33 | * A value of 7 means we are 1 bit away from the byte boundary. 34 | */ 35 | get byteOffset() { 36 | return this.pendingBits; 37 | } 38 | 39 | /** 40 | * Finish the current byte (assuming zeros for the remaining bits, if necessary) 41 | * and flushes the output. 42 | */ 43 | end() { 44 | this.finishByte(); 45 | this.flush(); 46 | } 47 | 48 | /** 49 | * Reset the bit offset of this writer back to zero. 50 | */ 51 | reset() { 52 | this._offset = 0; 53 | } 54 | 55 | private finishByte() { 56 | if (this.pendingBits > 0) { 57 | this.buffer[this.bufferedBytes++] = Number(this.pendingByte); 58 | this.pendingBits = 0; 59 | this.pendingByte = BigInt(0); 60 | } 61 | } 62 | 63 | flush() { 64 | if (this.bufferedBytes > 0) { 65 | this.stream.write(Buffer.from(this.buffer.slice(0, this.bufferedBytes))); 66 | this.bufferedBytes = 0; 67 | } 68 | } 69 | 70 | private textEncoder = new TextEncoder(); 71 | 72 | /** 73 | * Decode a string into a set of bytes and write it to the bitstream, bounding the string 74 | * by the given number of bytes, optionally using the given encoding (or UTF-8 if not specified). 75 | * @param byteCount The number of bytes to bound the output to 76 | * @param value The string to decode and write 77 | * @param encoding The encoding to use when writing the string. Defaults to utf-8 78 | */ 79 | writeString(byteCount : number, value : string, encoding : string = 'utf-8') { 80 | if (encoding === 'utf-8') { 81 | let buffer = new Uint8Array(byteCount); 82 | let strBuf = this.textEncoder.encode(value); 83 | buffer.set(strBuf, 0); 84 | this.writeBytes(buffer); 85 | } else { 86 | if (typeof Buffer === 'undefined') { 87 | throw new Error(`Encoding '${encoding}' is not supported: No Node.js Buffer implementation found, web standard TextEncoder only supports utf-8`); 88 | } 89 | 90 | let buffer = Buffer.alloc(byteCount); 91 | Buffer.from(value, encoding).copy(buffer); 92 | this.writeBuffer(buffer); 93 | } 94 | } 95 | 96 | /** 97 | * Write the given buffer to the bitstream. This is done by treating each byte as an 8-bit write. 98 | * Note that the bitstream does not need to be byte-aligned to call this method, meaning you can write 99 | * a set of bytes at a non=zero bit offset if you wish. 100 | * @param buffer The buffer to write 101 | * @deprecated Use writeBytes() instead 102 | */ 103 | writeBuffer(buffer : Uint8Array) { 104 | this.writeBytes(buffer); 105 | } 106 | 107 | /** 108 | * Write the given buffer to the bitstream. This is done by treating each byte as an 8-bit write. 109 | * Note that the bitstream does not need to be byte-aligned to call this method, meaning you can write 110 | * a set of bytes at a non=zero bit offset if you wish. 111 | * @param chunk The buffer to write 112 | */ 113 | writeBytes(chunk : Uint8Array, offset = 0, length? : number) { 114 | length ??= chunk.length - offset; 115 | 116 | // Fast path: Byte-aligned 117 | if (this.byteOffset === 0) { 118 | while (chunk.length > 0) { 119 | let writableLength = Math.min(chunk.length, this.buffer.length - this.bufferedBytes); 120 | this.buffer.set(chunk.subarray(0, writableLength), this.bufferedBytes); 121 | this.bufferedBytes += writableLength; 122 | chunk = chunk.subarray(writableLength); 123 | 124 | if (this.bufferedBytes >= this.buffer.length) 125 | this.flush(); 126 | } 127 | 128 | return; 129 | } 130 | 131 | for (let i = offset, max = Math.min(chunk.length, offset+length); i < max; ++i) 132 | this.write(8, chunk[i]); 133 | } 134 | 135 | private min(a : bigint, b : bigint) { 136 | if (a < b) 137 | return a; 138 | else 139 | return b; 140 | } 141 | 142 | /** 143 | * Write the given number to the bitstream with the given bitlength. If the number is too large for the 144 | * number of bits specified, the lower-order bits are written and the higher-order bits are ignored. 145 | * @param length The number of bits to write 146 | * @param value The number to write 147 | */ 148 | write(length : number, value : number) { 149 | if (value === void 0 || value === null) 150 | value = 0; 151 | 152 | value = Number(value); 153 | 154 | if (Number.isNaN(value)) 155 | throw new Error(`Cannot write to bitstream: Value ${value} is not a number`); 156 | if (!Number.isFinite(value)) 157 | throw new Error(`Cannot write to bitstream: Value ${value} must be finite`); 158 | 159 | let valueN = BigInt(value % Math.pow(2, length)); 160 | 161 | let remainingLength = length; 162 | 163 | while (remainingLength > 0) { 164 | let shift = BigInt(8 - this.pendingBits - remainingLength); 165 | let contribution = (shift >= 0 ? (valueN << shift) : (valueN >> -shift)); 166 | let writtenLength = Number(shift >= 0 ? remainingLength : this.min(-shift, BigInt(8 - this.pendingBits))); 167 | 168 | this.pendingByte = this.pendingByte | contribution; 169 | this.pendingBits += writtenLength; 170 | this._offset += writtenLength; 171 | 172 | remainingLength -= writtenLength; 173 | valueN = valueN % BigInt(Math.pow(2, remainingLength)); 174 | 175 | if (this.pendingBits === 8) { 176 | this.finishByte(); 177 | 178 | if (this.bufferedBytes >= this.buffer.length) { 179 | this.flush(); 180 | } 181 | } 182 | } 183 | } 184 | 185 | writeSigned(length : number, value : number) { 186 | if (value === undefined || value === null) 187 | value = 0; 188 | 189 | const originalValue = value; 190 | const max = 2**(length - 1) - 1; // ie 127 191 | const min = -(2**(length - 1)); // ie -128 192 | 193 | value = Number(value); 194 | 195 | if (Number.isNaN(value)) 196 | throw new Error(`Cannot write to bitstream: Value ${originalValue} is not a number`); 197 | if (!Number.isFinite(value)) 198 | throw new Error(`Cannot write to bitstream: Value ${value} must be finite`); 199 | if (value > max) 200 | throw new TypeError(`Cannot represent ${value} in I${length} format: Value too large (min=${min}, max=${max})`); 201 | if (value < min) 202 | throw new TypeError(`Cannot represent ${value} in I${length} format: Negative value too small (min=${min}, max=${max})`); 203 | 204 | return this.write(length, value >= 0 ? value : (~(-value) + 1) >>> 0); 205 | } 206 | 207 | writeFloat(length : number, value : number) { 208 | if (length !== 32 && length !== 64) 209 | throw new TypeError(`Invalid length (${length} bits) Only 4-byte (32 bit / single-precision) and 8-byte (64 bit / double-precision) IEEE 754 values are supported`); 210 | 211 | let buf = new ArrayBuffer(length / 8); 212 | let view = new DataView(buf); 213 | 214 | if (length === 32) 215 | view.setFloat32(0, value); 216 | else if (length === 64) 217 | view.setFloat64(0, value); 218 | 219 | for (let i = 0, max = buf.byteLength; i < max; ++i) 220 | this.write(8, view.getUint8(i)); 221 | } 222 | } 223 | 224 | /** 225 | * A specialized BitstreamWriter which does not write to a stream, but instead measures the number of 226 | * bits written by the caller. This is used to implement measurement in BitstreamElement 227 | */ 228 | export class BitstreamMeasurer extends BitstreamWriter { 229 | constructor() { 230 | super(null, 1); 231 | } 232 | 233 | bitLength = 0; 234 | 235 | writeString(byteCount : number, value : string, encoding : string = 'utf-8') { 236 | this.bitLength += byteCount * 8; 237 | } 238 | 239 | writeBuffer(buffer : Uint8Array) { 240 | this.bitLength += buffer.length * 8; 241 | } 242 | 243 | write(length : number, value : number) { 244 | this.bitLength += length; 245 | } 246 | } 247 | 248 | -------------------------------------------------------------------------------- /src/bitstream/writer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "razmin"; 2 | import { expect } from "chai"; 3 | import { BitstreamWriter } from "./writer"; 4 | 5 | describe('BitstreamWriter', it => { 6 | it('works for bit writes', () => { 7 | let bufs : Buffer[] = []; 8 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 9 | let writer = new BitstreamWriter(fakeStream); 10 | writer.write(1, 0b1); 11 | writer.write(1, 0b0); 12 | writer.write(1, 0b0); 13 | writer.write(1, 0b1); 14 | writer.write(1, 0b1); 15 | writer.write(1, 0b0); 16 | writer.write(1, 0b0); 17 | writer.write(1, 0b1); 18 | writer.write(1, 0b0); 19 | writer.write(1, 0b1); 20 | writer.write(1, 0b1); 21 | writer.write(1, 0b0); 22 | writer.write(1, 0b0); 23 | writer.write(1, 0b1); 24 | writer.write(1, 0b1); 25 | writer.write(1, 0b0); 26 | expect(bufs.length).to.equal(2); 27 | expect(bufs[0][0]).to.equal(0b10011001); 28 | expect(bufs[1][0]).to.equal(0b01100110); 29 | }); 30 | it('works for short writes', () => { 31 | let bufs : Buffer[] = []; 32 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 33 | let writer = new BitstreamWriter(fakeStream); 34 | writer.write(3, 0b010); 35 | writer.write(3, 0b101); 36 | writer.write(2, 0b11); 37 | expect(bufs.length).to.equal(1); 38 | expect(bufs[0][0]).to.equal(0b01010111); 39 | }); 40 | it('works for full-byte writes', () => { 41 | let bufs : Buffer[] = []; 42 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 43 | let writer = new BitstreamWriter(fakeStream); 44 | writer.write(8, 0b01010111); 45 | expect(bufs.length).to.equal(1); 46 | expect(bufs[0][0]).to.equal(0b01010111); 47 | }); 48 | it('works for offset full-byte writes', () => { 49 | let bufs : Buffer[] = []; 50 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 51 | let writer = new BitstreamWriter(fakeStream); 52 | writer.write(4, 0b1111); 53 | writer.write(8, 0b01010111); 54 | writer.write(4, 0b1111); 55 | expect(bufs.length).to.equal(2); 56 | expect(bufs[0][0]).to.equal(0b11110101); 57 | expect(bufs[1][0]).to.equal(0b01111111); 58 | }); 59 | it('works for large writes (1)', () => { 60 | let bufs : Buffer[] = []; 61 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 62 | let writer = new BitstreamWriter(fakeStream); 63 | writer.write(16, 0b1111111100000000); 64 | expect(bufs.length).to.equal(2); 65 | expect(bufs[0][0]).to.equal(0b11111111); 66 | expect(bufs[1][0]).to.equal(0b00000000); 67 | }); 68 | it('works for large writes (2)', () => { 69 | let bufs : Buffer[] = []; 70 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 71 | let writer = new BitstreamWriter(fakeStream); 72 | writer.write(16, 0b0101010110101010); 73 | expect(bufs.length).to.equal(2); 74 | expect(bufs[0][0]).to.equal(0b01010101); 75 | expect(bufs[1][0]).to.equal(0b10101010); 76 | }); 77 | it('works for offset large writes', () => { 78 | let bufs : Buffer[] = []; 79 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 80 | let writer = new BitstreamWriter(fakeStream); 81 | writer.write(4, 0b1111); 82 | writer.write(16, 0b0101010110101010); 83 | writer.write(4, 0b1111); 84 | 85 | expect(bufs.length).to.equal(3); 86 | expect(bufs[0][0]).to.equal(0b11110101); 87 | expect(bufs[1][0]).to.equal(0b01011010); 88 | expect(bufs[2][0]).to.equal(0b10101111); 89 | }); 90 | it('respects configured buffer size', () => { 91 | let bufs : Buffer[] = []; 92 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 93 | let writer = new BitstreamWriter(fakeStream, 2); 94 | writer.write(8, 0b11111100); 95 | expect(bufs.length).to.equal(0); 96 | writer.write(8, 0b11111101); 97 | expect(bufs.length).to.equal(1); 98 | writer.write(8, 0b11111110); 99 | expect(bufs.length).to.equal(1); 100 | writer.write(8, 0b11111111); 101 | expect(bufs.length).to.equal(2); 102 | 103 | expect(bufs[0].length).to.equal(2); 104 | expect(bufs[1].length).to.equal(2); 105 | expect(bufs[0][0]).to.equal(0b11111100); 106 | expect(bufs[0][1]).to.equal(0b11111101); 107 | expect(bufs[1][0]).to.equal(0b11111110); 108 | expect(bufs[1][1]).to.equal(0b11111111); 109 | }); 110 | it('throws when writing NaN as an unsigned integer', () => { 111 | let bufs : Buffer[] = []; 112 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 113 | let writer = new BitstreamWriter(fakeStream, 2); 114 | 115 | let caught; 116 | 117 | try { 118 | writer.write(8, NaN); 119 | } catch (e) { caught = e; } 120 | 121 | expect(caught, `Expected write(8, NaN) to throw an exception`).to.exist; 122 | }); 123 | it('throws when writing Infinity as an unsigned integer', () => { 124 | let bufs : Buffer[] = []; 125 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 126 | let writer = new BitstreamWriter(fakeStream, 2); 127 | 128 | let caught; 129 | try { 130 | writer.write(8, Infinity); 131 | } catch (e) { caught = e; } 132 | 133 | expect(caught, `Expected write(8, Infinity) to throw an exception`).to.exist; 134 | }); 135 | it('throws when writing NaN as a signed integer', () => { 136 | let bufs : Buffer[] = []; 137 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 138 | let writer = new BitstreamWriter(fakeStream, 2); 139 | 140 | let caught; 141 | try { 142 | writer.writeSigned(8, NaN); 143 | } catch (e) { caught = e; } 144 | 145 | expect(caught, `Expected write(8, NaN) to throw an exception`).to.exist; 146 | }); 147 | it('throws when writing values outside of range', () => { 148 | let bufs : Buffer[] = []; 149 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 150 | let writer = new BitstreamWriter(fakeStream, 2); 151 | 152 | writer.writeSigned(8, 0); 153 | writer.writeSigned(8, 127); 154 | writer.writeSigned(8, -128); 155 | 156 | let caught; 157 | try { 158 | writer.writeSigned(8, 200); 159 | } catch (e) { caught = e; } 160 | 161 | expect(caught, `Expected writeSigned(8, 200) to throw an exception`).to.exist; 162 | caught = undefined; 163 | 164 | try { 165 | writer.writeSigned(8, 128); 166 | } catch (e) { caught = e; } 167 | 168 | expect(caught, `Expected writeSigned(8, 128) to throw an exception`).to.exist; 169 | caught = undefined; 170 | 171 | try { 172 | writer.writeSigned(8, -129); 173 | } catch (e) { caught = e; } 174 | 175 | expect(caught, `Expected writeSigned(8, -129) to throw an exception`).to.exist; 176 | caught = undefined; 177 | 178 | try { 179 | writer.writeSigned(16, 999999); 180 | } catch (e) { caught = e; } 181 | 182 | expect(caught, `Expected writeSigned(8, 200) to throw an exception`).to.exist; 183 | caught = undefined; 184 | 185 | try { 186 | writer.writeSigned(16, -999999); 187 | } catch (e) { caught = e; } 188 | 189 | expect(caught, `Expected writeSigned(8, 128) to throw an exception`).to.exist; 190 | caught = undefined; 191 | }); 192 | it('throws when writing Infinity as a signed integer', () => { 193 | let bufs : Buffer[] = []; 194 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 195 | let writer = new BitstreamWriter(fakeStream, 2); 196 | 197 | let caught; 198 | try { 199 | writer.writeSigned(8, Infinity); 200 | } catch (e) { caught = e; } 201 | 202 | expect(caught, `Expected write(8, Infinity) to throw an exception`).to.exist; 203 | }); 204 | it('writes undefined as zero when unsigned', () => { 205 | let bufs : Buffer[] = []; 206 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 207 | let writer = new BitstreamWriter(fakeStream, 1); 208 | 209 | writer.write(8, undefined); 210 | expect(bufs[0][0]).to.equal(0); 211 | }); 212 | it('writes null as zero when unsigned', () => { 213 | let bufs : Buffer[] = []; 214 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 215 | let writer = new BitstreamWriter(fakeStream, 1); 216 | 217 | writer.write(8, null); 218 | expect(bufs[0][0]).to.equal(0); 219 | }); 220 | it('writes undefined as zero when signed', () => { 221 | let bufs : Buffer[] = []; 222 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 223 | let writer = new BitstreamWriter(fakeStream, 1); 224 | 225 | writer.writeSigned(8, undefined); 226 | expect(bufs[0][0]).to.equal(0); 227 | }); 228 | it('writes null as zero when signed', () => { 229 | let bufs : Buffer[] = []; 230 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 231 | let writer = new BitstreamWriter(fakeStream, 1); 232 | 233 | writer.writeSigned(8, null); 234 | expect(bufs[0][0]).to.equal(0); 235 | }); 236 | it('correctly handles signed integers', () => { 237 | let bufs : Buffer[] = []; 238 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 239 | let writer = new BitstreamWriter(fakeStream); 240 | 241 | writer.writeSigned(8, -5); expect(bufs[0][0]).to.equal(0xFB); 242 | writer.writeSigned(8, 5); expect(bufs[1][0]).to.equal(5); 243 | writer.writeSigned(8, 0); expect(bufs[2][0]).to.equal(0); 244 | 245 | bufs = []; 246 | writer = new BitstreamWriter(fakeStream, 2); 247 | 248 | writer.writeSigned(16, -1014); expect(Array.from(bufs[0])).to.eql([0xFC, 0x0A]); 249 | writer.writeSigned(16, 1014); expect(Array.from(bufs[1])).to.eql([0x03, 0xF6]); 250 | writer.writeSigned(16, 0); expect(Array.from(bufs[2])).to.eql([0, 0]); 251 | 252 | bufs = []; 253 | writer = new BitstreamWriter(fakeStream, 4); 254 | 255 | writer.writeSigned(32, -102336); expect(Array.from(bufs[0])).to.eql([0xFF, 0xFE, 0x70, 0x40]); 256 | writer.writeSigned(32, 102336); expect(Array.from(bufs[1])).to.eql([0x00, 0x01, 0x8F, 0xC0]); 257 | writer.writeSigned(32, 0); expect(Array.from(bufs[2])).to.eql([0, 0, 0, 0]); 258 | 259 | }); 260 | it('correctly handles floats', () => { 261 | let bufs : Buffer[] = []; 262 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 263 | let writer = new BitstreamWriter(fakeStream, 4); 264 | 265 | writer.writeFloat(32, 102.5); expect(Array.from(bufs[0])).to.eql([0x42, 0xCD, 0x00, 0x00]); 266 | writer.writeFloat(32, -436); expect(Array.from(bufs[1])).to.eql([0xC3, 0xDA, 0x00, 0x00]); 267 | writer.writeFloat(32, 0); expect(Array.from(bufs[2])).to.eql([0,0,0,0]); 268 | 269 | bufs = []; 270 | writer = new BitstreamWriter(fakeStream, 8); 271 | 272 | writer.writeFloat(64, 8745291.56); 273 | expect(Array.from(bufs[0])).to.eql([0x41, 0x60, 0xae, 0x29, 0x71, 0xeb, 0x85, 0x1f]); 274 | 275 | writer.writeFloat(64, -327721.17); 276 | expect(Array.from(bufs[1])).to.eql([0xc1, 0x14, 0x00, 0xa4, 0xae, 0x14, 0x7a, 0xe1]); 277 | 278 | writer.writeFloat(64, 0); 279 | expect(Array.from(bufs[2])).to.eql([0, 0, 0, 0, 0, 0, 0, 0]); 280 | }); 281 | 282 | it('.writeFloat() throws for lengths other than 32 and 64', () => { 283 | let bufs : Buffer[] = []; 284 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 285 | let writer = new BitstreamWriter(fakeStream, 4); 286 | 287 | let caught; 288 | try { 289 | writer.writeFloat(13, 123); 290 | } catch (e) { caught = e; } 291 | 292 | expect(caught).to.exist; 293 | }); 294 | 295 | it('correctly handles NaN', () => { 296 | let bufs : Buffer[] = []; 297 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 298 | let writer = new BitstreamWriter(fakeStream, 4); 299 | 300 | writer.writeFloat(32, NaN); expect(Array.from(bufs[0])).to.eql([0x7F, 0xC0, 0x00, 0x00]); 301 | 302 | bufs = []; 303 | writer = new BitstreamWriter(fakeStream, 8); 304 | 305 | writer.writeFloat(64, NaN); 306 | expect(Array.from(bufs[0])).to.eql([ 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]); 307 | }); 308 | 309 | it('correctly handles Infinity', () => { 310 | let bufs : Buffer[] = []; 311 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 312 | let writer = new BitstreamWriter(fakeStream, 4); 313 | 314 | writer.writeFloat(32, Infinity); expect(Array.from(bufs[0])).to.eql([ 0x7f, 0x80, 0x00, 0x00 ]); 315 | 316 | bufs = []; 317 | writer = new BitstreamWriter(fakeStream, 8); 318 | 319 | writer.writeFloat(64, Infinity); 320 | expect(Array.from(bufs[0])).to.eql([ 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]); 321 | }); 322 | it('.end() flushes full bytes', () => { 323 | let bufs : Buffer[] = []; 324 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 325 | let writer = new BitstreamWriter(fakeStream, 4); 326 | 327 | writer.write(8, 44); 328 | expect(bufs.length).to.equal(0); 329 | writer.end(); 330 | expect(bufs.length).to.equal(1); 331 | expect(bufs[0].length).to.equal(1); 332 | expect(bufs[0][0]).to.equal(44); 333 | }); 334 | it('.end() flushes partial bytes', () => { 335 | let bufs : Buffer[] = []; 336 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 337 | let writer = new BitstreamWriter(fakeStream, 4); 338 | 339 | writer.write(4, 0b1111); 340 | expect(bufs.length).to.equal(0); 341 | writer.end(); 342 | expect(bufs.length).to.equal(1); 343 | expect(bufs[0].length).to.equal(1); 344 | expect(bufs[0][0]).to.equal(0b11110000); 345 | }); 346 | it('.writeString() writes utf-8 strings correctly', () => { 347 | let bufs : Buffer[] = []; 348 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 349 | let writer = new BitstreamWriter(fakeStream, 5); 350 | 351 | writer.writeString(5, 'hello', 'utf-8'); 352 | expect(bufs.length).to.equal(1); 353 | 354 | let buf = Buffer.from(bufs[0]); 355 | 356 | expect(buf.length).to.equal(5); 357 | expect(buf.toString('utf-8')).to.equal('hello'); 358 | }); 359 | it('.writeString() writes utf16le strings correctly', () => { 360 | let bufs : Buffer[] = []; 361 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 362 | let writer = new BitstreamWriter(fakeStream, 10); 363 | 364 | writer.writeString(10, 'hello', 'utf16le'); 365 | 366 | let buf = Buffer.from(bufs[0]); 367 | 368 | expect(buf.toString('utf16le')).to.equal('hello'); 369 | }); 370 | it('.writeString() throws when any encoding other than utf-8 is used and Buffer is not available', () => { 371 | const BufferT = Buffer; 372 | (globalThis as any).Buffer = undefined; 373 | 374 | try { 375 | let bufs : Buffer[] = []; 376 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 377 | let writer = new BitstreamWriter(fakeStream, 10); 378 | 379 | let caught; 380 | 381 | try { 382 | writer.writeString(10, 'hello', 'utf16le'); 383 | } catch (e) { caught = e; } 384 | 385 | expect(caught).to.exist; 386 | } finally { 387 | (globalThis as any).Buffer = BufferT; 388 | } 389 | }); 390 | it('.writeBuffer() works correctly', () => { 391 | let bufs : Buffer[] = []; 392 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 393 | let writer = new BitstreamWriter(fakeStream, 2); 394 | 395 | let buf = Buffer.from([ 12, 34, 56, 78 ]); 396 | writer.writeBuffer(buf); 397 | 398 | expect(bufs.length).to.equal(2); 399 | expect(bufs[0][0]).to.equal(12); 400 | expect(bufs[0][1]).to.equal(34); 401 | expect(bufs[1][0]).to.equal(56); 402 | expect(bufs[1][1]).to.equal(78); 403 | }); 404 | it('.writeBuffer() works even when not byte-aligned', () => { 405 | let bufs : Buffer[] = []; 406 | let fakeStream : any = { write(buf) { bufs.push(buf); } } 407 | let writer = new BitstreamWriter(fakeStream, 5); 408 | 409 | let buf = Buffer.from([ 12, 34, 56, 78 ]); 410 | writer.write(4, 0); 411 | writer.writeBuffer(buf); 412 | writer.write(4, 0); 413 | 414 | expect(bufs.length).to.equal(1); 415 | expect(bufs[0].length).to.equal(5); 416 | expect(bufs[0][0]).to.equal(0); 417 | expect(bufs[0][1]).to.equal(194); 418 | expect(bufs[0][2]).to.equal(35); 419 | expect(bufs[0][3]).to.equal(132); 420 | expect(bufs[0][4]).to.equal(224); 421 | }); 422 | }); -------------------------------------------------------------------------------- /src/bitstream/reader.ts: -------------------------------------------------------------------------------- 1 | import { StringEncodingOptions } from "./string-encoding-options"; 2 | 3 | let maskMap: Map; 4 | 5 | /** 6 | * Represents a request to read a number of bits 7 | */ 8 | interface BitstreamRequest { 9 | resolve : (buffer : number) => void; 10 | reject: (error: Error) => void; 11 | promise: Promise; 12 | length : number; 13 | signed? : boolean; 14 | float? : boolean; 15 | assure? : boolean; 16 | } 17 | 18 | /** 19 | * A class which lets you read through one or more Buffers bit-by-bit. All data is read in big-endian (network) byte 20 | * order 21 | */ 22 | export class BitstreamReader { 23 | private buffers : Uint8Array[] = []; 24 | private bufferedLength : number = 0; 25 | private blockedRequest : BitstreamRequest = null; 26 | private _offsetIntoBuffer = 0; 27 | private _bufferIndex = 0; 28 | private _offset = 0; 29 | private _spentBufferSize = 0; 30 | 31 | /** 32 | * Get the index of the buffer currently being read. This will always be zero unless retainBuffers=true 33 | */ 34 | get bufferIndex() { 35 | return this._bufferIndex; 36 | } 37 | 38 | /** 39 | * Get the current offset in bits, starting from the very first bit read by this reader (across all 40 | * buffers added) 41 | */ 42 | get offset() { 43 | return this._offset; 44 | } 45 | 46 | /** 47 | * The total number of bits which were in buffers that have previously been read, and have since been discarded. 48 | */ 49 | get spentBufferSize() { 50 | return this._spentBufferSize; 51 | } 52 | 53 | /** 54 | * Set the current offset in bits, as measured from the very first bit read by this reader (across all buffers 55 | * added). If the given offset points into a previously discarded buffer, an error will be thrown. See the 56 | * retainBuffers option if you need to seek back into previous buffers. If the desired offset is in a previous 57 | * buffer which has not been discarded, the current read head is moved into the appropriate offset of that buffer. 58 | */ 59 | set offset(value) { 60 | if (value < this._spentBufferSize) { 61 | throw new Error( 62 | `Offset ${value} points into a discarded buffer! ` 63 | + `If you need to seek backwards outside the current buffer, make sure to set retainBuffers=true` 64 | ); 65 | } 66 | 67 | let offsetIntoBuffer = value - this._spentBufferSize; 68 | let bufferIndex = 0; 69 | 70 | for (let i = 0, max = this.buffers.length; i < max; ++i) { 71 | let buf = this.buffers[i]; 72 | let size = buf.length * 8; 73 | if (offsetIntoBuffer < size) { 74 | this._bufferIndex = bufferIndex; 75 | this._offset = value; 76 | this._offsetIntoBuffer = offsetIntoBuffer; 77 | this.bufferedLength = buf.length * 8 - this._offsetIntoBuffer; 78 | for (let j = i + 1; j < max; ++j) 79 | this.bufferedLength += this.buffers[j].length * 8; 80 | 81 | return; 82 | } 83 | 84 | offsetIntoBuffer -= size; 85 | ++bufferIndex; 86 | } 87 | } 88 | 89 | /** 90 | * Run a function which can synchronously read bits without affecting the read head after the function 91 | * has finished. 92 | * @param func 93 | */ 94 | simulateSync(func: () => T) { 95 | let oldRetainBuffers = this.retainBuffers; 96 | let originalOffset = this.offset; 97 | this.retainBuffers = true; 98 | try { 99 | return func(); 100 | } finally { 101 | this.retainBuffers = oldRetainBuffers; 102 | this.offset = originalOffset; 103 | } 104 | } 105 | 106 | /** 107 | * Run a function which can asynchronously read bits without affecting the read head after the function 108 | * has finished. 109 | * @param func 110 | */ 111 | async simulate(func: () => Promise) { 112 | let oldRetainBuffers = this.retainBuffers; 113 | let originalOffset = this.offset; 114 | this.retainBuffers = true; 115 | try { 116 | return await func(); 117 | } finally { 118 | this.retainBuffers = oldRetainBuffers; 119 | this.offset = originalOffset; 120 | } 121 | } 122 | 123 | /** 124 | * When true, buffers are not removed, which allows the user to 125 | * "rewind" the current offset back into buffers that have already been 126 | * visited. If you enable this, you will need to remove buffers manually using 127 | * clean() 128 | */ 129 | retainBuffers : boolean = false; 130 | 131 | /** 132 | * Remove any fully used up buffers. Only has an effect if retainBuffers is true. 133 | * Optional `count` parameter lets you control how many buffers can be freed. 134 | */ 135 | clean(count?: number) { 136 | /** 137 | * PERFORMANCE SENSITIVE 138 | * Previous versions of this function caused as much as an 18% overhead on top of simple 139 | * byte-aligned reads. 140 | */ 141 | let spent = count !== void 0 ? Math.min(count, this._bufferIndex) : this._bufferIndex; 142 | for (let i = 0, max = spent; i < max; ++i) { 143 | this._spentBufferSize += this.buffers[0].length * 8; 144 | this.buffers.shift(); 145 | } 146 | 147 | this._bufferIndex -= spent; 148 | } 149 | 150 | /** 151 | * The number of bits that are currently available. 152 | */ 153 | get available() { 154 | return this.bufferedLength - this.skippedLength; 155 | } 156 | 157 | /** 158 | * Check if the given number of bits are currently available. 159 | * @param length The number of bits to check for 160 | * @returns True if the required number of bits is available, false otherwise 161 | */ 162 | isAvailable(length : number) { 163 | return this.bufferedLength >= length; 164 | } 165 | 166 | private ensureNoReadPending() { 167 | if (this.blockedRequest) 168 | throw new Error(`Only one read() can be outstanding at a time.`); 169 | } 170 | 171 | private textDecoder = new TextDecoder(); 172 | 173 | /** 174 | * Asynchronously read the given number of bytes, encode it into a string, and return the result, 175 | * optionally using a specific text encoding. 176 | * @param length The number of bytes to read 177 | * @param options A set of options to control conversion into a string. @see StringEncodingOptions 178 | * @returns The resulting string 179 | */ 180 | async readString(length : number, options? : StringEncodingOptions): Promise { 181 | this.ensureNoReadPending(); 182 | await this.assure(8*length); 183 | return this.readStringSync(length, options); 184 | } 185 | 186 | /** 187 | * Synchronously read the given number of bytes, encode it into a string, and return the result, 188 | * optionally using a specific text encoding. 189 | * @param length The number of bytes to read 190 | * @param options A set of options to control conversion into a string. @see StringEncodingOptions 191 | * @returns The resulting string 192 | */ 193 | readStringSync(length : number, options? : StringEncodingOptions): string { 194 | if (!options) 195 | options = {}; 196 | 197 | this.ensureNoReadPending(); 198 | 199 | let buffer = new Uint8Array(length); 200 | let firstTerminator = -1; 201 | let charLength = 1; 202 | let encoding = options.encoding ?? 'utf-8'; 203 | 204 | if (['utf16le', 'ucs-2', 'ucs2'].includes(encoding)) { 205 | charLength = 2; 206 | } 207 | 208 | for (let i = 0, max = length; i < max; ++i) { 209 | buffer[i] = this.readSync(8); 210 | } 211 | 212 | for (let i = 0, max = length; i < max; i += charLength) { 213 | let char = buffer[i]; 214 | if (charLength === 2) 215 | char = (char << 8) | (buffer[i+1] ?? 0); 216 | 217 | if (char === 0) { 218 | firstTerminator = i; 219 | break; 220 | } 221 | } 222 | 223 | if (options.nullTerminated !== false) { 224 | if (firstTerminator >= 0) { 225 | buffer = buffer.subarray(0, firstTerminator); 226 | } 227 | } 228 | 229 | if (encoding === 'utf-8') { 230 | return this.textDecoder.decode(buffer); 231 | } else { 232 | if (typeof Buffer === 'undefined') 233 | throw new Error(`Encoding '${encoding}' is not supported: No Node.js Buffer implementation and TextDecoder only supports utf-8`); 234 | return Buffer.from(buffer).toString(encoding); 235 | } 236 | } 237 | 238 | /** 239 | * Read a number of the given bitlength synchronously without advancing 240 | * the read head. 241 | * @param length The number of bits to read 242 | * @returns The number read from the bitstream 243 | */ 244 | peekSync(length : number) { 245 | return this.readCoreSync(length, false); 246 | } 247 | 248 | private skippedLength = 0; 249 | 250 | /** 251 | * Skip the given number of bits. 252 | * @param length The number of bits to skip 253 | */ 254 | skip(length : number) { 255 | this.skippedLength += length; 256 | } 257 | 258 | /** 259 | * Read an unsigned integer of the given bit length synchronously. If there are not enough 260 | * bits available, an error is thrown. 261 | * 262 | * @param length The number of bits to read 263 | * @returns The unsigned integer that was read 264 | */ 265 | readSync(length : number): number { 266 | return this.readCoreSync(length, true); 267 | } 268 | 269 | /** 270 | * Read a number of bytes from the stream. Returns a generator that ends when the read is complete, 271 | * and yields a number of *bytes* still to be read (not bits like in other read methods) 272 | * 273 | * @param buffer The buffer/typed array to write to 274 | * @param offset The offset into the buffer to write to. Defaults to zero 275 | * @param length The length of bytes to read. Defaults to the length of the array (sans the offset) 276 | */ 277 | *readBytes(buffer : Uint8Array, offset : number = 0, length? : number): Generator { 278 | length ??= buffer.length - offset; 279 | 280 | let bitOffset = this._offsetIntoBuffer % 8; 281 | 282 | // If this is a byte-aligned read, we can do this more optimally than using readSync 283 | 284 | if (bitOffset === 0) { 285 | if (globalThis.BITSTREAM_TRACE) { 286 | console.log(`------------------------------------------------------------ Byte-aligned readBytes(), length=${length}`); 287 | console.log(`------------------------------------------------------------ readBytes(): Pre-operation: buffered=${this.bufferedLength} bits, bufferIndex=${this._bufferIndex}, bufferOffset=${this._offsetIntoBuffer}, bufferLength=${this.buffers[this._bufferIndex]?.length || ''} bufferCount=${this.buffers.length}`); 288 | } 289 | let remainingLength = length; 290 | let destBufferOffset = 0; 291 | while (remainingLength > 0) { 292 | if (this.available < remainingLength * 8) 293 | yield Math.max((remainingLength * 8 - this.available) / 8); 294 | 295 | let bufferOffset = Math.floor(this._offsetIntoBuffer / 8); 296 | let contributionBuffer = this.buffers[this._bufferIndex]; 297 | let contribution = Math.min(remainingLength, contributionBuffer.length); 298 | 299 | for (let i = 0; i < contribution; ++i) 300 | buffer[destBufferOffset + i] = contributionBuffer[bufferOffset + i]; 301 | 302 | destBufferOffset += contribution; 303 | 304 | let contributionBits = contribution * 8; 305 | this.consume(contributionBits); 306 | remainingLength -= contributionBits; 307 | 308 | if (globalThis.BITSTREAM_TRACE) { 309 | console.log(`------------------------------------------------------------ readBytes(): consumed=${contribution} bytes, remaining=${remainingLength}`); 310 | console.log(`------------------------------------------------------------ readBytes(): buffered=${this.bufferedLength} bits, bufferIndex=${this._bufferIndex}, bufferOffset=${this._offsetIntoBuffer}, bufferCount=${this.buffers.length}`); 311 | } 312 | } 313 | } else { 314 | 315 | // Non-byte-aligned, we need to construct bytes using bit-wise operations. 316 | // readSync is perfect for this 317 | 318 | for (let i = offset, max = Math.min(buffer.length, offset+length); i < max; ++i) { 319 | if (!this.isAvailable(8)) 320 | yield max - i; 321 | 322 | buffer[i] = this.readSync(8); 323 | } 324 | } 325 | 326 | return buffer; 327 | } 328 | 329 | /** 330 | * Read a number of bytes from the stream synchronously. If not enough bytes are available, an 331 | * exception is thrown. 332 | * 333 | * @param buffer The buffer/typed array to write to 334 | * @param offset The offset into the buffer to write to. Defaults to zero 335 | * @param length The length of bytes to read. Defaults to the length of the array (sans the offset) 336 | */ 337 | readBytesSync(buffer : Uint8Array, offset : number = 0, length? : number): Uint8Array { 338 | length ??= buffer.length - offset; 339 | let gen = this.readBytes(buffer, offset, length); 340 | 341 | while (true) { 342 | let result = gen.next(); 343 | if (result.done === false) 344 | throw new Error(`underrun: Not enough bits are available (requested ${length} bytes)`); 345 | else 346 | break; 347 | } 348 | 349 | return buffer; 350 | } 351 | 352 | /** 353 | * Read a number of bytes from the stream. Blocks and waits for more bytes if not enough bytes are available. 354 | * 355 | * @param buffer The buffer/typed array to write to 356 | * @param offset The offset into the buffer to write to. Defaults to zero 357 | * @param length The length of bytes to read. Defaults to the length of the array (sans the offset) 358 | */ 359 | async readBytesBlocking(buffer : Uint8Array, offset : number = 0, length? : number) { 360 | length ??= buffer.length - offset; 361 | let gen = this.readBytes(buffer, offset, length); 362 | 363 | while (true) { 364 | let result = gen.next(); 365 | if (result.done === false) 366 | await this.assure(result.value*8); 367 | else 368 | break; 369 | } 370 | 371 | return buffer; 372 | } 373 | 374 | /** 375 | * Read a two's complement signed integer of the given bit length synchronously. If there are not 376 | * enough bits available, an error is thrown. 377 | * 378 | * @param length The number of bits to read 379 | * @returns The signed integer that was read 380 | */ 381 | readSignedSync(length : number): number { 382 | const u = this.readSync(length); 383 | const signBit = (2**(length - 1)); 384 | const mask = signBit - 1; 385 | return (u & signBit) === 0 ? u : -((~(u - 1) & mask) >>> 0); 386 | } 387 | 388 | private maskOf(bits: number) { 389 | if (!maskMap) { 390 | maskMap = new Map(); 391 | for (let i = 0; i <= 64; ++i) { 392 | maskMap.set(i, Math.pow(0x2, i) - 1); 393 | } 394 | } 395 | 396 | return maskMap.get(bits) ?? (Math.pow(0x2, bits) - 1); 397 | } 398 | 399 | 400 | 401 | /** 402 | * Read an IEEE 754 floating point value with the given bit length (32 or 64). If there are not 403 | * enough bits available, an error is thrown. 404 | * 405 | * @param length Must be 32 for 32-bit single-precision or 64 for 64-bit double-precision. All 406 | * other values result in TypeError 407 | * @returns The floating point value that was read 408 | */ 409 | readFloatSync(length : number): number { 410 | if (length !== 32 && length !== 64) 411 | throw new TypeError(`Invalid length (${length} bits) Only 4-byte (32 bit / single-precision) and 8-byte (64 bit / double-precision) IEEE 754 values are supported`); 412 | 413 | if (!this.isAvailable(length)) 414 | throw new Error(`underrun: Not enough bits are available (requested=${length}, available=${this.bufferedLength}, buffers=${this.buffers.length})`); 415 | 416 | let buf = new ArrayBuffer(length / 8); 417 | let view = new DataView(buf); 418 | 419 | for (let i = 0, max = buf.byteLength; i < max; ++i) 420 | view.setUint8(i, this.readSync(8)); 421 | 422 | if (length === 32) 423 | return view.getFloat32(0, false); 424 | else if (length === 64) 425 | return view.getFloat64(0, false); 426 | } 427 | 428 | private readByteAligned(consume: boolean): number { 429 | let buffer = this.buffers[this._bufferIndex]; 430 | let value = buffer[this._offsetIntoBuffer / 8]; 431 | 432 | if (consume) { 433 | this.bufferedLength -= 8; 434 | this._offsetIntoBuffer += 8; 435 | this._offset += 8; 436 | if (this._offsetIntoBuffer >= buffer.length * 8) { 437 | this._bufferIndex += 1; 438 | this._offsetIntoBuffer = 0; 439 | if (!this.retainBuffers) { 440 | this.clean(); 441 | } 442 | } 443 | } 444 | 445 | return value; 446 | } 447 | 448 | private consume(length: number) { 449 | this.bufferedLength -= length; 450 | this._offsetIntoBuffer += length; 451 | this._offset += length; 452 | 453 | let buffer = this.buffers[this._bufferIndex]; 454 | while (buffer && this._offsetIntoBuffer >= (buffer.length * 8)) { 455 | this._bufferIndex += 1; 456 | this._offsetIntoBuffer -= buffer.length * 8; 457 | buffer = this.buffers[this._bufferIndex]; 458 | if (!this.retainBuffers) 459 | this.clean(); 460 | } 461 | } 462 | 463 | private readShortByteAligned(consume: boolean, byteOrder: 'lsb' | 'msb'): number { 464 | let buffer = this.buffers[this._bufferIndex]; 465 | let bufferOffset = this._offsetIntoBuffer / 8; 466 | let firstByte = buffer[bufferOffset]; 467 | let secondByte: number; 468 | 469 | if (bufferOffset + 1 >= buffer.length) 470 | secondByte = this.buffers[this._bufferIndex + 1][0]; 471 | else 472 | secondByte = buffer[bufferOffset + 1]; 473 | 474 | if (consume) 475 | this.consume(16); 476 | 477 | if (byteOrder === 'lsb') { 478 | let carry = firstByte; 479 | firstByte = secondByte; 480 | secondByte = carry; 481 | } 482 | 483 | return firstByte << 8 | secondByte; 484 | } 485 | 486 | private readLongByteAligned(consume: boolean, byteOrder: 'lsb' | 'msb'): number { 487 | let bufferIndex = this._bufferIndex; 488 | let buffer = this.buffers[bufferIndex]; 489 | let bufferOffset = this._offsetIntoBuffer / 8; 490 | 491 | let firstByte = buffer[bufferOffset++]; 492 | if (bufferOffset >= buffer.length) { 493 | buffer = this.buffers[++bufferIndex]; 494 | bufferOffset = 0; 495 | } 496 | 497 | let secondByte = buffer[bufferOffset++]; 498 | if (bufferOffset >= buffer.length) { 499 | buffer = this.buffers[++bufferIndex]; 500 | bufferOffset = 0; 501 | } 502 | 503 | let thirdByte = buffer[bufferOffset++]; 504 | if (bufferOffset >= buffer.length) { 505 | buffer = this.buffers[++bufferIndex]; 506 | bufferOffset = 0; 507 | } 508 | 509 | let fourthByte = buffer[bufferOffset++]; 510 | if (bufferOffset >= buffer.length) { 511 | buffer = this.buffers[++bufferIndex]; 512 | bufferOffset = 0; 513 | } 514 | 515 | if (consume) 516 | this.consume(32); 517 | 518 | let highBit = ((firstByte & 0x80) !== 0); 519 | firstByte &= ~0x80; 520 | 521 | if (byteOrder === 'lsb') { 522 | let b1 = fourthByte; 523 | let b2 = thirdByte; 524 | let b3 = secondByte; 525 | let b4 = firstByte; 526 | 527 | firstByte = b1; 528 | secondByte = b2; 529 | thirdByte = b3; 530 | fourthByte = b4; 531 | } 532 | 533 | let value = firstByte << 24 | secondByte << 16 | thirdByte << 8 | fourthByte; 534 | 535 | if (highBit) 536 | value += 2**31; 537 | 538 | return value; 539 | } 540 | 541 | private read3ByteAligned(consume: boolean, byteOrder: 'lsb' | 'msb'): number { 542 | let bufferIndex = this._bufferIndex; 543 | let buffer = this.buffers[bufferIndex]; 544 | let bufferOffset = this._offsetIntoBuffer / 8; 545 | 546 | let firstByte = buffer[bufferOffset++]; 547 | if (bufferOffset >= buffer.length) { 548 | buffer = this.buffers[++bufferIndex]; 549 | bufferOffset = 0; 550 | } 551 | 552 | let secondByte = buffer[bufferOffset++]; 553 | if (bufferOffset >= buffer.length) { 554 | buffer = this.buffers[++bufferIndex]; 555 | bufferOffset = 0; 556 | } 557 | 558 | let thirdByte = buffer[bufferOffset++]; 559 | if (bufferOffset >= buffer.length) { 560 | buffer = this.buffers[++bufferIndex]; 561 | bufferOffset = 0; 562 | } 563 | 564 | if (consume) 565 | this.consume(24); 566 | 567 | if (byteOrder === 'lsb') { 568 | let carry = firstByte; 569 | firstByte = thirdByte; 570 | thirdByte = carry; 571 | } 572 | 573 | return firstByte << 16 | secondByte << 8 | thirdByte; 574 | } 575 | 576 | private readPartialByte(length: number, consume: boolean) { 577 | let buffer = this.buffers[this._bufferIndex]; 578 | let byte = buffer[Math.floor(this._offsetIntoBuffer / 8)]; 579 | let bitOffset = this._offsetIntoBuffer % 8 | 0; 580 | 581 | if (consume) 582 | this.consume(length); 583 | 584 | return ((byte >> (8 - length - bitOffset)) & this.maskOf(length)) | 0; 585 | } 586 | 587 | /** 588 | * @param length 589 | * @param consume 590 | * @param byteOrder The byte order to use when the length is greater than 8 and is a multiple of 8. 591 | * Defaults to MSB (most significant byte). If the length is not a multiple of 8, 592 | * this is unused 593 | * @returns 594 | */ 595 | private readCoreSync(length : number, consume : boolean, byteOrder: 'msb' | 'lsb' = 'msb'): number { 596 | this.ensureNoReadPending(); 597 | 598 | if (this.available < length) 599 | throw new Error(`underrun: Not enough bits are available (requested=${length}, available=${this.bufferedLength}, buffers=${this.buffers.length})`); 600 | 601 | this.adjustSkip(); 602 | 603 | let offsetIntoByte = this._offsetIntoBuffer % 8; 604 | 605 | // Optimization cases ////////////////////////////////////////////////////////////////////////////// 606 | 607 | if (offsetIntoByte === 0) { 608 | if (length === 8) // Reading exactly one byte 609 | return this.readByteAligned(consume); 610 | else if (length === 16) // Reading a 16-bit value at byte boundary 611 | return this.readShortByteAligned(consume, byteOrder); 612 | else if (length === 24) 613 | return this.read3ByteAligned(consume, byteOrder); 614 | else if (length === 32) // Reading a 32-bit value at byte boundary 615 | return this.readLongByteAligned(consume, byteOrder); 616 | } 617 | 618 | if (length < 8 && ((8 - offsetIntoByte) | 0) >= length) // Reading less than 8 bits within a single byte 619 | return this.readPartialByte(length, consume); 620 | 621 | // The remaining path covers reads which are larger than one byte or which cross over byte boundaries. 622 | 623 | let remainingLength = length; 624 | let offset = this._offsetIntoBuffer; 625 | let bufferIndex = this._bufferIndex; 626 | let bigValue: bigint = BigInt(0); 627 | let value: number = 0; 628 | let useBigInt = length > 31; 629 | 630 | while (remainingLength > 0) { 631 | /* istanbul ignore next */ 632 | if (bufferIndex >= this.buffers.length) 633 | throw new Error(`Internal error: Buffer index out of range (index=${bufferIndex}, count=${this.buffers.length}), offset=${this.offset}, readLength=${length}, available=${this.available})`); 634 | 635 | let buffer = this.buffers[bufferIndex]; 636 | let byteOffset = Math.floor(offset / 8); 637 | 638 | /* istanbul ignore next */ 639 | if (byteOffset >= buffer.length) 640 | throw new Error(`Internal error: Current buffer (index ${bufferIndex}) has length ${buffer.length} but our position within the buffer is ${byteOffset}! offset=${this.offset}, bufs=${this.buffers.length}`); 641 | 642 | let bitOffset = offset % 8; 643 | let bitContribution: number; 644 | let byte = buffer[byteOffset]; 645 | 646 | bitContribution = Math.min(8 - bitOffset, remainingLength); 647 | 648 | if (useBigInt) { 649 | bigValue = (bigValue << BigInt(bitContribution)) 650 | | ((BigInt(buffer[byteOffset]) >> (BigInt(8) - BigInt(bitContribution) - BigInt(bitOffset))) 651 | & BigInt(this.maskOf(bitContribution))); 652 | } else { 653 | value = (value << bitContribution) 654 | | ((byte >> (8 - bitContribution - bitOffset)) 655 | & this.maskOf(bitContribution)); 656 | } 657 | 658 | // update counters 659 | 660 | offset += bitContribution; 661 | remainingLength -= bitContribution | 0; 662 | 663 | if (offset >= buffer.length*8) { 664 | bufferIndex += 1; 665 | offset = 0; 666 | } 667 | } 668 | 669 | if (consume) 670 | this.consume(length); 671 | 672 | if (useBigInt) 673 | return Number(bigValue); 674 | else 675 | return value; 676 | } 677 | 678 | private adjustSkip() { 679 | if (this.skippedLength <= 0) 680 | return; 681 | 682 | // First, remove any buffers that are completely skipped 683 | while (this.buffers && this.skippedLength > this.buffers[0].length*8-this._offsetIntoBuffer) { 684 | this.skippedLength -= (this.buffers[0].length*8 - this._offsetIntoBuffer); 685 | this._offsetIntoBuffer = 0; 686 | this.buffers.shift(); 687 | } 688 | 689 | // If any buffers are left, then the amount of remaining skipped bits is 690 | // less than the full length of the buffer, so entirely consume the skipped length 691 | // by putting it into the offset. 692 | if (this.buffers.length > 0) { 693 | this._offsetIntoBuffer += this.skippedLength; 694 | this.skippedLength = 0; 695 | } 696 | } 697 | 698 | /** 699 | * Wait until the given number of bits is available 700 | * @param length The number of bits to wait for 701 | * @param optional When true, the returned promise will resolve even if the stream ends before all bits are 702 | * available. Otherwise, the promise will reject. 703 | * @returns A promise which will resolve when the requested number of bits are available. Rejects if the stream 704 | * ends before the request is satisfied, unless optional parameter is true. 705 | */ 706 | assure(length : number, optional = false) : Promise { 707 | this.ensureNoReadPending(); 708 | 709 | if (this.bufferedLength >= length) { 710 | return Promise.resolve(); 711 | } 712 | 713 | return this.block({ length, assure: true }).then(available => { 714 | if (available < length && !optional) 715 | throw this.endOfStreamError(length); 716 | }); 717 | } 718 | 719 | 720 | 721 | /** 722 | * Read an unsigned integer with the given bit length, waiting until enough bits are 723 | * available for the operation. 724 | * 725 | * @param length The number of bits to read 726 | * @returns A promise which resolves to the unsigned integer once it is read 727 | */ 728 | read(length : number) : Promise { 729 | this.ensureNoReadPending(); 730 | 731 | if (this.available >= length) { 732 | return Promise.resolve(this.readSync(length)); 733 | } else { 734 | return this.block({ length }); 735 | } 736 | } 737 | 738 | /** 739 | * Read a two's complement signed integer with the given bit length, waiting until enough bits are 740 | * available for the operation. 741 | * 742 | * @param length The number of bits to read 743 | * @returns A promise which resolves to the signed integer value once it is read 744 | */ 745 | readSigned(length : number) : Promise { 746 | this.ensureNoReadPending(); 747 | 748 | if (this.available >= length) { 749 | return Promise.resolve(this.readSignedSync(length)); 750 | } else { 751 | return this.block({ length, signed: true }); 752 | } 753 | } 754 | 755 | private promise() { 756 | let resolve: (value: T) => void; 757 | let reject: (error: Error) => void; 758 | let promise = new Promise((rs, rj) => (resolve = rs, reject = rj)); 759 | return { promise, resolve, reject }; 760 | } 761 | 762 | private block(request: Omit): Promise { 763 | if (this._ended) { 764 | if (request.assure) { 765 | return Promise.resolve(this.available); 766 | } else { 767 | return Promise.reject(this.endOfStreamError(request.length)); 768 | } 769 | } 770 | 771 | this.blockedRequest = { 772 | ...request, 773 | ...this.promise() 774 | }; 775 | 776 | return this.blockedRequest.promise; 777 | } 778 | 779 | /** 780 | * Read an IEEE 754 floating point value with the given bit length, waiting until enough bits are 781 | * available for the operation. 782 | * 783 | * @param length The number of bits to read (must be 32 for 32-bit single-precision or 784 | * 64 for 64-bit double-precision) 785 | * @returns A promise which resolves to the floating point value once it is read 786 | */ 787 | readFloat(length : number) : Promise { 788 | this.ensureNoReadPending(); 789 | 790 | if (this.available >= length) { 791 | return Promise.resolve(this.readFloatSync(length)); 792 | } else { 793 | return this.block({ length, float: true }); 794 | } 795 | } 796 | 797 | /** 798 | * Asynchronously read a number of the given bitlength without advancing the read head. 799 | * @param length The number of bits to read. If there are not enough bits available 800 | * to complete the operation, the operation is delayed until enough bits become available. 801 | * @returns A promise which resolves iwth the number read from the bitstream 802 | */ 803 | async peek(length : number): Promise { 804 | await this.assure(length); 805 | return this.peekSync(length); 806 | } 807 | 808 | /** 809 | * Add a buffer onto the end of the bitstream. 810 | * @param buffer The buffer to add to the bitstream 811 | */ 812 | addBuffer(buffer : Uint8Array) { 813 | if (this._ended) 814 | throw new Error(`Cannot add buffers to a reader which has been marked as ended without calling reset() first`); 815 | 816 | this.buffers.push(buffer); 817 | this.bufferedLength += buffer.length * 8; 818 | 819 | if (this.blockedRequest && this.blockedRequest.length <= this.available) { 820 | let request = this.blockedRequest; 821 | this.blockedRequest = null; 822 | 823 | if (request.assure) { 824 | request.resolve(request.length); 825 | } else if (request.signed) { 826 | request.resolve(this.readSignedSync(request.length)); 827 | } else if (request.float) { 828 | request.resolve(this.readFloatSync(request.length)); 829 | } else { 830 | request.resolve(this.readSync(request.length)); 831 | } 832 | } 833 | } 834 | 835 | private _ended = false; 836 | get ended() { return this._ended; } 837 | 838 | reset() { 839 | if (this.blockedRequest) { 840 | throw new Error(`Cannot reset while there is a blocked request!`); 841 | } 842 | 843 | this.buffers = []; 844 | this.bufferedLength = 0; 845 | this.blockedRequest = null; 846 | this._offsetIntoBuffer = 0; 847 | this._bufferIndex = 0; 848 | this._offset = 0; 849 | this._spentBufferSize = 0; 850 | this._ended = false; 851 | } 852 | /** 853 | * Inform this reader that it will not receive any further buffers. Any requests to assure bits beyond the end of the 854 | * buffer will result ss 855 | */ 856 | end() { 857 | this._ended = true; 858 | 859 | if (this.blockedRequest) { 860 | let request = this.blockedRequest; 861 | this.blockedRequest = null; 862 | 863 | if (request.length <= this.available) 864 | throw new Error(`Internal inconsistency in @/bitstream: Should have granted request prior. Please report this bug.`); 865 | 866 | if (request.assure) { 867 | request.resolve(this.available); 868 | } else { 869 | request.reject(this.endOfStreamError(request.length)); 870 | } 871 | } 872 | } 873 | 874 | private endOfStreamError(length: number) { 875 | return new Error(`End of stream reached while reading ${length} bits, only ${this.available} bits are left in the stream`) 876 | } 877 | } 878 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @/bitstream 2 | 3 | [![npm](https://img.shields.io/npm/v/@astronautlabs/bitstream)](https://npmjs.com/package/@astronautlabs/bitstream) 4 | [![CircleCI](https://circleci.com/gh/astronautlabs/bitstream.svg?style=svg)](https://circleci.com/gh/astronautlabs/bitstream) 5 | 6 | - **Isomorphic**: Works in Node.js and in the browser 7 | - **Zero-dependency**: No runtime dependencies 8 | - **Battle-hardened**: Used to implement media standards at Astronaut Labs 9 | - **Comprehensive testing**: [90.38% coverage](https://218-305936359-gh.circle-artifacts.com/0/coverage/lcov-report/index.html) and growing! 10 | - **Performant**: Elements use generators (not promises) internally to maximize performance 11 | - **Flexible**: Supports both imperative and declarative styles 12 | - **Modern**: Ships as ES modules (with CommonJS fallback) 13 | 14 | Highly performant Typescript library for reading and writing to "bitstreams": tightly packed binary streams containing 15 | fields of varying lengths of bits. This package lets you treat a series of bytes as a series of bits without needing to 16 | manage which bytes the desired bit-fields fall within. Bitstreams are most useful when implementing network protocols 17 | and data formats (both encoders and decoders). 18 | 19 | # Motivation 20 | 21 | [Astronaut Labs](https://astronautlabs.com) is building a next-generation broadcast technology stack centered around 22 | Node.js and Typescript. That requires implementing a large number of binary specifications. We needed a way to do 23 | this at scale while ensuring accuracy, quality and comprehensiveness. We also see value in making our libraries open 24 | source so they can serve as approachable reference implementations for other implementors and increase competition in 25 | our industry. The best way to do that is to be extremely comprehensive in the way we build these libraries. 26 | Other implementations tend to skip "irrelevant" details or take shortcuts, we strive to avoid this to produce the most 27 | complete libraries possible, even if we don't need every detail for our immediate needs. 28 | 29 | # Installation 30 | 31 | `npm install @astronautlabs/bitstream` 32 | 33 | # Libraries using Bitstream 34 | 35 | The following libraries are using Bitstream. They are great examples of what you can do with it! If you would like your 36 | library to be listed here, please send a pull request! 37 | 38 | - https://github.com/astronautlabs/st2010 39 | - https://github.com/astronautlabs/st291 40 | - https://github.com/astronautlabs/scte35 41 | - https://github.com/astronautlabs/scte104 42 | - https://github.com/astronautlabs/rfc8331 43 | 44 | # BitstreamReader: Reading from bitstreams imperatively 45 | 46 | ```typescript 47 | import { BitstreamReader } from '@astronautlabs/bitstream'; 48 | 49 | let reader = new BitstreamReader(); 50 | 51 | reader.addBuffer(Buffer.from([0b11110000, 0b10001111])); 52 | 53 | await reader.read(2); // == 0b11 54 | await reader.read(3); // == 0b110 55 | await reader.read(4); // == 0b0001 56 | await reader.read(7); // == 0b0001111 57 | ``` 58 | 59 | The above will read the values as unsigned integers in big-endian (network byte order) format. 60 | 61 | ## Asynchronous versus Synchronous 62 | 63 | All read operations come in two flavors, asynchronous and synchronous. For instance to read an unsigned integer 64 | asynchronously, use `read()`. For this and other asynchronous read operations, resolution of the resulting promise 65 | is delayed until enough data is available to complete the operation. Note that there can be only one asynchronous 66 | read operation in progress at a time for a given BitstreamReader object. 67 | 68 | The synchronous method for reading unsigned integers is `readSync()`. When using synchronous methods, there must be 69 | enough bytes available to the reader (via `addBuffer()`) to read the desired number of bits. If this is not the case, 70 | an exception is thrown. You can check how many bits are available using the `isAvailable()` method: 71 | 72 | ```typescript 73 | if (reader.isAvailable(10)) { 74 | // 10 bits are available 75 | let value = reader.readSync(10); 76 | } 77 | ``` 78 | 79 | Alternatively, you can use `.assure()` to wait until the desired number of bits are available. Again, there can only be 80 | one pending call to `read()` or `assure()` at a time. This allows you to "batch" synchronous reads in a single 81 | "await" operation. 82 | 83 | ```typescript 84 | await reader.assure(13); 85 | let value1 = reader.readSync(3); 86 | let value2 = reader.readSync(10); 87 | ``` 88 | 89 | ## Reading signed integers 90 | 91 | Use the `readSigned` / `readSignedSync` methods to read a signed two's complement integer. 92 | 93 | ## Reading floating-point integers [IEEE 754] 94 | 95 | Use the `readFloat` / `readFloatSync` methods to read an IEEE 754 floating point value. The bit length passed must be 96 | either 32 (32-bit single-precision) or 64 (64-bit double-precision). 97 | 98 | ## Reading Strings 99 | 100 | Use the `readString` / `readStringSync` methods to read string values. 101 | 102 | ```typescript 103 | await reader.readString(10); // read a fixed length string with 10 characters. 104 | ``` 105 | 106 | By default `readString()` will cut off the string at the first character with value `0` (ie, the string is 107 | considered null-terminated) and stop reading. You can disable this behavior so that the returned string always 108 | contains all bytes that were in the bitstream: 109 | 110 | ```typescript 111 | await reader.readString(10, { nullTerminated: false }); 112 | ``` 113 | 114 | The default text encoding is UTF-8 (`utf-8`). You can read a string using any text encoding supported by the platform 115 | you are on. For Node.js these are the encodings supported by `Buffer`. On the web, only `utf-8` is available 116 | (see documentation for `TextEncoder`/`TextDecoder`). 117 | 118 | ```typescript 119 | await reader.readString(10, { encoding: 'utf16le' }) 120 | ``` 121 | 122 | > **Important**: In cases like above where you are using encodings where a character spans 123 | > multiple bytes (including UTF-8), the length given to `readString()` is always the _number of 124 | > bytes_ not the _number of characters_. It is easy to make mistaken assumptions in this regard. 125 | 126 | # BitstreamWriter: Writing to bitstreams imperatively 127 | 128 | ```typescript 129 | import { BitstreamWriter } from '@astronautlabs/bitstream'; 130 | 131 | let writer = new BitstreamWriter(writableStream, bufferLength); 132 | 133 | writer.write(2, 0b10); 134 | writer.write(10, 0b1010101010); 135 | writer.write(length, value); 136 | ``` 137 | 138 | `writableStream` can be any object which has a `write(chunk : Uint8Array)` method (see exported `Writable` interface). 139 | 140 | Examples of writables you can use include: 141 | - Node.js' writable streams (`Writable` from the `stream` package) 142 | - `WritableStreamDefaultWriter` from the WHATWG Streams specification in the browser 143 | - Any custom object which implements the `Writable` interface 144 | 145 | The `bufferLength` parameter determines how many bytes will be buffered before the buffer will be flushed out to the 146 | passed writable stream. This parameter is optional, the default (and minimum value) is `1` (one byte per buffer). 147 | 148 | # Writing unsigned integers 149 | 150 | ```typescript 151 | writer.write(2, 0b01); 152 | writer.write(2, 0b1101); 153 | writer.write(2, 0b1111101); // 0b01 will be written 154 | ``` 155 | 156 | > **Note**: Any bits in `value` above the `length`'th bit will be ignored, so all of the above are equivalent. 157 | 158 | # Writing signed integers 159 | 160 | Use the `writeSigned()` method to write signed two's complement integers. 161 | 162 | # Writing floating point values 163 | 164 | Use the `writeFloat()` method to write IEEE 754 floating point values. Only lengths of 32 (for 32-bit single-precision) 165 | and 64 (for 64-bit double-precision) are supported. 166 | 167 | # Writing strings 168 | 169 | Use the `writeString()` method to write string values. The default encoding is UTF-8 (`utf-8`). 170 | Any other encoding supported by the platform can be used (ie those supported by `Buffer` on Node.js). On the web, only `utf-8` is supported (see `TextEncoder` / `TextDecoder`). 171 | 172 | # Writing byte arrays 173 | 174 | Use the `writeBuffer()` method to write byte arrays. On Node.js you can also pass `Buffer`. 175 | 176 | # BitstreamElement: declarative structural serialization 177 | 178 | Efficient structural (de)serialization can be achieved by building subclasses of the `BitstreamElement` class. 179 | 180 | ## Deserialization (reading) 181 | 182 | You can declaratively specify elements of bitstreams, then read and write them to bitstream readers/writers as needed. To do this, extend the `BitstreamElement` class: 183 | 184 | ```typescript 185 | import { BitstreamElement, Field } from '@astronautlabs/bitstream'; 186 | 187 | class MyElement extends BitstreamElement { 188 | @Field(2) field1 : number; 189 | @Field(4) field2 : number; 190 | @Field(3) field3 : number; 191 | @Field(1) field4 : number; 192 | @Field(1) field5 : boolean; 193 | } 194 | ``` 195 | 196 | Then, read from a BitstreamReader using: 197 | 198 | ```typescript 199 | let element = await MyElement.read(bitstreamReader); 200 | ``` 201 | 202 | Or, deserialize from a Buffer using: 203 | 204 | ```typescript 205 | let element = await MyElement.deserialize(buffer); 206 | ``` 207 | 208 | ### Number fields 209 | 210 | - `null` and `undefined` are written as `0` during serialization 211 | - Numbers are treated as big-endian unsigned integers by default. Decimal portions of numbers are truncated. 212 | - Use the `{ number: { format: 'signed' }}` option to use signed two's complement integer. Decimals are truncated 213 | - Use the `{ number: { format: 'float' }}` option to use IEEE 754 floating point. Decimals are retained 214 | - An error is thrown when trying to serialize `NaN` or infinite values (except when using the `float` format) 215 | 216 | ### Boolean fields 217 | 218 | If you specify type `boolean` for a field, the integer value `0` will be deserialized to `false` and all other values 219 | will be deserialized as `true`. When booleans are serialized, `0` is used for `false` and `1` is used for true. 220 | 221 | You can customize this behavior using the boolean field options (ie `@Field(8, { boolean: { ... } })`): 222 | 223 | - `true`: The numeric value to use for `true` (default `1`) 224 | - `false`: The numeric value to use for `false` (default `0`) 225 | - `undefined`: The numeric value to use when writing `undefined` (default `0`) 226 | - `mode`: How to handle novel inputs when reading: 227 | - `"true-unless"`: The value is true unless the numeric value chosen for 'false' is observed (default mode). For 228 | example `0` is `false`, `1` is `true`, `100` is `true` 229 | - `"false-unless"`: The value is false unless the numeric value chosen for 'true' is observed. For example 230 | `0` is `false`, `1` is `true`, `100` is `false` 231 | - `"undefined"`: The value is `true` if the numeric value for 'true' is observed, `false` if the numeric value for 232 | 'false' is observed and `undefined` otherwise. For example `0` is `false`, `1` is `true`, `100` is `undefined`. 233 | 234 | If none of these options fit your use case, you can write a custom `Serializer`. 235 | 236 | ### String fields 237 | 238 | Elements can also handle serializing fixed-length strings. For example, to represent a fixed-length 5-byte string, 239 | set the length to `5` (note that this differs from other types where the length is specified in bits). 240 | 241 | > **Note:** Null-termination is the default here, make sure to set `nullTerminated` to `false` if you do not desire 242 | > that behavior. 243 | 244 | ```typescript 245 | @Field(5) stringField : string; 246 | ``` 247 | 248 | If you wish to control the encoding options, use the `string` options group: 249 | 250 | ```typescript 251 | @Field(5, { string: { encoding: 'ascii', nullTerminated: false } }) 252 | stringField : string; 253 | ``` 254 | 255 | For information about the available encodings, see "Reading Strings" above. 256 | 257 | ### Byte array fields (Buffers) 258 | 259 | You can represent a number of bytes as a byte array: 260 | 261 | ```typescript 262 | @Field(10*8) read10BytesIntoBuffer : Uint8Array; 263 | ``` 264 | 265 | When running on Node.js you can also specify `Buffer` as the type instead: 266 | 267 | ```typescript 268 | @Field(10*8) read10BytesIntoBuffer : Buffer; 269 | ``` 270 | 271 | ### Nested elements 272 | 273 | You can also nest element classes: 274 | 275 | ```typescript 276 | class PartElement extends BitstreamElement { 277 | @Field(3) a : number; 278 | @Field(5) b : number; 279 | } 280 | 281 | class WholeElement extends BitstreamElement { 282 | @Field() part1 : PartElement; 283 | @Field() part2 : PartElement; 284 | } 285 | ``` 286 | 287 | ### Arrays 288 | 289 | Many kinds of arrays are supported. 290 | 291 | #### Array with number values (unsigned/signed/float) 292 | 293 | Arrays with number elements are natively supported for convenience. 294 | 295 | For example, to read 10 8-bit unsigned integers: 296 | 297 | ```typescript 298 | class BufferElement extends BitstreamElement { 299 | @Field(10, { array: { type: Number, elementLength: 8 }}) 300 | array : number[]; 301 | } 302 | ``` 303 | 304 | > **Important:** 305 | > - You must specify the `array.type` option when using elements. 306 | > Typescript cannot convey the element type via reflection, only that the field is of type `Array` 307 | > which is not enough information to deserialize the array. 308 | > - When using arrays of numbers you must specify the `array.elementLength` option for the library to know how many 309 | > bits each array item represents within the bitstream. For other array item types such as BitstreamElement 310 | > the length is already known and `array.elementLength` is ignored 311 | 312 | You can read signed integers and floating point values instead of unsigned integers by specifying the `number.format` 313 | option (see Number Fields for more information). 314 | 315 | When handling arrays, the length parameter of `@Field()` is used to represent the item count for the array by default. Alternatively (and mainly for historical reasons) you can also set the `options.array.count` property. 316 | 317 | #### Derived lengths 318 | 319 | As with all fields, you can represent the number of items dynamically with a determinant fucntion, allowing the array 320 | length to be dependent on any value already read before the field being read. For more information about this, see the 321 | section on "Dynamic Lengths" below. 322 | 323 | You can pair this with the `writtenValue` feature to ensure that the correct length is always written to the bitstream. 324 | 325 | #### Array with preceding count 326 | 327 | You can specify that a preceding count field should be read to determine what the length of the array is in the 328 | `countFieldLength` option. If this option is not provided or undefined, no preceding count field is read. 329 | 330 | #### Array with element values 331 | 332 | You can also use arrays of elements: 333 | 334 | ```typescript 335 | 336 | class ItemElement extends BitstreamElement { 337 | @Field(3) a : number; 338 | @Field(5) b : number; 339 | } 340 | 341 | class ListElement extends BitstreamElement { 342 | @Field({ array: { type: PartElement, countFieldLength: 8 }}) 343 | parts : PartElement[]; 344 | } 345 | ``` 346 | 347 | The above will expect an 8-bit "count" field (indicates how many items are in the array), followed by that number of 348 | `PartElement` syntax objects. Since `ItemElement` is 8 bits long, when the count is `3`, there will be `3` additional 349 | bytes (24 bits) following the count in the bitstream before the next element begins. 350 | 351 | #### Dynamic arrays 352 | 353 | Some bitstream formats do not send information about the number of items prior to the items themselves. In such cases 354 | there is often a final "sentinel" value instead. This can be handled by using the `hasMore` discriminant: 355 | 356 | For instance, consider the following set up where the array ends when a suffix field is false: 357 | 358 | ```typescript 359 | class ItemElement extends BitstreamElement { 360 | @Field(8) value : number; 361 | @Field(8) hasMore : boolean; 362 | } 363 | 364 | class ListElement extends BitstreamElement { 365 | @Field(0, { array: { type: ItemElement, hasMore: a => a[a.length - 1].hasMore }}) 366 | array : ItemElement[]; 367 | } 368 | ``` 369 | 370 | ### Optional fields 371 | 372 | You can make any field optional by using the `presentWhen` and/or `excludedWhen` options: 373 | 374 | ```typescript 375 | class ItemElement extends BitstreamElement { 376 | @Field(3) a : number; 377 | @Field(5, { presentWhen: i => i.a === 10 }) b : number; 378 | } 379 | ``` 380 | 381 | In the above case, the second field is only present when the first field is `10`. 382 | 383 | `excludedWhen` is the opposite of `presentWhen` and is provided for convenience and expressiveness. 384 | 385 | ### Field Initializers 386 | 387 | When using `@Field()` to present a child BitstreamElement relationship, 388 | it can be useful to run some code to initialize the instance that is 389 | created. 390 | 391 | ```ts 392 | class Parent { 393 | @Field(8) 394 | context: number; 395 | 396 | @Field({ initializer: (instance, parent) => instance.context = parent.context }) 397 | child: Child; 398 | } 399 | 400 | class Child { 401 | context: number; 402 | } 403 | ``` 404 | 405 | You can also use this from the static `read()` method: 406 | 407 | ```ts 408 | Child.read(reader, { initializer: instance => instance.context = somePreviouslyParsedContextNumber }); 409 | ``` 410 | 411 | This is a useful pattern for passing knowledge needed for parsing a child element down from unknown parent elements. 412 | 413 | ### Dynamic lengths 414 | 415 | Sometimes the length of a field depends on what has been read before the field being read. You can specify lengths as _determinant_ functions which determine lengths dynamically: 416 | 417 | ```typescript 418 | class ItemElement extends BitstreamElement { 419 | @Field(3) length : number; 420 | @Field(i => i.length) value : number; 421 | } 422 | ``` 423 | 424 | In the above, if `length` is read as `30`, then the `value` field is read/written as 30 bits long. 425 | Determinants can make use of any property of the instance which appear in the bitstream before the current element. 426 | They are also passed the bitstream element which contains this one (if one exists). 427 | 428 | Determinants are available on many other properties as well. Where booleans are expected _discriminants_ work the same 429 | way. Examples of features that accept discriminants include `hasMore`, `presentWhen` and discriminants are the core 430 | concept which enabling element variation (`@Variant()`). 431 | 432 | ### Variation 433 | 434 | Many bitstream formats make use of the concept of specialization. BitstreamElement supports this using its variation 435 | system. Any BitstreamElement class may have zero or more classes (which are marked with `@Variant()`) which are 436 | automatically substituted for the class which is being read provided the discriminant passed to `@Variant()` holds true. 437 | 438 | ```typescript 439 | class BaseElement extends BitstreamElement { 440 | @Field(3) type : number; 441 | } 442 | 443 | @Variant(i => i.type === 0x1) 444 | class Type1Element extends BaseElement { 445 | @Field(5) field1 : number; 446 | } 447 | 448 | @Variant(i => i.type === 0x2) 449 | class Type2Element extends BaseElement { 450 | @Field(5) field1 : number; 451 | } 452 | ``` 453 | 454 | When reading an instance of `BaseElement` the library automatically checks the discriminants specified on the defined 455 | Variant classes to determine the appropriate subclass to substitute. In the case above, if `type` is read as `0x2` when 456 | performing `await BaseElement.read(reader)`, then the result will be an instance of `Type2Element`. 457 | 458 | > **Note:** Variation which occurs at _the end_ of a bitstream element (as above) is referred to as "tail variation". 459 | > Variation which occurs in _the middle_ of a bitstream element is referred to as "marked variation". 460 | 461 | ### Marked variation 462 | 463 | Sometimes a bitstream format specifies that the specialized fields fall somewhere in the middle of the overall 464 | structure, with the fields of the base class falling both before and after those found in the subclass. This is called 465 | "marked variation". 466 | 467 | `BitstreamElement` can accomodate this using `@VariantMarker()`: 468 | 469 | ```typescript 470 | class BaseElement extends BitstreamElement { 471 | @Field(3) type : number; 472 | @VariantMarker() $variant; 473 | @Field(8) checksum : number; 474 | } 475 | 476 | @Variant(i => i.type === 0x1) 477 | class Type1Element extends BaseElement { 478 | @Field(5) field1 : number; 479 | } 480 | 481 | @Variant(i => i.type === 0x2) 482 | class Type2Element extends BaseElement { 483 | @Field(5) field1 : number; 484 | } 485 | ``` 486 | 487 | In the above example, variation will occur after reading `type` but before reading `checksum`. After variation occurs 488 | (resulting in a `Type2Element` instance), the `checksum` field will then be read. 489 | 490 | ### Computing written values 491 | 492 | Sometimes it is desirable to override the value present in a field with a specific formula. This is useful when 493 | representing the lengths of arrays, Buffers, or ensuring that a certain hardcoded value is always written regardless 494 | of what is specified in the instance being written. Use the `writtenValue` option to override the value that is 495 | specified on an instance: 496 | 497 | ```typescript 498 | class Type2Element extends BaseElement { 499 | @Field(version, { writtenValue: () => 123 }) 500 | version : number; 501 | } 502 | ``` 503 | 504 | The above element will always write `123` in the field specified by `version`. 505 | 506 | You can depend on properties of the containing object as well. Unlike determinants used during reading, these functions 507 | have full access to the value being serialized, so they can look ahead as well as behind for determining the value to 508 | write. 509 | 510 | ```typescript 511 | class Type2Element extends BaseElement { 512 | @Field(version, { writtenValue: i => i.items.length }) 513 | count : number; 514 | 515 | @Field(i => i.count, { array: { type: Number, elementLength: 8 }}) 516 | items : number[]; 517 | } 518 | ``` 519 | 520 | This sort of arrangement is useful for handling more complex array length scenarios, as `count` is able to be parsed 521 | and serialized independently from `items`, thus allowing `items` to reference `count` in it's determinant function. 522 | 523 | Similarly, when writing the value to bitstream, the value found in `items` determines the value of `count`, ensuring 524 | that the two are always in sync _on the wire_. 525 | 526 | One downside of this approach is that callers interacting with the Type2Element instances must be aware that `count` 527 | and `items` are not intrinsically linked (even if temporarily), and thus may not agree at all times. 528 | 529 | A cleaner approach to handle this is to encapsulate this into the object and forbid writing to `count` outside of 530 | parsing: 531 | 532 | ```typescript 533 | class Type2Element extends BaseElement { 534 | @Field(version, { writtenValue: i => i.items.length }) 535 | private $count : number; 536 | 537 | get count() { return this.items?.length ?? this.$count; } 538 | 539 | @Field(i => i.$count, { array: { type: Number, elementLength: 8 }}) 540 | private $items : number[]; 541 | 542 | get items() { return this.items; } 543 | set items(value) { 544 | this.$items = value ?? []; 545 | this.$count = this.$items.length; 546 | } 547 | } 548 | ``` 549 | 550 | Here `count` and `items.length` will always agree at all times, with the bonus that assigning `undefined` to the array 551 | is forbidden. 552 | 553 | ### Measurement 554 | 555 | It can be useful to measure the bitlength of a portion of a BitstreamElement. Such a measurement could be used when 556 | defining the length of a bit field or when defining the value of a bitfield. Use the `measure()` method to accomplish 557 | this. 558 | 559 | ```typescript 560 | class Type2Element extends BaseElement { 561 | @Field(8, { writtenValue: i => i.measure('version', 'checksum') }) 562 | length : number; 563 | 564 | @Field(version, { writtenValue: () => 123 }) 565 | version : number; 566 | 567 | @VariantMarker() $variant; 568 | 569 | checksum : number; 570 | } 571 | ``` 572 | 573 | In the above, the written value of `length` will be the number of bits occupied by the fields starting from `version` through `checksum` inclusive, including all fields specified by the variant of the element which is being written. 574 | 575 | ### Markers 576 | 577 | Measurement is extremely useful, but because `measure()` only measures fields _inclusively_ it is tempting to introduce 578 | fields with zero length which can be used as _markers_ for measurement purposes. You can absolutely do this with fields 579 | marked `@Field(0)`, but there is a dedicated decorator for this: `@Marker()`, which also marks the field as "ignored", 580 | meaning it will never actually be read or written from the relevant object instance. 581 | 582 | ```typescript 583 | class Type2Element extends BaseElement { 584 | @Field(version, { writtenValue: () => 123 }) 585 | version : number; 586 | 587 | @Field(8, { writtenValue: i => i.measure('$lengthStart', '$lengthEnd') }) 588 | length : number; 589 | 590 | @Marker() $lengthStart; 591 | 592 | @VariantMarker() $variant; 593 | 594 | @Marker() $lengthEnd; 595 | 596 | checksum : number; 597 | } 598 | ``` 599 | 600 | In the above example, the written value of `length` will be the bit length of the fields provided by the variant subclass, not including any other fields. The fields `$lengthStart` and `$lengthEnd` will not contribute any data to the bitstream representation. 601 | 602 | ### Measurement shortcuts 603 | 604 | There is also `measureTo()` and `measureFrom()` which measure the entire element up to and including a specific field, and from a specific field to the end of an element, respectively. You can also use `measureField()` to measure the size of a specific field. 605 | 606 | ### Type-safe field references 607 | 608 | Note that in prior examples we specified field references as strings when calling `measure()`. You can also specify field references as functions which allow for better type safety: 609 | 610 | ```typescript 611 | class Type2Element extends BaseElement { 612 | @Field(version, { writtenValue: () => 123 }) 613 | version : number; 614 | 615 | @Field(8, { writtenValue: (i : Type2Element) => i.measureFrom(i => i.$lengthStart) }) 616 | length : number; 617 | 618 | @Marker() $lengthStart; 619 | @VariantMarker() $variant; 620 | } 621 | ``` 622 | 623 | In the case above the type of the determinant is carried through into the `measureFrom()` call, and Typescript will 624 | alert you if you reference a field that doesn't exist on `Type2Element`. 625 | 626 | ### Reserved fields 627 | 628 | There is also `@Reserved(length)` which can be used in place of `@Field(length)`. This decorator marks a field as 629 | taking up space in the bitstream, but ignores the value in the object instance when reading/writing. Instead the value 630 | is always high bits. That is, a field marked with `@Reserved(8)` will always be written as `0xFF` (`0b11111111`). This 631 | can be useful for standards which specify that reserved space should be filled with high bits. `@ReservedLow()` is also 632 | provided which will do the opposite- filling the covered bits with all zeroes. 633 | 634 | Other schemes can be accomodated with custom decorators. For instance: 635 | 636 | ```typescript 637 | function RandomReserved(length : LengthDeterminant) { 638 | return Field(length, { ignored: true, writtenValue: () => Math.floor(Math.random() * 2**length) }); 639 | } 640 | ``` 641 | 642 | The above function can be used as a decorator to specify that the values read/written should be ignored (and not read/ 643 | written to the object), and when writing the value to a bitstream it should be populated with a random value. It could 644 | be used like so: 645 | 646 | ```typescript 647 | class MyElement extends BitstreamElement { 648 | @RandomReserved() reserved : number; 649 | } 650 | ``` 651 | 652 | ## Writing elements to bitstreams 653 | 654 | Write to a BitstreamWriter with: 655 | 656 | ```typescript 657 | element.write(bitstreamWriter); 658 | ``` 659 | 660 | Serialize to a Buffer using: 661 | 662 | ```typescript 663 | let buffer = element.serialize(); 664 | ``` 665 | 666 | ## Custom Serializers 667 | 668 | `BitstreamElement` can be extended to support arbitrary field types by implementing new serializers. Simply 669 | implement the `Serializer` interface and use the `serializer` option on the `@Field()` properties you wish to use it 670 | with. 671 | 672 | ## Advanced element serialization 673 | 674 | If you need to dynamically omit or include some fields, or otherwise implement custom serialization, 675 | you can override the `deserializeFrom()` and `serializeTo()` methods in your subclass. This allows you 676 | to control exactly how fields are populated. You can call `deserializeGroup(bitstreamReader, '*')` to 677 | immediately deserialize all fields, or `deserializeGroup(bitstreamReader, 'groupName')` to deserialize 678 | fields which are in the group named `groupName`. 679 | 680 | You may also require additional information to be available to your deserialization/serialization 681 | routines. For instance: 682 | 683 | ```typescript 684 | export class FirstElement extends BitstreamElement { 685 | @Field(2) foo : number; 686 | } 687 | 688 | export class SecondElement extends BitstreamElement { 689 | deserializeFrom(bitstreamReader : BitstreamReader, firstElement : BitstreamElement) { 690 | 691 | } 692 | } 693 | 694 | let firstElement = FirstElement.deserializeSync(bitstreamReader); 695 | let secondElement = new SecondElement(); 696 | secondElement.deserializeFrom(bitstreamReader, firstElement); 697 | ``` 698 | 699 | Here, we are passing a previously decoded element into the `deserializeFrom()` of the element being deserialized. You 700 | could pass any arbitrary data in this fashion, giving you flexibility in how you handle advanced serialization. 701 | 702 | ### Allowing Exhaustion 703 | 704 | When using `BitstreamElement#deserialize()` to parse an element object from a byte array / buffer, you can use 705 | `allowExhaustion` to suppress the exception when the available bits are exhausted and instead return the partially read 706 | object. This can be very useful for diagnostics or for cases where there are optional tailing fields. 707 | 708 | # Architecture 709 | 710 | ## Generators 711 | 712 | The elements system uses generators internally in a coroutine-like fashion for allowing parsing to work asynchronously 713 | while still taking little or no performance penalty when using the parser in a synchronous manner. When the bitstream 714 | becomes exhausted during reading, the reader yields the number of bits it needs. The caller is then responsible 715 | for waiting until that amount of bits is available, and then resuming the generator where it left off. The caller uses 716 | `BitstreamReader.assure()` to accomplish this. 717 | 718 | For this reason, many lower level read operations return `Generator` instead of the final value or the `Promise`. In 719 | a fully synchronous setting (such as `deserialize` or an already assured read), the caller will use the generator to 720 | get a single (and final) value back which will be the value that was read. 721 | 722 | Using generators internally provides a massive performance boost over doing the same with Promises as the original 723 | version of this library did. See below for an analysis of the relevant performance aspects. 724 | 725 | # Performance 726 | 727 | When reading data from BitstreamReader, you have two options: use the synchronous methods which will throw if not 728 | enough data is available, or the asynchronous methods which will wait for the data to arrive before completing the read 729 | operation. If you know you have enough data to complete the operation, you can read synchronously to avoid the overhead 730 | of creating and awaiting a Promise. If your application is less performance intensive you can instead receive a Promise 731 | for when the data becomes available (which happens by a `addBuffer()` call). This allows you to create a 732 | pseudo-blocking control flow similar to what is done in lower level languages like C/C++. However using promises in 733 | this manner can cause a huge reduction in performance while reading data. You should only use the async API when 734 | performance requirements are relaxed. 735 | 736 | When reading data into a **declarative BitstreamElement** class however, ECMAscript generators are used to control 737 | whether the library needs to wait for more data. When reading a BitstreamElement using the Promise-based API you are 738 | only incurring the overhead of the Promise API once for the initial call, and once each time there is not enough data 739 | available in the underlying BitstreamReader, which will only happen if the raw data is not arriving fast enough. In 740 | that case, though the Promise will have the typical overhead, it will not impact throughput because the IO wait time 741 | will be larger than the time necessary to handle the Promise overhead. We believe that this effectively eliminates the 742 | overhead of using an async/await pattern with reasonably sized BitstreamElements, so we encourage you start there and 743 | optimize only if you run into throughput / CPU / memory bottlenecks. 744 | 745 | With generators at the core of BitstreamElement, implementing high throughput applications such as audio and video 746 | processing are quite viable with this library. You are more likely to run into a bottleneck with Javascript or Node.js 747 | itself than to be bottlenecked by using declarative BitstreamElement classes, of course your mileage may vary! If you 748 | believe BitstreamElement is a performance bottleneck for you, please file an issue! 749 | 750 | ## So is it faster to "hand roll" using BitstreamReader instead of using BitstreamElements? 751 | 752 | Absolutely not. You should use BitstreamElement whereever possible, because it is using generators as the core 753 | mechanism for handling bitstream exhaustion events. Using generators internally instead of promises is _dramatically_ 754 | faster. To see this, compare the performance of @astronautlabs/bitstream@1 with @astronautlabs/bitstream@2. Using 755 | generators in the core was introduced in v2.0.0, prior to that a Promise was issued for every individual read call. 756 | 757 | How many times can each version of the library read the following BitstreamElement structure within a specified period 758 | of time? 759 | 760 | ```typescript 761 | class SampleItem extends BitstreamElement { 762 | @Field(1) b1 : number; 763 | @Field(1) b2 : number; 764 | @Field(1) b3 : number; 765 | @Field(1) b4 : number; 766 | @Field(1) b5 : number; 767 | @Field(1) b6 : number; 768 | @Field(1) b7 : number; 769 | @Field(1) b8 : number; 770 | } 771 | 772 | class SampleContainer extends BitstreamElement { 773 | @Field(0, { array: { type: SampleItem, countFieldLength: 32 }}) 774 | items : SampleItem[]; 775 | } 776 | ``` 777 | 778 | The results are night and day: 779 | > **Iteration count while parsing a 103-byte buffer in 500ms** (_Intel Core i7 6700_) 780 | > - @astronautlabs/bitstream@1.1.0: Read the buffer 104 times 781 | > - @astronautlabs/bitstream@2.0.2: Read the buffer 1,163,576 times 782 | 783 | While we're proud of the performance improvement, it really just shows the overhead of Promises and how that 784 | architecture was the wrong choice for BitstreamElements in V1. 785 | 786 | Similarly, we tried giving a 1GB buffer to each version with the above test -- V2 was able to parse the entire buffer 787 | in less than a millisecond, whereas V1 effectively _did not complete_ -- it takes _minutes_ to parse such a Buffer with 788 | V1 even once, and quite frankly we gave up waiting for it to complete. 789 | 790 | # Debugging Element Serialization 791 | 792 | It can be tricky to know where your app gets stuck reading a Bitstream Element. If you set 793 | `globalThis.BITSTREAM_TRACE = true` then Bitstream will begin outputting some trace messages to help you understand 794 | what field your app got stuck on while reading. This can be very helpful when your protocol descriptions have too many 795 | bits. 796 | 797 | # Contributing 798 | 799 | We encourage you to file issues and send pull requests! Please be sure to follow the 800 | [Code of Conduct](CODE_OF_CONDUCT.md). 801 | 802 | ## Tests 803 | 804 | Use `npm test` to run the test suite after making changes. Code coverage will also be generated. **Important:** Istanbul (the code coverage tool we are using) instruments the code to perform its coverage analysis. This breaks line numbers in stack traces. Use `npm test:nocov` to skip Code Coverage generation to obtain correct line numbers during testing. --------------------------------------------------------------------------------