├── .github ├── FUNDING.yml └── workflows │ ├── checks.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmarks ├── arrays.ts ├── big_struct.ts ├── popular_encodings.ts ├── string.ts ├── struct.ts └── tuple.ts ├── deno.json ├── examples └── basic.ts ├── mod.ts ├── src ├── array │ ├── array.ts │ ├── array_buffer.ts │ ├── array_buffer_test.ts │ ├── array_test.ts │ ├── mod.ts │ ├── sized_array.ts │ ├── typed_array.ts │ └── typed_array_test.ts ├── bitflags │ ├── _common.ts │ ├── bitflags16.ts │ ├── bitflags16_test.ts │ ├── bitflags32.ts │ ├── bitflags32_test.ts │ ├── bitflags64.ts │ ├── bitflags64_test.ts │ ├── bitflags8.ts │ ├── bitflags8_test.ts │ └── mod.ts ├── compound │ ├── mod.ts │ ├── sized_struct.ts │ ├── sized_struct_test.ts │ ├── struct.ts │ ├── struct_test.ts │ ├── tagged_union.ts │ ├── tagged_union_test.ts │ ├── tuple.ts │ ├── tuple_test.ts │ ├── union.ts │ └── union_test.ts ├── meta_types.ts ├── meta_types_test.ts ├── mod.ts ├── non-standard-numbers │ ├── big_numbers.ts │ ├── big_numbers_test.ts │ ├── mod.ts │ └── small_numbers.ts ├── primitives │ ├── bool.ts │ ├── bool_test.ts │ ├── f32.ts │ ├── f32_test.ts │ ├── f64.ts │ ├── f64_test.ts │ ├── i16.ts │ ├── i16_test.ts │ ├── i32.ts │ ├── i32_test.ts │ ├── i64.ts │ ├── i64_test.ts │ ├── i8.ts │ ├── i8_test.ts │ ├── mod.ts │ ├── u16.ts │ ├── u16_test.ts │ ├── u32.ts │ ├── u32_test.ts │ ├── u64.ts │ ├── u64_test.ts │ ├── u8.ts │ └── u8_test.ts ├── string │ ├── _common.ts │ ├── cstring.ts │ ├── cstring_test.ts │ ├── fixed_length.ts │ ├── fixed_length_test.ts │ ├── mod.ts │ └── prefixed_length.ts ├── types │ ├── common.ts │ ├── mod.ts │ ├── sized.ts │ └── unsized.ts ├── util.ts └── varint │ ├── _common.ts │ ├── i32_leb128.ts │ ├── i32_leb128_test.ts │ ├── i64_leb128.ts │ ├── i64_leb128_test.ts │ └── mod.ts └── test_deps.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: denosaurs 2 | github: denosaurs 3 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout sources 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup latest deno version 13 | uses: denoland/setup-deno@main 14 | 15 | - name: Run deno fmt 16 | run: deno fmt --check 17 | 18 | - name: Run deno lint 19 | run: deno lint 20 | 21 | test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout sources 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup latest deno version 28 | uses: denoland/setup-deno@main 29 | 30 | - name: Run deno test 31 | run: deno test 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: denoland/setup-deno@v2 17 | with: 18 | deno-version: v2.x 19 | - run: deno publish 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 the denosaurs team 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # byte_type 2 | 3 | [![Tags](https://img.shields.io/github/release/denosaurs/byte_type)](https://github.com/denosaurs/byte_type/releases) 4 | [![Checks](https://img.shields.io/github/actions/workflow/status/denosaurs/byte_type/checks.yml?branch=main)](https://github.com/denosaurs/byte_type/actions) 5 | [![License](https://img.shields.io/github/license/denosaurs/byte_type)](https://github.com/denosaurs/byte_type/blob/master/LICENSE) 6 | 7 | `byte_type` is a small helper module for efficiently working with different raw 8 | types represented as a bunch of bytes. Now with performance being close to 9 | native js performance and ergonomic interfaces! 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import { Struct, u32, u8 } from "https://deno.land/x/byte_type/mod.ts"; 15 | 16 | const buffer = new ArrayBuffer(8); 17 | const dt = new DataView(buffer); 18 | 19 | const struct = new Struct({ "b": u8, "a": u32 }); 20 | 21 | struct.write({ b: 8, a: 32 }, dt); 22 | console.log(struct.read(dt)); 23 | console.log(buffer); 24 | 25 | // Output: 26 | // { b: 8, a: 32 } 27 | // ArrayBuffer { 28 | // [Uint8Contents]: <08 00 00 00 20 00 00 00>, 29 | // byteLength: 8 30 | // } 31 | ``` 32 | 33 | ## Maintainers 34 | 35 | - Elias Sjögreen ([@eliassjogreen](https://github.com/eliassjogreen)) 36 | - Dean Srebnik ([@load1n9](https://github.com/load1n9)) 37 | - Skye ([@MierenManz](https://github.com/mierenmanz)) 38 | 39 | ### Contribution 40 | 41 | Pull request, issues and feedback are very welcome. Code style is formatted with 42 | `deno fmt` and commit messages are done following Conventional Commits spec. 43 | 44 | ### Licence 45 | 46 | Copyright 2021-2024, the denosaurs team. All rights reserved. MIT license. 47 | -------------------------------------------------------------------------------- /benchmarks/arrays.ts: -------------------------------------------------------------------------------- 1 | import { ArrayType, SizedArrayType, u32 } from "../mod.ts"; 2 | 3 | for (let i = 2; i < 10; i++) { 4 | const codec = new ArrayType(u32, 1 << i); 5 | const sizedCodec = new SizedArrayType(u32, 1 << i); 6 | const data = new Array(1 << i).fill(0).map((_, i) => i); 7 | const buff = new Uint32Array(1 << i).fill(0).map((_, i) => i); 8 | const dt = new DataView(buff.buffer); 9 | 10 | Deno.bench({ 11 | name: "Array (Read)", 12 | baseline: true, 13 | group: `read-${i}`, 14 | fn: () => { 15 | codec.readPacked(dt); 16 | }, 17 | }); 18 | 19 | Deno.bench({ 20 | name: "Sized Array (Read)", 21 | group: `read-${i}`, 22 | fn: () => { 23 | sizedCodec.readPacked(dt); 24 | }, 25 | }); 26 | Deno.bench({ 27 | name: "Array (Write)", 28 | baseline: true, 29 | group: `write-${i}`, 30 | fn: () => { 31 | codec.writePacked(data, dt); 32 | }, 33 | }); 34 | 35 | Deno.bench({ 36 | name: "Sized Array (Write)", 37 | group: `write-${i}`, 38 | fn: () => { 39 | sizedCodec.writePacked(data, dt); 40 | }, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /benchmarks/big_struct.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type InnerType, 3 | SizedStruct, 4 | Strings, 5 | Struct, 6 | u32, 7 | u8, 8 | } from "../mod.ts"; 9 | const innerDescriptor = { 10 | name: new Strings.FixedLengthString(11), 11 | hp: u8, 12 | damage: u8, 13 | shield: u32, 14 | }; 15 | 16 | const baseDescriptor = { 17 | handIndex: u8, 18 | fieldIndex: u8, 19 | }; 20 | 21 | const codec = new Struct({ 22 | ...baseDescriptor, 23 | card: new Struct(innerDescriptor), 24 | }); 25 | 26 | const sizedJITCodec = new SizedStruct({ 27 | ...baseDescriptor, 28 | card: new SizedStruct(innerDescriptor), 29 | }); 30 | 31 | const sizedCodec = new SizedStruct({ 32 | ...baseDescriptor, 33 | card: new SizedStruct(innerDescriptor, false), 34 | }, false); 35 | 36 | const data: InnerType = { 37 | handIndex: 255, 38 | fieldIndex: 255, 39 | card: { 40 | name: "InvalidCard", 41 | hp: 255, 42 | damage: 255, 43 | shield: 255, 44 | }, 45 | }; 46 | 47 | const ARRAY_BUFFER = new ArrayBuffer(20); 48 | const DATA_VIEW = new DataView(ARRAY_BUFFER); 49 | const BIN_VIEW = new Uint8Array(ARRAY_BUFFER, 2, 11); 50 | const DECODER = new TextDecoder(); 51 | const ENCODER = new TextEncoder(); 52 | 53 | Deno.bench("no-op", () => {}); 54 | 55 | Deno.bench({ 56 | name: "Struct (Read)", 57 | group: "read", 58 | fn: () => { 59 | codec.readPacked(DATA_VIEW); 60 | }, 61 | }); 62 | 63 | Deno.bench({ 64 | name: "SizedStruct JIT (Read)", 65 | group: "read", 66 | fn: () => { 67 | sizedJITCodec.readPacked(DATA_VIEW); 68 | }, 69 | }); 70 | 71 | Deno.bench({ 72 | name: "SizedStruct (Read)", 73 | group: "read", 74 | fn: () => { 75 | sizedCodec.readPacked(DATA_VIEW); 76 | }, 77 | }); 78 | 79 | Deno.bench({ 80 | name: "Primitives (Read)", 81 | group: "read", 82 | baseline: true, 83 | fn: () => { 84 | ({ 85 | handIndex: DATA_VIEW.getUint8(0), 86 | fieldIndex: DATA_VIEW.getUint8(1), 87 | card: { 88 | name: DECODER.decode(BIN_VIEW), 89 | hp: DATA_VIEW.getUint8(13), 90 | damage: DATA_VIEW.getUint8(14), 91 | shield: DATA_VIEW.getUint8(16), 92 | }, 93 | }); 94 | }, 95 | }); 96 | 97 | Deno.bench({ 98 | name: "Struct (Write)", 99 | group: "write", 100 | fn: () => { 101 | codec.writePacked(data, DATA_VIEW); 102 | }, 103 | }); 104 | 105 | Deno.bench({ 106 | name: "SizedStruct JIT (Write)", 107 | group: "write", 108 | fn: () => { 109 | sizedJITCodec.writePacked(data, DATA_VIEW); 110 | }, 111 | }); 112 | 113 | Deno.bench({ 114 | name: "SizedStruct (Write)", 115 | group: "write", 116 | fn: () => { 117 | sizedCodec.writePacked(data, DATA_VIEW); 118 | }, 119 | }); 120 | 121 | Deno.bench({ 122 | name: "Primitives (Write)", 123 | group: "write", 124 | fn: () => { 125 | DATA_VIEW.setUint8(0, data.handIndex); 126 | DATA_VIEW.setUint8(1, data.fieldIndex); 127 | ENCODER.encodeInto(data.card.name, BIN_VIEW); 128 | DATA_VIEW.setUint8(13, data.card.hp); 129 | DATA_VIEW.setUint8(14, data.card.damage); 130 | DATA_VIEW.setUint32(16, data.card.shield); 131 | }, 132 | }); 133 | -------------------------------------------------------------------------------- /benchmarks/popular_encodings.ts: -------------------------------------------------------------------------------- 1 | import { type InnerType, Strings, Struct, u32, u8 } from "../mod.ts"; 2 | import { 3 | decode as msgpackRead, 4 | encode as msgpackWrite, 5 | } from "jsr:@std/msgpack@0.218"; 6 | import { SizedStruct } from "../src/compound/sized_struct.ts"; 7 | 8 | const innerDescriptor = { 9 | name: new Strings.FixedLengthString(11), 10 | hp: u8, 11 | damage: u8, 12 | shield: u32, 13 | }; 14 | 15 | const baseDescriptor = { 16 | handIndex: u8, 17 | fieldIndex: u8, 18 | }; 19 | 20 | const descriptor = { 21 | ...baseDescriptor, 22 | card: new Struct(innerDescriptor), 23 | }; 24 | 25 | const sizedDescriptor = { 26 | ...baseDescriptor, 27 | card: new SizedStruct(innerDescriptor), 28 | }; 29 | 30 | const codec = new Struct(descriptor); 31 | const sizedCodec = new SizedStruct(sizedDescriptor); 32 | 33 | const data: InnerType = { 34 | handIndex: 255, 35 | fieldIndex: 255, 36 | card: { 37 | name: "InvalidCard", 38 | hp: 255, 39 | damage: 255, 40 | shield: 255, 41 | }, 42 | }; 43 | 44 | const jsonString = JSON.stringify(data); 45 | const msgPackBuff = msgpackWrite(data); 46 | 47 | const ARRAY_BUFFER = new ArrayBuffer(20); 48 | const DATA_VIEW = new DataView(ARRAY_BUFFER); 49 | 50 | Deno.bench("nop", () => {}); 51 | 52 | Deno.bench({ 53 | name: "JSON (Write)", 54 | group: "write", 55 | baseline: true, 56 | fn: () => { 57 | JSON.stringify(data); 58 | }, 59 | }); 60 | 61 | Deno.bench({ 62 | name: "JSON (Read)", 63 | group: "read", 64 | baseline: true, 65 | fn: () => { 66 | JSON.parse(jsonString); 67 | }, 68 | }); 69 | 70 | Deno.bench({ 71 | name: "JSON (Roundtrip)", 72 | group: "roundtrip", 73 | baseline: true, 74 | fn: () => { 75 | JSON.stringify(data); 76 | JSON.parse(jsonString); 77 | }, 78 | }); 79 | 80 | Deno.bench({ 81 | name: "Struct (Write)", 82 | group: "write", 83 | fn: () => { 84 | codec.writePacked(data, DATA_VIEW); 85 | }, 86 | }); 87 | 88 | Deno.bench({ 89 | name: "Struct (Read)", 90 | group: "read", 91 | fn: () => { 92 | codec.readPacked(DATA_VIEW); 93 | }, 94 | }); 95 | 96 | Deno.bench({ 97 | name: "Struct (Roundtrip)", 98 | group: "roundtrip", 99 | fn: () => { 100 | codec.writePacked(data, DATA_VIEW); 101 | codec.readPacked(DATA_VIEW); 102 | }, 103 | }); 104 | 105 | Deno.bench({ 106 | name: "SizedStruct (Write)", 107 | group: "write", 108 | fn: () => { 109 | sizedCodec.writePacked(data, DATA_VIEW); 110 | }, 111 | }); 112 | 113 | Deno.bench({ 114 | name: "SizedStruct (Read)", 115 | group: "read", 116 | fn: () => { 117 | sizedCodec.readPacked(DATA_VIEW); 118 | }, 119 | }); 120 | 121 | Deno.bench({ 122 | name: "SizedStruct (Roundtrip)", 123 | group: "roundtrip", 124 | fn: () => { 125 | sizedCodec.writePacked(data, DATA_VIEW); 126 | sizedCodec.readPacked(DATA_VIEW); 127 | }, 128 | }); 129 | 130 | Deno.bench({ 131 | name: "MsgPack (Write)", 132 | group: "write", 133 | fn: () => { 134 | msgpackWrite(data); 135 | }, 136 | }); 137 | 138 | Deno.bench({ 139 | name: "MsgPack (Read)", 140 | group: "read", 141 | fn: () => { 142 | msgpackRead(msgPackBuff); 143 | }, 144 | }); 145 | 146 | Deno.bench({ 147 | name: "MsgPack (Roundtrip)", 148 | group: "roundtrip", 149 | fn: () => { 150 | msgpackWrite(data); 151 | msgpackRead(msgPackBuff); 152 | }, 153 | }); 154 | -------------------------------------------------------------------------------- /benchmarks/string.ts: -------------------------------------------------------------------------------- 1 | import { Strings } from "../mod.ts"; 2 | 3 | const stringThing = new Strings.FixedLengthString(12); 4 | 5 | const ab = new TextEncoder().encode("Hello World!").buffer; 6 | const dt = new DataView(ab); 7 | 8 | Deno.bench("no-op", () => {}); 9 | 10 | Deno.bench({ 11 | name: "Read", 12 | fn: () => { 13 | stringThing.read(dt); 14 | }, 15 | }); 16 | 17 | Deno.bench({ 18 | name: "Write", 19 | fn: () => { 20 | stringThing.write("hello world!", dt); 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /benchmarks/struct.ts: -------------------------------------------------------------------------------- 1 | import { Struct, u32 } from "../mod.ts"; 2 | import { SizedStruct } from "../src/compound/sized_struct.ts"; 3 | 4 | const data = new DataView(new ArrayBuffer(8)); 5 | 6 | const object = { a: 123, b: 456 }; 7 | const struct = new Struct({ 8 | a: u32, 9 | b: u32, 10 | }); 11 | 12 | const sizedStruct = new SizedStruct({ 13 | a: u32, 14 | b: u32, 15 | }); 16 | 17 | Deno.bench("no-op", () => {}); 18 | 19 | Deno.bench({ 20 | baseline: true, 21 | name: "object", 22 | group: "read", 23 | fn: () => { 24 | object.a; 25 | object.b; 26 | }, 27 | }); 28 | 29 | Deno.bench({ 30 | name: "struct", 31 | group: "read", 32 | fn: () => { 33 | struct.read(data); 34 | }, 35 | }); 36 | 37 | Deno.bench({ 38 | name: "struct Sized", 39 | group: "read", 40 | fn: () => { 41 | sizedStruct.read(data); 42 | }, 43 | }); 44 | 45 | Deno.bench({ 46 | name: "DataView", 47 | group: "read", 48 | fn: () => { 49 | data.getUint32(0); 50 | data.getUint32(4); 51 | }, 52 | }); 53 | 54 | Deno.bench({ 55 | baseline: true, 56 | name: "object", 57 | group: "write", 58 | fn: () => { 59 | object.a = 0xffff; 60 | object.b = 0xffff; 61 | }, 62 | }); 63 | 64 | Deno.bench({ 65 | name: "struct", 66 | group: "write", 67 | fn: () => { 68 | struct.write({ 69 | a: 0xffff, 70 | b: 0xffff, 71 | }, data); 72 | }, 73 | }); 74 | 75 | Deno.bench({ 76 | name: "struct Sized", 77 | group: "write", 78 | fn: () => { 79 | sizedStruct.write({ 80 | a: 0xffff, 81 | b: 0xffff, 82 | }, data); 83 | }, 84 | }); 85 | 86 | Deno.bench({ 87 | name: "DataView", 88 | group: "write", 89 | fn: () => { 90 | data.setUint32(0, 0xffff); 91 | data.setUint32(4, 0xffff); 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /benchmarks/tuple.ts: -------------------------------------------------------------------------------- 1 | import { Tuple, u32 } from "../mod.ts"; 2 | 3 | const benchTuple = new Tuple([u32, u32]); 4 | const u32arr = new Uint32Array([2, 4]); 5 | const dt = new DataView(u32arr.buffer); 6 | 7 | Deno.bench("no-op", () => {}); 8 | 9 | Deno.bench({ 10 | name: "Read", 11 | fn: () => { 12 | benchTuple.read(dt); 13 | }, 14 | }); 15 | 16 | Deno.bench({ 17 | name: "Write", 18 | fn: () => { 19 | benchTuple.write([4, 2], dt); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@denosaurs/byte-type", 3 | "version": "0.4.1", 4 | "exports": "./mod.ts", 5 | "lock": false, 6 | "lint": { 7 | "rules": { 8 | "tags": ["jsr"], 9 | "include": ["verbatim-module-syntax", "no-inferrable-types"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/basic.ts: -------------------------------------------------------------------------------- 1 | import { Struct, u32, u8 } from "../src/mod.ts"; 2 | 3 | const buffer = new ArrayBuffer(8); 4 | const dt = new DataView(buffer); 5 | 6 | const o = new Struct({ "b": u8, "a": u32 }); 7 | 8 | o.write({ b: 8, a: 32 }, dt); 9 | console.log(o.read(dt)); 10 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/mod.ts"; 2 | -------------------------------------------------------------------------------- /src/array/array.ts: -------------------------------------------------------------------------------- 1 | import { type Options, UnsizedType } from "../types/mod.ts"; 2 | 3 | export class ArrayType extends UnsizedType { 4 | override readonly maxSize: number | null; 5 | 6 | constructor(readonly type: UnsizedType, readonly length: number) { 7 | super(type.byteAlignment); 8 | this.maxSize = type.maxSize ? type.maxSize * length : null; 9 | } 10 | 11 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): T[] { 12 | if (this.length === 0) return []; 13 | const result = new Array(this.length); 14 | const { type } = this; 15 | 16 | for (let i = 0; i < result.length; i++) { 17 | result[i] = type.readPacked(dt, options); 18 | // No need for the increment offset. This is handled by the `type.readPacked` function 19 | } 20 | 21 | return result; 22 | } 23 | 24 | override read(dt: DataView, options: Options = { byteOffset: 0 }): T[] { 25 | if (this.length === 0) return []; 26 | const result = new Array(this.length); 27 | const { type } = this; 28 | 29 | for (let i = 0; i < result.length; i++) { 30 | result[i] = type.read(dt, options); 31 | // No need for the increment offset. This is handled by the `type.read` function 32 | } 33 | 34 | return result; 35 | } 36 | 37 | writePacked( 38 | value: T[], 39 | dt: DataView, 40 | options: Options = { byteOffset: 0 }, 41 | ): void { 42 | if (value.length !== this.length) { 43 | throw new TypeError("T[].length !== ArrayType.length"); 44 | } 45 | 46 | const { type } = this; 47 | for (let i = 0; i < value.length; i++) { 48 | type.writePacked(value[i], dt, options); 49 | // No need for the increment offset. This is handled by the `type.writePacked` function 50 | } 51 | } 52 | 53 | override write( 54 | value: T[], 55 | dt: DataView, 56 | options: Options = { byteOffset: 0 }, 57 | ): void { 58 | if (value.length !== this.length) { 59 | throw new TypeError("T[].length !== ArrayType.length"); 60 | } 61 | 62 | const { type } = this; 63 | for (let i = 0; i < value.length; i++) { 64 | type.write(value[i], dt, options); 65 | // No need for the increment offset. This is handled by the `type.write` function 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/array/array_buffer.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | 3 | export class ArrayBufferType extends SizedType { 4 | constructor(byteSize: number, byteAlignment = 1) { 5 | super(byteSize, byteAlignment); 6 | } 7 | 8 | readPacked( 9 | dt: DataView, 10 | options: Options = { byteOffset: 0 }, 11 | ): ArrayBuffer { 12 | const resultAB = new ArrayBuffer(this.byteSize); 13 | const resultView = new Uint8Array(resultAB); 14 | 15 | resultView.set( 16 | new Uint8Array( 17 | dt.buffer, 18 | dt.byteOffset + options.byteOffset, 19 | this.byteSize, 20 | ), 21 | ); 22 | 23 | super.incrementOffset(options); 24 | 25 | return resultAB; 26 | } 27 | 28 | writePacked( 29 | value: ArrayBuffer, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | const view = new Uint8Array( 34 | dt.buffer, 35 | dt.byteOffset + options.byteOffset, 36 | this.byteSize, 37 | ); 38 | 39 | view.set(new Uint8Array(value)); 40 | 41 | super.incrementOffset(options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/array/array_buffer_test.ts: -------------------------------------------------------------------------------- 1 | import { ArrayBufferType } from "./array_buffer.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "ArrayBufferType", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(2); 8 | const dt = new DataView(ab); 9 | dt.setUint16(0, 0xFF); 10 | 11 | const type = new ArrayBufferType(2); 12 | 13 | await t.step("estimated size", () => { 14 | assertEquals(type.maxSize, 2); 15 | }); 16 | 17 | await t.step("Read", () => { 18 | const newAb = type.read(dt); 19 | assertEquals(new Uint8Array(newAb), new Uint8Array(ab)); 20 | }); 21 | 22 | await t.step("Write", () => { 23 | const newAb = new ArrayBuffer(2); 24 | type.write(newAb, dt); 25 | assertEquals(new Uint8Array(ab), new Uint8Array(newAb)); 26 | }); 27 | 28 | await t.step("OOB Read", () => { 29 | assertThrows(() => { 30 | type.read(dt, { byteOffset: 2 }); 31 | }, RangeError); 32 | }); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/array/array_test.ts: -------------------------------------------------------------------------------- 1 | import { u8 } from "../mod.ts"; 2 | import { ArrayType } from "./array.ts"; 3 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 4 | 5 | Deno.test({ 6 | name: "Array", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(2); 9 | const dt = new DataView(ab); 10 | const type = new ArrayType(u8, 2); 11 | 12 | await t.step("estimated size", () => { 13 | assertEquals(type.maxSize, 2); 14 | }); 15 | 16 | await t.step("Read", () => { 17 | dt.setUint16(0, 0xFFFF); 18 | const result = type.read(dt); 19 | assertEquals(result, [255, 255]); 20 | }); 21 | 22 | await t.step("Write", () => { 23 | type.write([127, 0], dt); 24 | assertEquals(dt.getUint16(0), 0x7F00); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 2 }); 30 | }, RangeError); 31 | }); 32 | 33 | await t.step("Inequal length", () => { 34 | assertThrows(() => { 35 | type.writePacked([1, 2, 3, 4], dt); 36 | }, TypeError); 37 | 38 | assertThrows(() => { 39 | type.write([1, 2, 3, 4], dt); 40 | }, TypeError); 41 | }); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/array/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./array_buffer.ts"; 2 | export * from "./array.ts"; 3 | export * from "./typed_array.ts"; 4 | export * from "./sized_array.ts"; 5 | -------------------------------------------------------------------------------- /src/array/sized_array.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { ArrayType } from "./array.ts"; 3 | 4 | export class SizedArrayType extends SizedType implements ArrayType { 5 | #inner: ArrayType; 6 | 7 | constructor(readonly type: SizedType, readonly length: number) { 8 | super(length * type.byteSize, type.byteAlignment); 9 | this.#inner = new ArrayType(type, length); 10 | } 11 | 12 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): T[] { 13 | return this.#inner.readPacked(dt, options); 14 | } 15 | 16 | override read(dt: DataView, options: Options = { byteOffset: 0 }): T[] { 17 | return this.#inner.read(dt, options); 18 | } 19 | 20 | writePacked( 21 | value: T[], 22 | dt: DataView, 23 | options: Options = { byteOffset: 0 }, 24 | ): void { 25 | this.#inner.writePacked(value, dt, options); 26 | } 27 | 28 | override write( 29 | value: T[], 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | this.#inner.write(value, dt, options); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/array/typed_array.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | 3 | type TypedArrays = 4 | | Uint8Array 5 | | Uint8ClampedArray 6 | | Int8Array 7 | | Uint16Array 8 | | Int16Array 9 | | Uint32Array 10 | | Int32Array 11 | | Float32Array 12 | | Float64Array 13 | | BigUint64Array 14 | | BigInt64Array; 15 | 16 | type TypedConstructors = T extends 17 | Uint8Array ? Uint8ArrayConstructor 18 | : T extends Uint8ClampedArray ? Uint8ClampedArrayConstructor 19 | : T extends Int8Array ? Int8ArrayConstructor 20 | : T extends Uint16Array ? Uint16ArrayConstructor 21 | : T extends Int16Array ? Int16ArrayConstructor 22 | : T extends Uint32Array ? Uint32ArrayConstructor 23 | : T extends Int32Array ? Int32ArrayConstructor 24 | : T extends Float32Array ? Float32ArrayConstructor 25 | : T extends Float64Array ? Float64ArrayConstructor 26 | : T extends BigUint64Array ? BigUint64ArrayConstructor 27 | : T extends BigInt64Array ? BigInt64ArrayConstructor 28 | : never; 29 | 30 | export class TypedArray extends SizedType { 31 | constructor( 32 | readonly arrayConstructor: TypedConstructors, 33 | readonly length: number, 34 | ) { 35 | super( 36 | length * arrayConstructor.BYTES_PER_ELEMENT, 37 | arrayConstructor.BYTES_PER_ELEMENT, 38 | ); 39 | } 40 | 41 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): T { 42 | const value = new this.arrayConstructor( 43 | // @ts-expect-error: 44 | dt.buffer, 45 | dt.byteOffset + options.byteOffset, 46 | this.length, 47 | ).slice() as T; 48 | 49 | super.incrementOffset(options); 50 | return value; 51 | } 52 | 53 | writePacked( 54 | value: T, 55 | dt: DataView, 56 | options: Options = { byteOffset: 0 }, 57 | ): void { 58 | const view = new this.arrayConstructor( 59 | // @ts-expect-error: 60 | dt.buffer, 61 | dt.byteOffset + options.byteOffset, 62 | this.length, 63 | ); 64 | view.set(value as unknown as (ArrayLike & ArrayLike)); 65 | super.incrementOffset(options); 66 | } 67 | } 68 | 69 | export interface TypedArrayTypeConstructor { 70 | new (length: number): TypedArray; 71 | } 72 | 73 | function createTypedArrayType>( 74 | constructor: E, 75 | ): TypedArrayTypeConstructor> { 76 | return class extends TypedArray> { 77 | constructor(length: number) { 78 | super(constructor, length); 79 | } 80 | }; 81 | } 82 | 83 | export const Uint8ArrayType: TypedArrayTypeConstructor = 84 | createTypedArrayType(Uint8Array); 85 | export const Uint8ClampedArrayType: TypedArrayTypeConstructor< 86 | Uint8ClampedArray 87 | > = createTypedArrayType(Uint8ClampedArray); 88 | export const Int8ArrayType: TypedArrayTypeConstructor = 89 | createTypedArrayType(Int8Array); 90 | export const Uint16ArrayType: TypedArrayTypeConstructor = 91 | createTypedArrayType(Uint16Array); 92 | export const Int16ArrayType: TypedArrayTypeConstructor = 93 | createTypedArrayType(Int16Array); 94 | export const Uint32ArrayType: TypedArrayTypeConstructor = 95 | createTypedArrayType(Uint32Array); 96 | export const Int32ArrayType: TypedArrayTypeConstructor = 97 | createTypedArrayType(Int32Array); 98 | export const Float32ArrayType: TypedArrayTypeConstructor = 99 | createTypedArrayType(Float32Array); 100 | export const Float64ArrayType: TypedArrayTypeConstructor = 101 | createTypedArrayType(Float64Array); 102 | export const BigUint64ArrayType: TypedArrayTypeConstructor = 103 | createTypedArrayType(BigUint64Array); 104 | export const BigInt64ArrayType: TypedArrayTypeConstructor = 105 | createTypedArrayType(BigInt64Array); 106 | -------------------------------------------------------------------------------- /src/array/typed_array_test.ts: -------------------------------------------------------------------------------- 1 | import { isLittleEndian } from "../util.ts"; 2 | import { Uint16ArrayType } from "./typed_array.ts"; 3 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 4 | 5 | Deno.test({ 6 | name: "TypedArray", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(2); 9 | const dt = new DataView(ab); 10 | const type = new Uint16ArrayType(1); 11 | 12 | await t.step("estimated size", () => { 13 | assertEquals(type.maxSize, 2); 14 | }); 15 | 16 | await t.step("Read", () => { 17 | dt.setUint16(0, 0xFFFF); 18 | const result = type.read(dt); 19 | assertEquals(result, Uint16Array.of(0xFFFF)); 20 | }); 21 | 22 | await t.step("Write", () => { 23 | type.write(Uint16Array.of(0x7F00), dt); 24 | assertEquals(dt.getUint16(0, isLittleEndian), 0x7F00); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 2 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/bitflags/_common.ts: -------------------------------------------------------------------------------- 1 | export type OutputRecord> = { 2 | [K in keyof T]: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /src/bitflags/bitflags16.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import type { OutputRecord } from "./_common.ts"; 3 | 4 | export class BitFlags16< 5 | I extends Record, 6 | O = OutputRecord, 7 | > extends SizedType { 8 | #recordEntries: Array<[string, number]>; 9 | 10 | constructor(record: I) { 11 | super(2, 2); 12 | this.#recordEntries = Object.entries(record); 13 | } 14 | 15 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): O { 16 | const returnObject: Record = {}; 17 | 18 | const byteBag = dt.getUint16(options.byteOffset); 19 | for (const { 0: key, 1: flag } of this.#recordEntries) { 20 | returnObject[key] = (byteBag & flag) === flag; 21 | } 22 | 23 | super.incrementOffset(options); 24 | 25 | return returnObject as O; 26 | } 27 | 28 | writePacked( 29 | value: O, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | let flags = 0; 34 | 35 | for (const { 0: key, 1: flagValue } of this.#recordEntries) { 36 | if (value[key as keyof O]) { 37 | flags |= flagValue; 38 | } 39 | } 40 | 41 | dt.setUint16(options.byteOffset, flags); 42 | super.incrementOffset(options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bitflags/bitflags16_test.ts: -------------------------------------------------------------------------------- 1 | import { BitFlags16 } from "./bitflags16.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "BitFlags16", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(2); 8 | const dt = new DataView(ab); 9 | const type = new BitFlags16({ 10 | one: 1, 11 | two: 2, 12 | }); 13 | 14 | await t.step("estimated size", () => { 15 | assertEquals(type.maxSize, 2); 16 | }); 17 | 18 | await t.step("Read", () => { 19 | dt.setUint8(1, 0b01); 20 | const result = type.read(dt); 21 | assertEquals(result, { one: true, two: false }); 22 | }); 23 | 24 | await t.step("Write", () => { 25 | type.write({ one: false, two: true }, dt); 26 | assertEquals(dt.getUint8(1), 0b10); 27 | }); 28 | 29 | await t.step("OOB Read", () => { 30 | assertThrows(() => { 31 | type.read(dt, { byteOffset: 3 }); 32 | }, RangeError); 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/bitflags/bitflags32.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import type { OutputRecord } from "./_common.ts"; 3 | 4 | export class BitFlags32< 5 | I extends Record, 6 | O = OutputRecord, 7 | > extends SizedType { 8 | #recordEntries: Array<[string, number]>; 9 | 10 | constructor(record: I) { 11 | super(4, 4); 12 | this.#recordEntries = Object.entries(record); 13 | } 14 | 15 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): O { 16 | const returnObject: Record = {}; 17 | 18 | const byteBag = dt.getUint32(options.byteOffset); 19 | for (const { 0: key, 1: flag } of this.#recordEntries) { 20 | returnObject[key] = (byteBag & flag) === flag; 21 | } 22 | 23 | super.incrementOffset(options); 24 | 25 | return returnObject as O; 26 | } 27 | 28 | writePacked( 29 | value: O, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | let flags = 0; 34 | 35 | for (const { 0: key, 1: flagValue } of this.#recordEntries) { 36 | if (value[key as keyof O]) { 37 | flags |= flagValue; 38 | } 39 | } 40 | 41 | dt.setUint32(options.byteOffset, flags); 42 | super.incrementOffset(options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bitflags/bitflags32_test.ts: -------------------------------------------------------------------------------- 1 | import { BitFlags32 } from "./bitflags32.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "BitFlags32", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(4); 8 | const dt = new DataView(ab); 9 | const type = new BitFlags32({ 10 | one: 1, 11 | two: 2, 12 | }); 13 | 14 | await t.step("estimated size", () => { 15 | assertEquals(type.maxSize, 4); 16 | }); 17 | 18 | await t.step("Read", () => { 19 | dt.setUint8(3, 0b01); 20 | const result = type.read(dt); 21 | assertEquals(result, { one: true, two: false }); 22 | }); 23 | 24 | await t.step("Write", () => { 25 | type.write({ one: false, two: true }, dt); 26 | assertEquals(dt.getUint8(3), 0b10); 27 | }); 28 | 29 | await t.step("OOB Read", () => { 30 | assertThrows(() => { 31 | type.read(dt, { byteOffset: 5 }); 32 | }, RangeError); 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/bitflags/bitflags64.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import type { OutputRecord } from "./_common.ts"; 3 | 4 | export class BitFlags64< 5 | I extends Record, 6 | O = OutputRecord, 7 | > extends SizedType { 8 | #recordEntries: Array<[string, bigint]>; 9 | 10 | constructor(record: I) { 11 | super(8, 8); 12 | this.#recordEntries = Object.entries(record); 13 | } 14 | 15 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): O { 16 | const returnObject: Record = {}; 17 | 18 | const byteBag = dt.getBigUint64(options.byteOffset); 19 | for (const { 0: key, 1: flag } of this.#recordEntries) { 20 | returnObject[key] = (byteBag & flag) === flag; 21 | } 22 | 23 | super.incrementOffset(options); 24 | 25 | return returnObject as O; 26 | } 27 | 28 | writePacked( 29 | value: O, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | let flags = 0n; 34 | 35 | for (const { 0: key, 1: flagValue } of this.#recordEntries) { 36 | if (value[key as keyof O]) { 37 | flags |= flagValue; 38 | } 39 | } 40 | 41 | dt.setBigUint64(options.byteOffset, flags); 42 | super.incrementOffset(options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bitflags/bitflags64_test.ts: -------------------------------------------------------------------------------- 1 | import { BitFlags64 } from "./bitflags64.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "BitFlags64", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = new BitFlags64({ 10 | one: 1n, 11 | two: 2n, 12 | }); 13 | 14 | await t.step("estimated size", () => { 15 | assertEquals(type.maxSize, 8); 16 | }); 17 | 18 | await t.step("Read", () => { 19 | dt.setUint8(7, 0b01); 20 | const result = type.read(dt); 21 | assertEquals(result, { one: true, two: false }); 22 | }); 23 | 24 | await t.step("Write", () => { 25 | type.write({ one: false, two: true }, dt); 26 | assertEquals(dt.getUint8(7), 0b10); 27 | }); 28 | 29 | await t.step("OOB Read", () => { 30 | assertThrows(() => { 31 | type.read(dt, { byteOffset: 9 }); 32 | }, RangeError); 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/bitflags/bitflags8.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import type { OutputRecord } from "./_common.ts"; 3 | 4 | export class BitFlags8< 5 | I extends Record, 6 | O = OutputRecord, 7 | > extends SizedType { 8 | #recordEntries: Array<[string, number]>; 9 | 10 | constructor(record: I) { 11 | super(1, 1); 12 | this.#recordEntries = Object.entries(record); 13 | } 14 | 15 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): O { 16 | const returnObject: Record = {}; 17 | 18 | const byteBag = dt.getUint8(options.byteOffset); 19 | for (const { 0: key, 1: flag } of this.#recordEntries) { 20 | returnObject[key] = (byteBag & flag) === flag; 21 | } 22 | 23 | super.incrementOffset(options); 24 | 25 | return returnObject as O; 26 | } 27 | 28 | writePacked( 29 | value: O, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | let flags = 0; 34 | 35 | for (const { 0: key, 1: flagValue } of this.#recordEntries) { 36 | if (value[key as keyof O]) { 37 | flags |= flagValue; 38 | } 39 | } 40 | 41 | dt.setUint8(options.byteOffset, flags); 42 | super.incrementOffset(options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bitflags/bitflags8_test.ts: -------------------------------------------------------------------------------- 1 | import { BitFlags8 } from "./bitflags8.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "BitFlags8", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(1); 8 | const dt = new DataView(ab); 9 | const type = new BitFlags8({ 10 | one: 1, 11 | two: 2, 12 | }); 13 | 14 | await t.step("estimated size", () => { 15 | assertEquals(type.maxSize, 1); 16 | }); 17 | 18 | await t.step("Read", () => { 19 | dt.setUint8(0, 0b01); 20 | const result = type.read(dt); 21 | assertEquals(result, { one: true, two: false }); 22 | }); 23 | 24 | await t.step("Write", () => { 25 | type.write({ one: false, two: true }, dt); 26 | assertEquals(dt.getUint8(0), 0b10); 27 | }); 28 | 29 | await t.step("OOB Read", () => { 30 | assertThrows(() => { 31 | type.read(dt, { byteOffset: 2 }); 32 | }, RangeError); 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/bitflags/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./bitflags8.ts"; 2 | export * from "./bitflags16.ts"; 3 | export * from "./bitflags32.ts"; 4 | export * from "./bitflags64.ts"; 5 | -------------------------------------------------------------------------------- /src/compound/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./struct.ts"; 2 | export * from "./tagged_union.ts"; 3 | export * from "./tuple.ts"; 4 | export * from "./sized_struct.ts"; 5 | -------------------------------------------------------------------------------- /src/compound/sized_struct.ts: -------------------------------------------------------------------------------- 1 | import { type InnerType, type Options, SizedType } from "../mod.ts"; 2 | import { alignmentOf, sizeOf } from "../util.ts"; 3 | import { Struct } from "./mod.ts"; 4 | 5 | type ReadFn = (dt: DataView, options: Options) => R; 6 | type WriteFn = (value: V, dt: DataView, options: Options) => void; 7 | 8 | const createRead = (key: string, method: string) => 9 | `"${key}": ${key}.${method}(dt, options)`; 10 | 11 | const createWrite = (key: string, method: string) => 12 | `${key}.${method}(value.${key}, dt, options);`; 13 | 14 | function createReadMethod( 15 | input: Record>, 16 | isPacked: boolean, 17 | ): ReadFn { 18 | const method = isPacked ? "readPacked" : "read"; 19 | const keys = Object.keys(input); 20 | 21 | const generatedCodec = keys.map((k) => createRead(k, method)).join(",\n"); 22 | const body = `const { ${keys} } = this;\nreturn { ${generatedCodec} }`; 23 | 24 | return Function("dt", "options", body).bind(input); 25 | } 26 | 27 | function createWriteMethod( 28 | input: Record>, 29 | isPacked: boolean, 30 | ): WriteFn { 31 | const method = isPacked ? "writePacked" : "write"; 32 | const keys = Object.keys(input); 33 | 34 | const generatedCodec = keys.map((k) => createWrite(k, method)).join("\n"); 35 | const body = `const { ${keys} } = this;\n${generatedCodec}`; 36 | 37 | return Function("value", "dt", "options", body).bind(input); 38 | } 39 | 40 | export class SizedStruct< 41 | T extends Record>, 42 | V extends { [K in keyof T]: InnerType } = { 43 | [K in keyof T]: InnerType; 44 | }, 45 | > // Omit here to make typescript shut up about 2720. Maybe a bug. 46 | extends SizedType 47 | implements Omit, "#record"> { 48 | #inner: Partial>; 49 | 50 | constructor(input: T, jitEnabled: boolean = true) { 51 | super(sizeOf(input), alignmentOf(input)); 52 | if (jitEnabled) { 53 | this.#inner = { 54 | read: createReadMethod(input, false), 55 | readPacked: createReadMethod(input, true), 56 | write: createWriteMethod(input, false), 57 | writePacked: createWriteMethod(input, true), 58 | }; 59 | } else { 60 | this.#inner = new Struct(input); 61 | } 62 | } 63 | 64 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): V { 65 | return this.#inner.readPacked!(dt, options); 66 | } 67 | 68 | override read(dt: DataView, options: Options = { byteOffset: 0 }): V { 69 | return this.#inner.read!(dt, options); 70 | } 71 | 72 | writePacked( 73 | value: V, 74 | dt: DataView, 75 | options: Options = { byteOffset: 0 }, 76 | ): void { 77 | this.#inner.writePacked!(value, dt, options); 78 | } 79 | 80 | override write( 81 | value: V, 82 | dt: DataView, 83 | options: Options = { byteOffset: 0 }, 84 | ): void { 85 | this.#inner.write!(value, dt, options); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/compound/sized_struct_test.ts: -------------------------------------------------------------------------------- 1 | import { u32le, u8 } from "../mod.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | import { SizedStruct } from "./mod.ts"; 4 | 5 | Deno.test({ 6 | name: "Sized Struct JIT", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(8); 9 | const dt = new DataView(ab); 10 | const type = new SizedStruct({ byte: u8, word: u32le }); 11 | 12 | await t.step("estimated size", () => { 13 | assertEquals(type.maxSize, 8); 14 | }); 15 | 16 | await t.step("Read", () => { 17 | dt.setUint8(0, 127); 18 | dt.setUint32(4, 255, true); 19 | const result = type.read(dt); 20 | assertEquals(result, { byte: 127, word: 255 }); 21 | }); 22 | 23 | await t.step("Read Packed", () => { 24 | dt.setUint8(0, 127); 25 | dt.setUint32(1, 255, true); 26 | const result = type.readPacked(dt); 27 | assertEquals(result, { byte: 127, word: 255 }); 28 | }); 29 | 30 | dt.setBigUint64(0, 0n); 31 | 32 | await t.step("Write", () => { 33 | type.write({ byte: 255, word: 127 }, dt); 34 | assertEquals(dt.getUint32(0, true), 255); 35 | assertEquals(dt.getUint32(4, true), 127); 36 | }); 37 | 38 | await t.step("Write Packed", () => { 39 | type.writePacked({ byte: 255, word: 127 }, dt); 40 | assertEquals(dt.getUint8(0), 255); 41 | assertEquals(dt.getUint32(1, true), 127); 42 | }); 43 | 44 | await t.step("OOB Read", () => { 45 | assertThrows(() => { 46 | type.read(dt, { byteOffset: 9 }); 47 | }, RangeError); 48 | }); 49 | }, 50 | }); 51 | 52 | Deno.test({ 53 | name: "Sized Struct NON-JIT", 54 | fn: async (t) => { 55 | const ab = new ArrayBuffer(8); 56 | const dt = new DataView(ab); 57 | const type = new SizedStruct({ byte: u8, word: u32le }, false); 58 | 59 | await t.step("Read", () => { 60 | dt.setUint8(0, 127); 61 | dt.setUint32(4, 255, true); 62 | const result = type.read(dt); 63 | assertEquals(result, { byte: 127, word: 255 }); 64 | }); 65 | 66 | await t.step("Read Packed", () => { 67 | dt.setUint8(0, 127); 68 | dt.setUint32(1, 255, true); 69 | const result = type.readPacked(dt); 70 | assertEquals(result, { byte: 127, word: 255 }); 71 | }); 72 | 73 | dt.setBigUint64(0, 0n); 74 | 75 | await t.step("Write", () => { 76 | type.write({ byte: 255, word: 127 }, dt); 77 | assertEquals(dt.getUint32(0, true), 255); 78 | assertEquals(dt.getUint32(4, true), 127); 79 | }); 80 | 81 | await t.step("Write Packed", () => { 82 | type.writePacked({ byte: 255, word: 127 }, dt); 83 | assertEquals(dt.getUint8(0), 255); 84 | assertEquals(dt.getUint32(1, true), 127); 85 | }); 86 | 87 | await t.step("OOB Read", () => { 88 | assertThrows(() => { 89 | type.read(dt, { byteOffset: 9 }); 90 | }, RangeError); 91 | }); 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /src/compound/struct.ts: -------------------------------------------------------------------------------- 1 | import { type InnerType, type Options, UnsizedType } from "../types/mod.ts"; 2 | import { align, alignmentOf } from "../util.ts"; 3 | 4 | export class Struct< 5 | T extends Record>, 6 | V extends { [K in keyof T]: InnerType } = { 7 | [K in keyof T]: InnerType; 8 | }, 9 | > extends UnsizedType { 10 | #record: Array<[string, UnsizedType]>; 11 | override readonly maxSize: number | null; 12 | 13 | constructor(input: T) { 14 | super(alignmentOf(input)); 15 | this.#record = Object.entries(input); 16 | this.maxSize = this.#record.every(([_, a]) => a.maxSize !== null) 17 | ? this.#record.reduce( 18 | (acc, [_, x]) => acc + align(Number(x.maxSize), this.byteAlignment), 19 | 0, 20 | ) 21 | : null; 22 | } 23 | 24 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): V { 25 | if (this.#record.length === 0) return {} as V; 26 | 27 | const result: Record = {}; 28 | 29 | for (let i = 0; i < this.#record.length; i++) { 30 | const { 0: key, 1: type } = this.#record[i]; 31 | result[key] = type.readPacked(dt, options); 32 | } 33 | 34 | return result as V; 35 | } 36 | 37 | override read(dt: DataView, options: Options = { byteOffset: 0 }): V { 38 | if (this.#record.length === 0) return {} as V; 39 | 40 | const result: Record = {}; 41 | 42 | for (let i = 0; i < this.#record.length; i++) { 43 | const { 0: key, 1: type } = this.#record[i]; 44 | result[key] = type.read(dt, options); 45 | } 46 | 47 | return result as V; 48 | } 49 | 50 | writePacked( 51 | value: V, 52 | dt: DataView, 53 | options: Options = { byteOffset: 0 }, 54 | ): void { 55 | if (this.#record.length === 0) return; 56 | 57 | for (let i = 0; i < this.#record.length; i++) { 58 | const { 0: key, 1: type } = this.#record[i]; 59 | type.writePacked(value[key], dt, options); 60 | } 61 | } 62 | 63 | override write( 64 | value: V, 65 | dt: DataView, 66 | options: Options = { byteOffset: 0 }, 67 | ): void { 68 | if (this.#record.length === 0) return; 69 | 70 | for (let i = 0; i < this.#record.length; i++) { 71 | const { 0: key, 1: type } = this.#record[i]; 72 | type.write(value[key], dt, options); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/compound/struct_test.ts: -------------------------------------------------------------------------------- 1 | import { Struct } from "./struct.ts"; 2 | import { u32le, u8 } from "../mod.ts"; 3 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 4 | 5 | Deno.test({ 6 | name: "Struct", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(8); 9 | const dt = new DataView(ab); 10 | const type = new Struct({ byte: u8, word: u32le }); 11 | 12 | await t.step("estimated size", () => { 13 | assertEquals(type.maxSize, 8); 14 | }); 15 | 16 | await t.step("Read", () => { 17 | dt.setUint8(0, 127); 18 | dt.setUint32(4, 255, true); 19 | const result = type.read(dt); 20 | assertEquals(result, { byte: 127, word: 255 }); 21 | }); 22 | 23 | await t.step("Read Packed", () => { 24 | dt.setUint8(0, 127); 25 | dt.setUint32(1, 255, true); 26 | const result = type.readPacked(dt); 27 | assertEquals(result, { byte: 127, word: 255 }); 28 | }); 29 | 30 | dt.setBigUint64(0, 0n); 31 | 32 | await t.step("Write", () => { 33 | type.write({ byte: 255, word: 127 }, dt); 34 | assertEquals(dt.getUint32(0, true), 255); 35 | assertEquals(dt.getUint32(4, true), 127); 36 | }); 37 | 38 | await t.step("Write Packed", () => { 39 | type.writePacked({ byte: 255, word: 127 }, dt); 40 | assertEquals(dt.getUint8(0), 255); 41 | assertEquals(dt.getUint32(1, true), 127); 42 | }); 43 | 44 | await t.step("OOB Read", () => { 45 | assertThrows(() => { 46 | type.read(dt, { byteOffset: 9 }); 47 | }, RangeError); 48 | }); 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/compound/tagged_union.ts: -------------------------------------------------------------------------------- 1 | import { u8 } from "../primitives/mod.ts"; 2 | import { 3 | type InnerType, 4 | type Options, 5 | UnsizedType, 6 | type ValueOf, 7 | } from "../types/mod.ts"; 8 | import { align, alignmentOf } from "../util.ts"; 9 | 10 | type FindDiscriminant = (variant: V) => D; 11 | 12 | type Keys = Exclude; 13 | 14 | /** Union for when the inner type's don't write their own discriminant */ 15 | export class TaggedUnion< 16 | T extends Record>, 17 | V extends ValueOf<{ [K in keyof T]: InnerType }> = ValueOf< 18 | { [K in keyof T]: InnerType } 19 | >, 20 | > extends UnsizedType { 21 | #record: T; 22 | #variantFinder: FindDiscriminant>; 23 | #discriminant: UnsizedType; 24 | 25 | override maxSize: number | null; 26 | 27 | constructor( 28 | input: T, 29 | variantFinder: FindDiscriminant>, 30 | discriminant: Keys extends string ? UnsizedType : never, 31 | ); 32 | 33 | constructor( 34 | input: T, 35 | variantFinder: FindDiscriminant>, 36 | discriminant?: Keys extends number ? UnsizedType : never, 37 | ); 38 | 39 | constructor( 40 | input: T, 41 | variantFinder: FindDiscriminant>, 42 | discriminant: UnsizedType = u8, 43 | ) { 44 | super(alignmentOf(input)); 45 | this.#record = input; 46 | this.#variantFinder = variantFinder; 47 | this.#discriminant = discriminant; 48 | this.maxSize = Object.values(input).some((x) => x.maxSize === null) 49 | ? null 50 | : Object.values(input).reduce( 51 | (acc: number, x) => 52 | Math.max(acc, align(x.maxSize as number, this.byteAlignment)), 53 | 0, 54 | ); 55 | } 56 | 57 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): V { 58 | const discriminant = this.#discriminant.readPacked(dt, options); 59 | const codec = this.#record[discriminant]; 60 | if (!codec) throw new TypeError("Unknown discriminant"); 61 | return codec.readPacked(dt, options) as V; 62 | } 63 | 64 | override read(dt: DataView, options: Options = { byteOffset: 0 }): V { 65 | const discriminant = this.#discriminant.read(dt, options); 66 | super.alignOffset(options); 67 | const codec = this.#record[discriminant]; 68 | if (!codec) throw new TypeError("Unknown discriminant"); 69 | return codec.read(dt, options) as V; 70 | } 71 | 72 | writePacked( 73 | variant: V, 74 | dt: DataView, 75 | options: Options = { byteOffset: 0 }, 76 | ): void { 77 | const discriminant = this.#variantFinder(variant); 78 | const codec = this.#record[discriminant]; 79 | if (!codec) throw new TypeError("Unknown discriminant"); 80 | 81 | this.#discriminant.writePacked(discriminant, dt, options); 82 | codec.writePacked(variant, dt, options); 83 | } 84 | 85 | override write( 86 | variant: V, 87 | dt: DataView, 88 | options: Options = { byteOffset: 0 }, 89 | ): void { 90 | const discriminant = this.#variantFinder(variant); 91 | const codec = this.#record[discriminant]; 92 | if (!codec) throw new TypeError("Unknown discriminant"); 93 | 94 | this.#discriminant.write(discriminant, dt, options); 95 | codec.write(variant, dt, options); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/compound/tagged_union_test.ts: -------------------------------------------------------------------------------- 1 | import { TaggedUnion } from "./tagged_union.ts"; 2 | import { cstring, u32le, u8 } from "../mod.ts"; 3 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 4 | 5 | Deno.test({ 6 | name: "Tagged Union", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(8); 9 | const dt = new DataView(ab); 10 | const type = new TaggedUnion({ 11 | 0: u32le, 12 | 1: u8, 13 | 2: u8, 14 | }, (a) => a === 32 ? 0 : 1); 15 | 16 | await t.step("estimate size", () => { 17 | const type = new TaggedUnion({ 18 | 0: u32le, 19 | 1: u8, 20 | 2: u8, 21 | }, (a) => a === 32 ? 0 : 1); 22 | assertEquals(type.maxSize, 4); 23 | 24 | const unknownSizedType = new TaggedUnion({ 25 | 0: cstring, 26 | }, () => 0); 27 | 28 | assertEquals(unknownSizedType.maxSize, null); 29 | }); 30 | 31 | await t.step("Read", () => { 32 | dt.setUint8(0, 1); 33 | dt.setUint8(1, 11); 34 | dt.setUint8(2, 22); 35 | dt.setUint8(4, 33); 36 | const result = type.read(dt); 37 | assertEquals(result, 33); 38 | }); 39 | 40 | await t.step("Read Packed", () => { 41 | dt.setUint8(0, 1); 42 | dt.setUint8(1, 11); 43 | dt.setUint8(2, 22); 44 | dt.setUint8(4, 33); 45 | const result = type.readPacked(dt); 46 | assertEquals(result, 11); 47 | }); 48 | 49 | dt.setBigUint64(0, 0n); 50 | 51 | await t.step("Write", () => { 52 | type.write(32, dt); 53 | assertEquals(new Uint32Array(ab), Uint32Array.of(0, 32)); 54 | }); 55 | 56 | dt.setBigUint64(0, 0n); 57 | 58 | await t.step("Write Packed", () => { 59 | type.writePacked(32, dt); 60 | assertEquals( 61 | new Uint8Array(ab).subarray(0, 5), 62 | Uint8Array.of(0, 32, 0, 0, 0), 63 | ); 64 | }); 65 | 66 | await t.step("OOB Read", () => { 67 | assertThrows(() => { 68 | type.read(dt, { byteOffset: 9 }); 69 | }, RangeError); 70 | }); 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/compound/tuple.ts: -------------------------------------------------------------------------------- 1 | import { type InnerType, type Options, UnsizedType } from "../types/mod.ts"; 2 | import { align, alignmentOf } from "../util.ts"; 3 | 4 | export class Tuple< 5 | T extends [...UnsizedType[]], 6 | V extends [...unknown[]] = { [I in keyof T]: InnerType }, 7 | > extends UnsizedType { 8 | #tupleTypes: T; 9 | #length: number; 10 | override maxSize: number | null; 11 | 12 | constructor(types: T) { 13 | super(alignmentOf(types)); 14 | this.#tupleTypes = types; 15 | this.#length = types.length; 16 | 17 | this.maxSize = types.every((a) => a.maxSize !== null) 18 | ? types.reduce( 19 | (acc, x) => acc + align(Number(x.maxSize), this.byteAlignment), 20 | 0, 21 | ) 22 | : null; 23 | } 24 | 25 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): V { 26 | if (this.#length === 0) return [] as unknown as V; 27 | const result: unknown[] = new Array(this.#length); 28 | 29 | const tupleTypes = this.#tupleTypes; 30 | for (let i = 0; i < result.length; i++) { 31 | result[i] = tupleTypes[i].readPacked(dt, options); 32 | } 33 | 34 | return result as V; 35 | } 36 | 37 | override read(dt: DataView, options: Options = { byteOffset: 0 }): V { 38 | if (this.#length === 0) return [] as unknown as V; 39 | const result: unknown[] = new Array(this.#length); 40 | 41 | const tupleTypes = this.#tupleTypes; 42 | for (let i = 0; i < result.length; i++) { 43 | result[i] = tupleTypes[i].read(dt, options); 44 | } 45 | 46 | return result as V; 47 | } 48 | 49 | writePacked( 50 | value: V, 51 | dt: DataView, 52 | options: Options = { byteOffset: 0 }, 53 | ): void { 54 | if (value.length !== this.#length) { 55 | throw new TypeError( 56 | `value V has more entries than expected\nExpected:${this.#length} but got ${value.length}`, 57 | ); 58 | } 59 | if (value.length === 0) return; 60 | 61 | const tupleTypes = this.#tupleTypes; 62 | for (let i = 0; i < value.length; i++) { 63 | tupleTypes[i].writePacked(value[i], dt, options); 64 | } 65 | } 66 | 67 | override write( 68 | value: V, 69 | dt: DataView, 70 | options: Options = { byteOffset: 0 }, 71 | ): void { 72 | if (value.length !== this.#length) { 73 | throw new TypeError( 74 | `value V has more entries than expected\nExpected:${this.#length} but got ${value.length}`, 75 | ); 76 | } 77 | if (value.length === 0) return; 78 | 79 | const tupleTypes = this.#tupleTypes; 80 | for (let i = 0; i < value.length; i++) { 81 | tupleTypes[i].write(value[i], dt, options); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/compound/tuple_test.ts: -------------------------------------------------------------------------------- 1 | import { Tuple } from "./tuple.ts"; 2 | import { u32le, u8 } from "../mod.ts"; 3 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 4 | 5 | Deno.test({ 6 | name: "Tuple", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(8); 9 | const dt = new DataView(ab); 10 | const type = new Tuple([u8, u32le]); 11 | 12 | await t.step("estimated size", () => { 13 | assertEquals(type.maxSize, 8); 14 | }); 15 | 16 | await t.step("Read", () => { 17 | dt.setUint32(0, 127, true); 18 | dt.setUint32(4, 255, true); 19 | const result = type.read(dt); 20 | assertEquals(result, [127, 255]); 21 | }); 22 | 23 | await t.step("Read Packed", () => { 24 | dt.setUint32(0, 255, true); 25 | dt.setUint32(1, 127, true); 26 | const result = type.readPacked(dt); 27 | assertEquals(result, [255, 127]); 28 | }); 29 | 30 | dt.setBigUint64(0, 0n); 31 | 32 | await t.step("Write", () => { 33 | type.write([127, 255], dt); 34 | assertEquals(dt.getUint32(0, true), 127); 35 | assertEquals(dt.getUint32(4, true), 255); 36 | }); 37 | 38 | dt.setBigUint64(0, 0n); 39 | 40 | await t.step("Write Packed", () => { 41 | type.write([255, 127], dt); 42 | assertEquals(dt.getUint32(0, true), 255); 43 | assertEquals(dt.getUint32(4, true), 127); 44 | }); 45 | 46 | await t.step("OOB Read", () => { 47 | assertThrows(() => { 48 | type.read(dt, { byteOffset: 9 }); 49 | }, RangeError); 50 | }); 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /src/compound/union.ts: -------------------------------------------------------------------------------- 1 | import { u8 } from "../primitives/mod.ts"; 2 | import { 3 | type InnerType, 4 | type Options, 5 | UnsizedType, 6 | type ValueOf, 7 | } from "../types/mod.ts"; 8 | import { align, alignmentOf } from "../util.ts"; 9 | 10 | type FindDiscriminant = (variant: V) => D; 11 | 12 | type Keys = Exclude; 13 | 14 | /** Union for when the inner type's do write their own discriminant */ 15 | export class Union< 16 | T extends Record>, 17 | V extends ValueOf<{ [K in keyof T]: InnerType }>, 18 | > extends UnsizedType { 19 | #record: T; 20 | #variantFinder: FindDiscriminant>; 21 | #discriminant = u8; 22 | 23 | override maxSize: number | null; 24 | 25 | constructor( 26 | input: T, 27 | variantFinder: FindDiscriminant>, 28 | ) { 29 | super(alignmentOf(input)); 30 | this.#record = input; 31 | this.#variantFinder = variantFinder; 32 | 33 | this.maxSize = Object.values(input).some((x) => x.maxSize === null) 34 | ? null 35 | : Object.values(input).reduce( 36 | (acc: number, x) => 37 | Math.max(acc, align(x.maxSize as number, this.byteAlignment)), 38 | 0, 39 | ); 40 | } 41 | 42 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): V { 43 | const discriminant = this.#discriminant.readPacked(dt, { 44 | byteOffset: options.byteOffset, 45 | }); 46 | const codec = this.#record[discriminant]; 47 | if (!codec) throw new TypeError("Unknown discriminant"); 48 | return codec.readPacked(dt, options) as V; 49 | } 50 | 51 | override read(dt: DataView, options: Options = { byteOffset: 0 }): V { 52 | const discriminant = this.#discriminant.read(dt, { 53 | byteOffset: options.byteOffset, 54 | }); 55 | const codec = this.#record[discriminant]; 56 | if (!codec) throw new TypeError("Unknown discriminant"); 57 | return codec.readPacked(dt, options) as V; 58 | } 59 | 60 | writePacked( 61 | variant: V, 62 | dt: DataView, 63 | options: Options = { byteOffset: 0 }, 64 | ): void { 65 | const discriminant = this.#variantFinder(variant); 66 | const codec = this.#record[discriminant]; 67 | if (!codec) throw new TypeError("Unknown discriminant"); 68 | codec.writePacked(variant, dt, options); 69 | } 70 | 71 | override write( 72 | variant: V, 73 | dt: DataView, 74 | options: Options = { byteOffset: 0 }, 75 | ): void { 76 | const discriminant = this.#variantFinder(variant); 77 | const codec = this.#record[discriminant]; 78 | if (!codec) throw new TypeError("Unknown discriminant"); 79 | codec.write(variant, dt, options); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/compound/union_test.ts: -------------------------------------------------------------------------------- 1 | import { cstring, u32le, u8 } from "../mod.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | import { Union } from "./union.ts"; 4 | 5 | Deno.test({ 6 | name: "Union", 7 | fn: async (t) => { 8 | const ab = new ArrayBuffer(8); 9 | const dt = new DataView(ab); 10 | const type = new Union({ 11 | 0: u32le, 12 | 1: u8, 13 | 2: u8, 14 | }, (a) => a === 32 ? 0 : 1); 15 | 16 | await t.step("estimate size", () => { 17 | const type = new Union({ 18 | 0: u32le, 19 | 1: u8, 20 | 2: u8, 21 | }, (a) => a === 32 ? 0 : 1); 22 | assertEquals(type.maxSize, 4); 23 | 24 | const unknownSizedType = new Union({ 25 | 0: cstring, 26 | }, () => 0); 27 | 28 | assertEquals(unknownSizedType.maxSize, null); 29 | }); 30 | 31 | await t.step("Read", () => { 32 | dt.setUint8(0, 1); 33 | dt.setUint8(1, 11); 34 | dt.setUint8(2, 22); 35 | dt.setUint8(4, 33); 36 | const result = type.read(dt); 37 | assertEquals(result, 1); 38 | }); 39 | 40 | await t.step("Read Packed", () => { 41 | dt.setUint8(0, 1); 42 | dt.setUint8(1, 11); 43 | dt.setUint8(2, 22); 44 | dt.setUint8(4, 33); 45 | const result = type.readPacked(dt); 46 | assertEquals(result, 1); 47 | }); 48 | 49 | dt.setBigUint64(0, 0n); 50 | 51 | await t.step("Write", () => { 52 | type.write(32, dt); 53 | assertEquals(new Uint32Array(ab), Uint32Array.of(32, 0)); 54 | }); 55 | 56 | dt.setBigUint64(0, 0n); 57 | 58 | await t.step("Write Packed", () => { 59 | type.writePacked(32, dt); 60 | assertEquals( 61 | new Uint8Array(ab).subarray(0, 5), 62 | Uint8Array.of(32, 0, 0, 0, 0), 63 | ); 64 | }); 65 | 66 | await t.step("OOB Read", () => { 67 | assertThrows(() => { 68 | type.read(dt, { byteOffset: 9 }); 69 | }, RangeError); 70 | }); 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/meta_types.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from "./mod.ts"; 2 | import { SizedType } from "./types/mod.ts"; 3 | 4 | export class Offset extends SizedType { 5 | override readonly maxSize: number | null; 6 | 7 | constructor(byteSize: number) { 8 | // Magic trick for flooring 9 | super(byteSize | 0, 1); 10 | this.maxSize = 0; 11 | } 12 | 13 | override readPacked( 14 | dt: DataView, 15 | options: Options = { byteOffset: 0 }, 16 | ): null { 17 | if ( 18 | this.byteSize + options.byteOffset < 0 || 19 | this.byteSize + options.byteOffset >= dt.byteLength 20 | ) { 21 | throw new RangeError("Read goes out of bound."); 22 | } 23 | 24 | super.incrementOffset(options); 25 | return null; 26 | } 27 | 28 | override writePacked( 29 | _value: null, 30 | dt: DataView, 31 | options: Options = { byteOffset: 0 }, 32 | ): void { 33 | if ( 34 | this.byteSize + options.byteOffset < 0 || 35 | this.byteSize + options.byteOffset >= dt.byteLength 36 | ) { 37 | throw new RangeError("Write goes out of bound."); 38 | } 39 | 40 | super.incrementOffset(options); 41 | } 42 | } 43 | 44 | export class Skip extends Offset { 45 | constructor(byteSize: number) { 46 | super(Math.abs(byteSize)); 47 | } 48 | } 49 | 50 | export class Reverse extends Offset { 51 | constructor(byteSize: number) { 52 | super(-Math.abs(byteSize)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/meta_types_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "../test_deps.ts"; 2 | import { Offset, Reverse, Skip, Struct, u32 } from "./mod.ts"; 3 | 4 | Deno.test({ 5 | name: "Offset (meta)", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = new Struct({ 10 | junk: new Offset(3), 11 | version: u32, 12 | }); 13 | 14 | await t.step("Read", () => { 15 | dt.setUint32(4, 32, true); 16 | const result = type.read(dt); 17 | assertEquals(result, { junk: null, version: 32 }); 18 | }); 19 | 20 | await t.step("Read Packed", () => { 21 | dt.setUint32(3, 32, true); 22 | const result = type.readPacked(dt); 23 | assertEquals(result, { junk: null, version: 32 }); 24 | }); 25 | 26 | await t.step("Write", () => { 27 | // Make sure value doesn't get written over. 28 | dt.setUint32(0, 12, true); 29 | dt.setUint32(4, 0, true); 30 | type.write({ junk: null, version: 1 }, dt); 31 | 32 | assertEquals(dt.getUint32(0, true), 12); 33 | assertEquals(dt.getUint32(4, true), 1); 34 | }); 35 | 36 | await t.step("Write Packed", () => { 37 | // Make sure value doesn't get written over. 38 | dt.setUint16(0, 12, true); 39 | dt.setUint8(2, 12); 40 | dt.setUint32(3, 0, true); 41 | type.writePacked({ junk: null, version: 1 }, dt); 42 | 43 | assertEquals(dt.getUint16(0, true), 12); 44 | assertEquals(dt.getUint8(2), 12); 45 | assertEquals(dt.getUint32(3, true), 1); 46 | }); 47 | 48 | await t.step("OOB Read", () => { 49 | assertThrows(() => { 50 | new Offset(3).read(dt, { byteOffset: 9 }); 51 | }, RangeError); 52 | }); 53 | }, 54 | }); 55 | 56 | Deno.test({ 57 | name: "Skip (meta)", 58 | fn: async (t) => { 59 | const ab = new ArrayBuffer(8); 60 | const dt = new DataView(ab); 61 | const type = new Struct({ 62 | junk: new Skip(3), 63 | version: u32, 64 | }); 65 | 66 | await t.step("Read", () => { 67 | dt.setUint32(4, 32, true); 68 | const result = type.read(dt); 69 | assertEquals(result, { junk: null, version: 32 }); 70 | }); 71 | 72 | await t.step("Read Packed", () => { 73 | dt.setUint32(3, 32, true); 74 | const result = type.readPacked(dt); 75 | assertEquals(result, { junk: null, version: 32 }); 76 | }); 77 | 78 | await t.step("Write", () => { 79 | // Make sure value doesn't get written over. 80 | dt.setUint32(0, 12, true); 81 | dt.setUint32(4, 0, true); 82 | type.write({ junk: null, version: 1 }, dt); 83 | 84 | assertEquals(dt.getUint32(0, true), 12); 85 | assertEquals(dt.getUint32(4, true), 1); 86 | }); 87 | 88 | await t.step("Write Packed", () => { 89 | // Make sure value doesn't get written over. 90 | dt.setUint16(0, 12, true); 91 | dt.setUint8(2, 12); 92 | dt.setUint32(3, 0, true); 93 | type.writePacked({ junk: null, version: 1 }, dt); 94 | 95 | assertEquals(dt.getUint16(0, true), 12); 96 | assertEquals(dt.getUint8(2), 12); 97 | assertEquals(dt.getUint32(3, true), 1); 98 | }); 99 | 100 | await t.step("OOB Read", () => { 101 | assertThrows(() => { 102 | new Skip(3).read(dt, { byteOffset: 9 }); 103 | }, RangeError); 104 | }); 105 | }, 106 | }); 107 | 108 | Deno.test({ 109 | name: "Reverse (meta)", 110 | fn: async (t) => { 111 | const ab = new ArrayBuffer(8); 112 | const dt = new DataView(ab); 113 | const type = new Struct({ 114 | $meta: new Reverse(4), 115 | version: u32, 116 | }); 117 | 118 | await t.step("Read", () => { 119 | dt.setUint32(0, 32, true); 120 | const result = type.read(dt, { byteOffset: 4 }); 121 | assertEquals(result, { 122 | version: 32, 123 | $meta: null, 124 | }); 125 | }); 126 | 127 | await t.step("Read Packed", () => { 128 | dt.setUint32(0, 32, true); 129 | const result = type.readPacked(dt, { byteOffset: 4 }); 130 | assertEquals(result, { 131 | version: 32, 132 | $meta: null, 133 | }); 134 | }); 135 | 136 | await t.step("Write", () => { 137 | dt.setUint32(0, 0, true); 138 | type.write({ $meta: null, version: 2 }, dt, { byteOffset: 4 }); 139 | 140 | assertEquals(dt.getUint32(0, true), 2); 141 | assertEquals(dt.getUint32(4, true), 0); 142 | }); 143 | 144 | await t.step("Write Packed", () => { 145 | dt.setUint32(0, 0, true); 146 | type.writePacked({ $meta: null, version: 2 }, dt, { byteOffset: 4 }); 147 | 148 | assertEquals(dt.getUint32(0, true), 2); 149 | assertEquals(dt.getUint32(4, true), 0); 150 | }); 151 | 152 | await t.step("OOB Read", () => { 153 | assertThrows(() => { 154 | new Reverse(10).read(dt); 155 | }); 156 | }); 157 | }, 158 | }); 159 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/mod.ts"; 2 | export * from "./util.ts"; 3 | 4 | export * from "./array/mod.ts"; 5 | export * as Arrays from "./array/mod.ts"; 6 | 7 | export * from "./bitflags/mod.ts"; 8 | export * as Bitflags from "./bitflags/mod.ts"; 9 | 10 | export * from "./primitives/mod.ts"; 11 | export * as Primitives from "./primitives/mod.ts"; 12 | 13 | export * from "./string/mod.ts"; 14 | export * as Strings from "./string/mod.ts"; 15 | 16 | export * from "./varint/mod.ts"; 17 | export * as VarInts from "./varint/mod.ts"; 18 | 19 | export * from "./compound/mod.ts"; 20 | export * as Compounds from "./compound/mod.ts"; 21 | 22 | export * from "./meta_types.ts"; 23 | -------------------------------------------------------------------------------- /src/non-standard-numbers/big_numbers.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class U128 extends SizedType { 5 | constructor( 6 | readonly littleEndian: boolean = isLittleEndian, 7 | /** 8 | * Clang, GCC and rust 1.78 (with LLVM 18) say 16 byte alignment. 9 | * 10 | * Older rust versions (or rust with older LLVM versions) say 8 byte alignment. 11 | * 12 | * Due to compatiblity reason we allow 8 byte alignment with these older versions. 13 | * 14 | * But by default it uses the now correct 16 byte alignment. 15 | * 16 | * See [Rust's blogpost](https://blog.rust-lang.org/2024/03/30/i128-layout-update.html) for more details 17 | */ 18 | alignment: 8 | 16 = 16, 19 | ) { 20 | super(16, alignment); 21 | } 22 | 23 | override readPacked( 24 | dt: DataView, 25 | options: Options = { byteOffset: 0 }, 26 | ): bigint { 27 | const partOne = dt.getBigUint64(options.byteOffset, this.littleEndian); 28 | const partTwo = dt.getBigUint64(options.byteOffset + 8, this.littleEndian); 29 | super.incrementOffset(options); 30 | // deno-fmt-ignore 31 | return this.littleEndian 32 | ? (partTwo << 64n) | partOne 33 | : (partOne << 64n) | partTwo; 34 | } 35 | 36 | override writePacked( 37 | value: bigint, 38 | dt: DataView, 39 | options: Options = { byteOffset: 0 }, 40 | ): void { 41 | const hi = value >> 64n; 42 | dt.setBigUint64( 43 | options.byteOffset, 44 | this.littleEndian ? hi : value, 45 | this.littleEndian, 46 | ); 47 | dt.setBigUint64( 48 | options.byteOffset + 8, 49 | this.littleEndian ? value : hi, 50 | this.littleEndian, 51 | ); 52 | 53 | super.incrementOffset(options); 54 | } 55 | } 56 | 57 | export class I128 extends SizedType { 58 | constructor( 59 | readonly littleEndian: boolean = isLittleEndian, 60 | /** 61 | * Clang, GCC and rust 1.78 (with LLVM 18) say 16 byte alignment. 62 | * 63 | * Older rust versions (or rust with older LLVM versions) say 8 byte alignment. 64 | * 65 | * Due to compatiblity reason we allow 8 byte alignment with these older versions. 66 | * 67 | * But by default it uses the now correct 16 byte alignment. 68 | * 69 | * See [Rust's blogpost](https://blog.rust-lang.org/2024/03/30/i128-layout-update.html) for more details 70 | */ 71 | alignment: 8 | 16 = 16, 72 | ) { 73 | super(16, alignment); 74 | } 75 | 76 | override readPacked( 77 | dt: DataView, 78 | options: Options = { byteOffset: 0 }, 79 | ): bigint { 80 | const partOne = dt.getBigInt64(options.byteOffset, this.littleEndian); 81 | const partTwo = dt.getBigInt64(options.byteOffset + 8, this.littleEndian); 82 | super.incrementOffset(options); 83 | // deno-fmt-ignore 84 | return this.littleEndian 85 | ? (partTwo << 64n) | partOne 86 | : (partOne << 64n) | partTwo; 87 | } 88 | 89 | override writePacked( 90 | value: bigint, 91 | dt: DataView, 92 | options: Options = { byteOffset: 0 }, 93 | ): void { 94 | const hi = value >> 64n; 95 | dt.setBigInt64( 96 | options.byteOffset, 97 | this.littleEndian ? hi : value, 98 | this.littleEndian, 99 | ); 100 | dt.setBigInt64( 101 | options.byteOffset + 8, 102 | this.littleEndian ? value : hi, 103 | this.littleEndian, 104 | ); 105 | 106 | super.incrementOffset(options); 107 | } 108 | } 109 | 110 | export const u128le: U128 = new U128(true); 111 | export const u128be: U128 = new U128(false); 112 | export const u128: U128 = new U128(); 113 | 114 | export const i128le: I128 = new I128(true); 115 | export const i128be: I128 = new I128(false); 116 | export const i128: I128 = new I128(); 117 | -------------------------------------------------------------------------------- /src/non-standard-numbers/big_numbers_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 2 | import { i128be, i128le, u128be, u128le } from "./big_numbers.ts"; 3 | 4 | Deno.test("u128", async (t) => { 5 | const buff = new ArrayBuffer(16); 6 | const dt = new DataView(buff); 7 | const value = 12n; 8 | 9 | await t.step("estimate size", () => { 10 | assertEquals(u128be.maxSize, 16); 11 | }); 12 | await t.step("read", () => { 13 | // Little endian 14 | const lo = value & 0xffffffffffffffffn; 15 | const hi = value >> 64n; 16 | dt.setBigUint64(0, lo, true); 17 | dt.setBigUint64(8, hi, true); 18 | assertEquals(value, u128le.read(dt)); 19 | // Big endian 20 | dt.setBigUint64(0, hi, false); 21 | dt.setBigUint64(8, lo, false); 22 | assertEquals(value, u128be.read(dt)); 23 | }); 24 | 25 | await t.step("write", () => { 26 | // Little endian 27 | u128le.write(value, dt); 28 | let lo = dt.getBigInt64(0, true); 29 | let hi = dt.getBigInt64(8, true); 30 | assertEquals(value, (lo << 64n) | hi); 31 | // Big endian 32 | u128be.write(value, dt); 33 | lo = dt.getBigInt64(8, false); 34 | hi = dt.getBigInt64(0, false); 35 | assertEquals(value, (lo << 64n) | hi); 36 | }); 37 | 38 | await t.step("OOB Read", () => { 39 | assertThrows(() => { 40 | u128le.read(dt, { byteOffset: 9 }); 41 | }, RangeError); 42 | }); 43 | }); 44 | 45 | Deno.test("i128", async (t) => { 46 | const buff = new ArrayBuffer(16); 47 | const dt = new DataView(buff); 48 | const value = 12n; 49 | 50 | await t.step("estimate size", () => { 51 | assertEquals(i128be.maxSize, 16); 52 | }); 53 | await t.step("read", () => { 54 | // Little endian 55 | const lo = value & 0xffffffffffffffffn; 56 | const hi = value >> 64n; 57 | dt.setBigUint64(0, lo, true); 58 | dt.setBigUint64(8, hi, true); 59 | assertEquals(value, i128le.read(dt)); 60 | // Big endian 61 | dt.setBigUint64(0, hi, false); 62 | dt.setBigUint64(8, lo, false); 63 | assertEquals(value, i128be.read(dt)); 64 | }); 65 | 66 | await t.step("write", () => { 67 | // Little endian 68 | i128le.write(value, dt); 69 | let lo = dt.getBigInt64(0, true); 70 | let hi = dt.getBigInt64(8, true); 71 | assertEquals(value, (lo << 64n) | hi); 72 | // Big endian 73 | i128be.write(value, dt); 74 | lo = dt.getBigInt64(8, false); 75 | hi = dt.getBigInt64(0, false); 76 | assertEquals(value, (lo << 64n) | hi); 77 | }); 78 | 79 | await t.step("OOB Read", () => { 80 | assertThrows(() => { 81 | i128le.read(dt, { byteOffset: 9 }); 82 | }, RangeError); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/non-standard-numbers/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./small_numbers.ts"; 2 | export * from "./big_numbers.ts"; 3 | -------------------------------------------------------------------------------- /src/non-standard-numbers/small_numbers.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../mod.ts"; 2 | 3 | type U2Number = 0 | 1 | 2 | 3; 4 | type U4Number = U2Number | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15; 5 | 6 | export class U2 extends SizedType { 7 | constructor() { 8 | super(1, 1); 9 | } 10 | 11 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): U2Number { 12 | const v = dt.getUint8(options.byteOffset) & 0b11; 13 | super.incrementOffset(options); 14 | return v as U2Number; 15 | } 16 | 17 | writePacked( 18 | value: U2Number, 19 | dt: DataView, 20 | options: Options = { byteOffset: 0 }, 21 | ): void { 22 | dt.setUint8(options.byteOffset, value & 0b11); 23 | super.incrementOffset(options); 24 | } 25 | } 26 | 27 | export class U4 extends SizedType { 28 | constructor() { 29 | super(1, 1); 30 | } 31 | 32 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): U4Number { 33 | const v = dt.getUint8(options.byteOffset) & 0b1111; 34 | super.incrementOffset(options); 35 | return v as U4Number; 36 | } 37 | 38 | writePacked( 39 | value: U4Number, 40 | dt: DataView, 41 | options: Options = { byteOffset: 0 }, 42 | ): void { 43 | dt.setUint8(options.byteOffset, value & 0b1111); 44 | super.incrementOffset(options); 45 | } 46 | } 47 | 48 | export const u2: U2 = new U2(); 49 | export const u4: U4 = new U4(); 50 | -------------------------------------------------------------------------------- /src/primitives/bool.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | 3 | export class Bool extends SizedType { 4 | constructor() { 5 | super(1, 1); 6 | } 7 | 8 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): boolean { 9 | const value = !!dt.getInt8(options.byteOffset); 10 | super.incrementOffset(options); 11 | return value; 12 | } 13 | 14 | writePacked( 15 | value: boolean, 16 | dt: DataView, 17 | options: Options = { byteOffset: 0 }, 18 | ): void { 19 | dt.setInt8(options.byteOffset, Number(value)); 20 | super.incrementOffset(options); 21 | } 22 | } 23 | 24 | export const bool: Bool = new Bool(); 25 | -------------------------------------------------------------------------------- /src/primitives/bool_test.ts: -------------------------------------------------------------------------------- 1 | import { bool } from "./bool.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "Boolean", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = bool; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 1); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setUint8(0, 1); 16 | const result = type.read(dt); 17 | assertEquals(result, true); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(false, dt); 24 | assertEquals(dt.getUint8(0), 0); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/f32.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class F32 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(4, 4); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getFloat32(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setFloat32(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const f32le: F32 = new F32(true); 26 | export const f32be: F32 = new F32(false); 27 | export const f32: F32 = new F32(); 28 | -------------------------------------------------------------------------------- /src/primitives/f32_test.ts: -------------------------------------------------------------------------------- 1 | import { f32le } from "./f32.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "F32", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = f32le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 4); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setFloat32(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getFloat32(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/f64.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class F64 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(8, 8); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getFloat64(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setFloat64(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const f64le: F64 = new F64(true); 26 | export const f64be: F64 = new F64(false); 27 | export const f64: F64 = new F64(); 28 | -------------------------------------------------------------------------------- /src/primitives/f64_test.ts: -------------------------------------------------------------------------------- 1 | import { f64le } from "./f64.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "F64", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = f64le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 8); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setFloat64(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getFloat64(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/i16.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class I16 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(2, 2); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getInt16(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setInt16(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const i16le: I16 = new I16(true); 26 | export const i16be: I16 = new I16(false); 27 | export const i16: I16 = new I16(); 28 | -------------------------------------------------------------------------------- /src/primitives/i16_test.ts: -------------------------------------------------------------------------------- 1 | import { i16le } from "./i16.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "I16", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = i16le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 2); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setInt16(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getInt16(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/i32.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class I32 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(4, 4); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getInt32(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setInt32(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const i32le: I32 = new I32(true); 26 | export const i32be: I32 = new I32(false); 27 | export const i32: I32 = new I32(); 28 | -------------------------------------------------------------------------------- /src/primitives/i32_test.ts: -------------------------------------------------------------------------------- 1 | import { i32le } from "./i32.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "I32", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = i32le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 4); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setInt32(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getInt32(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/i64.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class I64 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(8, 8); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): bigint { 10 | const value = dt.getBigInt64(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: bigint, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setBigInt64(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const i64le: I64 = new I64(true); 26 | export const i64be: I64 = new I64(false); 27 | export const i64: I64 = new I64(); 28 | -------------------------------------------------------------------------------- /src/primitives/i64_test.ts: -------------------------------------------------------------------------------- 1 | import { i64le } from "./i64.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "I64", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = i64le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 8); 13 | }); 14 | 15 | await t.step("Read", () => { 16 | dt.setBigInt64(0, 12n, true); 17 | const result = type.read(dt); 18 | assertEquals(result, 12n); 19 | }); 20 | 21 | dt.setBigUint64(0, 0n); 22 | 23 | await t.step("Write", () => { 24 | type.write(10n, dt); 25 | assertEquals(dt.getBigInt64(0, true), 10n); 26 | }); 27 | 28 | await t.step("OOB Read", () => { 29 | assertThrows(() => { 30 | type.read(dt, { byteOffset: 9 }); 31 | }, RangeError); 32 | }); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/primitives/i8.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | 3 | export class I8 extends SizedType { 4 | constructor() { 5 | super(1, 1); 6 | } 7 | 8 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 9 | const value = dt.getInt8(options.byteOffset); 10 | super.incrementOffset(options); 11 | return value; 12 | } 13 | 14 | writePacked( 15 | value: number, 16 | dt: DataView, 17 | options: Options = { byteOffset: 0 }, 18 | ): void { 19 | dt.setInt8(options.byteOffset, value); 20 | super.incrementOffset(options); 21 | } 22 | } 23 | 24 | export const i8: I8 = new I8(); 25 | -------------------------------------------------------------------------------- /src/primitives/i8_test.ts: -------------------------------------------------------------------------------- 1 | import { i8 } from "./i8.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "I8", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = i8; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 1); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setInt8(0, 12); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getInt8(0), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./bool.ts"; 2 | export * from "./f32.ts"; 3 | export * from "./f64.ts"; 4 | export * from "./i8.ts"; 5 | export * from "./i16.ts"; 6 | export * from "./i32.ts"; 7 | export * from "./i64.ts"; 8 | export * from "./u8.ts"; 9 | export * from "./u16.ts"; 10 | export * from "./u32.ts"; 11 | export * from "./u64.ts"; 12 | -------------------------------------------------------------------------------- /src/primitives/u16.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class U16 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(2, 2); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getUint16(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setUint16(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const u16le: U16 = new U16(true); 26 | export const u16be: U16 = new U16(false); 27 | export const u16: U16 = new U16(); 28 | -------------------------------------------------------------------------------- /src/primitives/u16_test.ts: -------------------------------------------------------------------------------- 1 | import { u16le } from "./u16.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "U16", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = u16le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 2); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setUint16(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getUint16(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/u32.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class U32 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(4, 4); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 10 | const value = dt.getUint32(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: number, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setUint32(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const u32le: U32 = new U32(true); 26 | export const u32be: U32 = new U32(false); 27 | export const u32: U32 = new U32(); 28 | -------------------------------------------------------------------------------- /src/primitives/u32_test.ts: -------------------------------------------------------------------------------- 1 | import { u32le } from "./u32.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "U32", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = u32le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 4); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setUint32(0, 12, true); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getUint32(0, true), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/primitives/u64.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { isLittleEndian } from "../util.ts"; 3 | 4 | export class U64 extends SizedType { 5 | constructor(readonly littleEndian: boolean = isLittleEndian) { 6 | super(8, 8); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): bigint { 10 | const value = dt.getBigUint64(options.byteOffset, this.littleEndian); 11 | super.incrementOffset(options); 12 | return value; 13 | } 14 | 15 | writePacked( 16 | value: bigint, 17 | dt: DataView, 18 | options: Options = { byteOffset: 0 }, 19 | ): void { 20 | dt.setBigUint64(options.byteOffset, value, this.littleEndian); 21 | super.incrementOffset(options); 22 | } 23 | } 24 | 25 | export const u64le: U64 = new U64(true); 26 | export const u64be: U64 = new U64(false); 27 | export const u64: U64 = new U64(); 28 | -------------------------------------------------------------------------------- /src/primitives/u64_test.ts: -------------------------------------------------------------------------------- 1 | import { u64le } from "./u64.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "U64", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = u64le; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 8); 13 | }); 14 | 15 | await t.step("Read", () => { 16 | dt.setBigUint64(0, 12n, true); 17 | const result = type.read(dt); 18 | assertEquals(result, 12n); 19 | }); 20 | 21 | dt.setBigUint64(0, 0n); 22 | 23 | await t.step("Write", () => { 24 | type.write(10n, dt); 25 | assertEquals(dt.getBigUint64(0, true), 10n); 26 | }); 27 | 28 | await t.step("OOB Read", () => { 29 | assertThrows(() => { 30 | type.read(dt, { byteOffset: 9 }); 31 | }, RangeError); 32 | }); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/primitives/u8.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | 3 | export class U8 extends SizedType { 4 | constructor() { 5 | super(1, 1); 6 | } 7 | 8 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 9 | const value = dt.getUint8(options.byteOffset); 10 | super.incrementOffset(options); 11 | return value; 12 | } 13 | 14 | writePacked( 15 | value: number, 16 | dt: DataView, 17 | options: Options = { byteOffset: 0 }, 18 | ): void { 19 | dt.setUint8(options.byteOffset, value); 20 | super.incrementOffset(options); 21 | } 22 | } 23 | 24 | export const u8: U8 = new U8(); 25 | -------------------------------------------------------------------------------- /src/primitives/u8_test.ts: -------------------------------------------------------------------------------- 1 | import { u8 } from "./u8.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "U8", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = u8; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, 1); 13 | }); 14 | await t.step("Read", () => { 15 | dt.setUint8(0, 12); 16 | const result = type.read(dt); 17 | assertEquals(result, 12); 18 | }); 19 | 20 | dt.setBigUint64(0, 0n); 21 | 22 | await t.step("Write", () => { 23 | type.write(10, dt); 24 | assertEquals(dt.getInt8(0), 10); 25 | }); 26 | 27 | await t.step("OOB Read", () => { 28 | assertThrows(() => { 29 | type.read(dt, { byteOffset: 9 }); 30 | }, RangeError); 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/string/_common.ts: -------------------------------------------------------------------------------- 1 | export const TEXT_ENCODER = new TextEncoder(); 2 | export const TEXT_DECODER = new TextDecoder(); 3 | -------------------------------------------------------------------------------- /src/string/cstring.ts: -------------------------------------------------------------------------------- 1 | import { type Options, UnsizedType } from "../types/mod.ts"; 2 | import { TEXT_DECODER, TEXT_ENCODER } from "./_common.ts"; 3 | 4 | export class CString extends UnsizedType { 5 | constructor() { 6 | super(1); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): string { 10 | const view = new Uint8Array(dt.buffer, dt.byteOffset + options.byteOffset); 11 | let i = 0; 12 | while (view[i] !== 0x00) { 13 | i++; 14 | } 15 | 16 | const value = TEXT_DECODER.decode(view.subarray(0, i)); 17 | // increment offset by `i + 1` because of the `null` at the end of the string 18 | super.incrementOffset(options, i + 1); 19 | return value; 20 | } 21 | 22 | /** `value` should not be a cstring. `\0` get's appended automatically */ 23 | writePacked( 24 | value: string, 25 | dt: DataView, 26 | options: Options = { byteOffset: 0 }, 27 | ): void { 28 | const view = new Uint8Array(dt.buffer, dt.byteOffset + options.byteOffset); 29 | 30 | const paddedValue = value + "\0"; 31 | 32 | TEXT_ENCODER.encodeInto(paddedValue, view); 33 | 34 | super.incrementOffset(options, paddedValue.length); 35 | } 36 | } 37 | 38 | export const cstring: CString = new CString(); 39 | -------------------------------------------------------------------------------- /src/string/cstring_test.ts: -------------------------------------------------------------------------------- 1 | import { cstring } from "./cstring.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | Deno.test({ 5 | name: "Cstring", 6 | fn: async (t) => { 7 | const ab = new ArrayBuffer(8); 8 | const dt = new DataView(ab); 9 | const type = cstring; 10 | 11 | await t.step("estimate size", () => { 12 | assertEquals(type.maxSize, null); 13 | }); 14 | 15 | await t.step("Read", () => { 16 | new TextEncoder().encodeInto("Hello", new Uint8Array(ab)); 17 | const result = type.read(dt); 18 | assertEquals(result, "Hello"); 19 | }); 20 | 21 | dt.setBigUint64(0, 0n); 22 | 23 | await t.step("Write", () => { 24 | type.write("World!", dt); 25 | assertEquals( 26 | new TextDecoder().decode(new Uint8Array(ab, 0, 6)), 27 | "World!", 28 | ); 29 | }); 30 | 31 | await t.step("OOB Read", () => { 32 | assertThrows(() => { 33 | type.read(dt, { byteOffset: 9 }); 34 | }, RangeError); 35 | }); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/string/fixed_length.ts: -------------------------------------------------------------------------------- 1 | import { type Options, SizedType } from "../types/mod.ts"; 2 | import { TEXT_DECODER, TEXT_ENCODER } from "./_common.ts"; 3 | 4 | export class FixedLengthString extends SizedType { 5 | constructor(length: number) { 6 | super(length, 1); 7 | } 8 | 9 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): string { 10 | const view = new Uint8Array( 11 | dt.buffer, 12 | dt.byteOffset + options.byteOffset, 13 | this.byteSize, 14 | ); 15 | const value = TEXT_DECODER.decode(view); 16 | super.incrementOffset(options); 17 | return value; 18 | } 19 | 20 | /** if `value.length` is bigger than the allowed length it will be truncated */ 21 | writePacked( 22 | value: string, 23 | dt: DataView, 24 | options: Options = { byteOffset: 0 }, 25 | ): void { 26 | const view = new Uint8Array( 27 | dt.buffer, 28 | dt.byteOffset + options.byteOffset, 29 | this.byteSize, 30 | ); 31 | 32 | TEXT_ENCODER.encodeInto(value, view); 33 | super.incrementOffset(options); 34 | } 35 | } 36 | 37 | export const asciiChar: FixedLengthString = new FixedLengthString(1); 38 | export const utf8Char: FixedLengthString = new FixedLengthString(4); 39 | -------------------------------------------------------------------------------- /src/string/fixed_length_test.ts: -------------------------------------------------------------------------------- 1 | import { asciiChar } from "./fixed_length.ts"; 2 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 3 | 4 | const decoder = new TextDecoder(); 5 | const encoder = new TextEncoder(); 6 | 7 | Deno.test({ 8 | name: "Fixed Length", 9 | fn: async (t) => { 10 | const ab = new ArrayBuffer(8); 11 | const dt = new DataView(ab); 12 | const view = new Uint8Array(ab); 13 | const type = asciiChar; 14 | 15 | await t.step("estimated size", () => { 16 | assertEquals(type.maxSize, 1); 17 | }); 18 | 19 | await t.step("Read", () => { 20 | encoder.encodeInto("H", view); 21 | const result = type.read(dt); 22 | assertEquals(result, "H"); 23 | }); 24 | 25 | dt.setBigUint64(0, 0n); 26 | 27 | await t.step("Write", () => { 28 | type.write("W", dt); 29 | assertEquals(decoder.decode(view.subarray(0, 1)), "W"); 30 | }); 31 | 32 | await t.step("OOB Read", () => { 33 | assertThrows(() => { 34 | type.read(dt, { byteOffset: 9 }); 35 | }, RangeError); 36 | }); 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/string/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./cstring.ts"; 2 | export * from "./fixed_length.ts"; 3 | -------------------------------------------------------------------------------- /src/string/prefixed_length.ts: -------------------------------------------------------------------------------- 1 | import { u8, UnsizedType } from "../mod.ts"; 2 | import type { Options } from "../types/mod.ts"; 3 | import { TEXT_DECODER, TEXT_ENCODER } from "./_common.ts"; 4 | 5 | export class PrefixedString extends UnsizedType { 6 | #prefixCodec: UnsizedType; 7 | 8 | constructor(prefixCodec: UnsizedType = u8) { 9 | super(1); 10 | this.#prefixCodec = prefixCodec; 11 | } 12 | 13 | writePacked( 14 | value: string, 15 | dt: DataView, 16 | options: Options = { byteOffset: 0 }, 17 | ): void { 18 | this.#prefixCodec.writePacked(value.length, dt, options); 19 | 20 | const view = new Uint8Array( 21 | dt.buffer, 22 | dt.byteOffset + options.byteOffset, 23 | value.length, 24 | ); 25 | 26 | TEXT_ENCODER.encodeInto(value, view); 27 | super.incrementOffset(options, value.length); 28 | } 29 | 30 | override write( 31 | value: string, 32 | dt: DataView, 33 | options: Options = { byteOffset: 0 }, 34 | ): void { 35 | this.#prefixCodec.write(value.length, dt, options); 36 | super.alignOffset(options); 37 | 38 | const view = new Uint8Array( 39 | dt.buffer, 40 | dt.byteLength + options.byteOffset, 41 | value.length, 42 | ); 43 | 44 | TEXT_ENCODER.encodeInto(value, view); 45 | super.incrementOffset(options, value.length); 46 | } 47 | 48 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): string { 49 | const length = this.#prefixCodec.readPacked(dt, options); 50 | const view = new Uint8Array( 51 | dt.buffer, 52 | dt.byteOffset + options.byteOffset, 53 | length, 54 | ); 55 | 56 | super.incrementOffset(options, length); 57 | return TEXT_DECODER.decode(view); 58 | } 59 | 60 | override read(dt: DataView, options: Options = { byteOffset: 0 }): string { 61 | const length = this.#prefixCodec.read(dt, options); 62 | super.alignOffset(options); 63 | 64 | const view = new Uint8Array( 65 | dt.buffer, 66 | dt.byteOffset + options.byteOffset, 67 | length, 68 | ); 69 | 70 | super.incrementOffset(options, length); 71 | return TEXT_DECODER.decode(view); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | import type { Unsized } from "./unsized.ts"; 2 | 3 | export interface Options { 4 | byteOffset: number; 5 | } 6 | 7 | /** Extract the inner value of a codec */ 8 | export type InnerType = T extends Unsized ? I : never; 9 | export type ValueOf = T[keyof T]; 10 | -------------------------------------------------------------------------------- /src/types/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./common.ts"; 2 | export { SizedType } from "./sized.ts"; 3 | export { UnsizedType } from "./unsized.ts"; 4 | -------------------------------------------------------------------------------- /src/types/sized.ts: -------------------------------------------------------------------------------- 1 | import { type Unsized, UnsizedType } from "./unsized.ts"; 2 | import type { Options } from "./common.ts"; 3 | import { align } from "../util.ts"; 4 | 5 | interface Sized extends Unsized { 6 | readonly byteSize: number; 7 | } 8 | 9 | /** 10 | * `SizedType` is one of the two base classes for implementing a codec. 11 | * 12 | * It is recommended to use this class if you know the size of your type ahead of time. 13 | * In future released this may be used for certain optimizations 14 | */ 15 | export abstract class SizedType extends UnsizedType implements Sized { 16 | override readonly maxSize: number | null; 17 | 18 | constructor(readonly byteSize: number, byteAlignment: number = 1) { 19 | super(byteAlignment); 20 | this.maxSize = align(byteSize, byteAlignment); 21 | } 22 | 23 | /** Increments offset by `this.byteSize` */ 24 | protected override incrementOffset(options: Options): void { 25 | super.incrementOffset(options, this.byteSize); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/types/unsized.ts: -------------------------------------------------------------------------------- 1 | import { align } from "../util.ts"; 2 | import type { Options } from "./common.ts"; 3 | 4 | export interface Unsized { 5 | readonly byteAlignment: number; 6 | readonly maxSize: number | null; 7 | 8 | readPacked(dt: DataView, options?: Options): T; 9 | writePacked(value: T, dt: DataView, options?: Options): void; 10 | } 11 | 12 | /** 13 | * `UnsizedType` is one of the two base classes for implementing a codec. 14 | * 15 | * This is the most common used class for when you do not know the size of your struct. 16 | */ 17 | export abstract class UnsizedType implements Unsized { 18 | readonly maxSize: null | number = null; 19 | 20 | constructor(readonly byteAlignment: number) {} 21 | 22 | /** 23 | * Read a value from the provided buffer while consuming as little bytes as possible. 24 | * This method is used for data over the network or when reading memory that may not be aligned 25 | * 26 | * ### Implementors be aware! 27 | * - This method is the base functionality of `Unsized.read()` if that method is not overridden. 28 | * - This method does not automatically offset or align the `Options.byteOffset`. You need to do this yourself. 29 | */ 30 | abstract readPacked(dt: DataView, options?: Options): T; 31 | 32 | /** 33 | * write a value into the provided buffer while consuming as little bytes as possible. 34 | * This method is used for data over the network or when writing memory that may not be aligned 35 | * 36 | * ### Implementors be aware! 37 | * - This method is the base functionality of `Unsized.write()` if it's not implemented 38 | * - This method does not automatically offset or align the `Options.byteOffset`. You need to do this yourself 39 | */ 40 | abstract writePacked(value: T, dt: DataView, options?: Options): void; 41 | 42 | /** 43 | * Read a aligned value from the provided buffer and optional byte offset 44 | * 45 | * ### Implementors be aware 46 | * - This is a function that is automatically implemented but may not be correct all the time. 47 | * - This function only works in a vacuum. This means that it will only work on single types. 48 | * - Composable types may not give the correct result and need to be written from scratch. 49 | */ 50 | read(dt: DataView, options: Options = { byteOffset: 0 }): T { 51 | this.alignOffset(options); 52 | return this.readPacked(dt, options); 53 | } 54 | 55 | /** 56 | * Write a value into the provided buffer at a optional offset that is automatically aligned 57 | * 58 | * ### Implementors be aware 59 | * - This is a function that is automatically implemented but may not be correct all the time. 60 | * - This function only works in a vacuum. This means that it will only work on single types. 61 | * - Composable types may not give the correct result and need to be written from scratch. 62 | */ 63 | write(value: T, dt: DataView, options: Options = { byteOffset: 0 }): void { 64 | this.alignOffset(options); 65 | this.writePacked(value, dt, options); 66 | } 67 | 68 | /** Align the offset of `Options.byteOffset` to the nearest integer divisable by `UnsizedType.byteAlignment` */ 69 | protected alignOffset(options: Options) { 70 | options.byteOffset = align(options.byteOffset, this.byteAlignment); 71 | } 72 | 73 | /** Increment offset by the provided `byteSize` */ 74 | protected incrementOffset(options: Options, byteSize: number) { 75 | options.byteOffset += byteSize; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import type { SizedType, UnsizedType } from "./mod.ts"; 2 | 3 | /** 4 | * The endianess of your machine, true if little endian and false if big endian. 5 | */ 6 | export const isLittleEndian: boolean = (() => { 7 | const buffer = new ArrayBuffer(2); 8 | new DataView(buffer).setUint16(0, 256, true); 9 | return new Uint16Array(buffer)[0] === 256; 10 | })(); 11 | 12 | /** Align the value `unaligned` to the first integer that is divisible by `alignment` */ 13 | export const align = (unaligned: number, alignment: number): number => 14 | (unaligned + alignment - 1) & ~(alignment - 1); 15 | 16 | type ArrayOrRecord = Array | Record; 17 | 18 | /** Find and returns the biggest alignment of a record/array of types */ 19 | export const alignmentOf = ( 20 | input: ArrayOrRecord>, 21 | ): number => 22 | Object.values(input) 23 | .reduce((acc, x) => Math.max(acc, x.byteAlignment), 0); 24 | /** 25 | * Find and returns the biggest alignment of a record/array of types 26 | * @deprecated 27 | */ 28 | export const getBiggestAlignment = alignmentOf; 29 | 30 | /** Find and return the unaligned size of a record/array of types */ 31 | export const sizeOf = ( 32 | input: ArrayOrRecord>, 33 | ): number => 34 | Object.values(input) 35 | .reduce((acc, x) => acc + x.byteSize, 0); 36 | 37 | /** 38 | * Find and return the unaligned size of a record/array of types 39 | * @deprecated 40 | */ 41 | export const calculateTotalSize = sizeOf; 42 | -------------------------------------------------------------------------------- /src/varint/_common.ts: -------------------------------------------------------------------------------- 1 | export const SEGMENT_BITS = 0b01111111; 2 | export const CONTINUE_BIT = 0b10000000; 3 | -------------------------------------------------------------------------------- /src/varint/i32_leb128.ts: -------------------------------------------------------------------------------- 1 | import { type Options, UnsizedType } from "../types/mod.ts"; 2 | import { CONTINUE_BIT, SEGMENT_BITS } from "./_common.ts"; 3 | 4 | export class I32Leb128 extends UnsizedType { 5 | override maxSize = 5; 6 | 7 | constructor() { 8 | super(1); 9 | } 10 | 11 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): number { 12 | let value = 0, position = 0; 13 | while (true) { 14 | const currentByte = dt.getInt8(options.byteOffset); 15 | value |= (currentByte & SEGMENT_BITS) << position; 16 | 17 | if ((currentByte & CONTINUE_BIT) === 0) break; 18 | 19 | position += 7; 20 | options.byteOffset++; 21 | 22 | if (position >= 32) { 23 | throw new RangeError("I32LEB128 cannot exceed 32 bits in length"); 24 | } 25 | } 26 | 27 | return value; 28 | } 29 | 30 | writePacked( 31 | value: number, 32 | dt: DataView, 33 | options: Options = { byteOffset: 0 }, 34 | ): void { 35 | while (true) { 36 | if ((value & ~SEGMENT_BITS) === 0) { 37 | dt.setUint8(options.byteOffset, value); 38 | return; 39 | } 40 | 41 | dt.setUint8(options.byteOffset, value & SEGMENT_BITS | CONTINUE_BIT); 42 | super.incrementOffset(options, 1); 43 | value >>>= 7; 44 | } 45 | } 46 | } 47 | 48 | export const i32leb128: I32Leb128 = new I32Leb128(); 49 | -------------------------------------------------------------------------------- /src/varint/i32_leb128_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 2 | import { i32leb128 } from "./mod.ts"; 3 | 4 | Deno.test("i32leb128", async ({ step }) => { 5 | await step("estimated size", () => { 6 | assertEquals(i32leb128.maxSize, 5); 7 | }); 8 | 9 | await step("read", async ({ step }) => { 10 | await step("positive", () => { 11 | let data = Uint8Array.of(127); 12 | let result = i32leb128.read(new DataView(data.buffer)); 13 | assertEquals(result, 127); 14 | 15 | data = Uint8Array.of(128, 1); 16 | result = i32leb128.read(new DataView(data.buffer)); 17 | assertEquals(result, 128); 18 | 19 | data = Uint8Array.of(221, 199, 1); 20 | result = i32leb128.read(new DataView(data.buffer)); 21 | assertEquals(result, 25565); 22 | 23 | data = Uint8Array.of(255, 255, 255, 255, 7); 24 | result = i32leb128.read(new DataView(data.buffer)); 25 | assertEquals(result, 2147483647); 26 | }); 27 | 28 | await step("negative", () => { 29 | let data = Uint8Array.of(255, 255, 255, 255, 15); 30 | let result = i32leb128.read(new DataView(data.buffer)); 31 | assertEquals(result, -1); 32 | 33 | data = Uint8Array.of(128, 128, 128, 128, 8); 34 | result = i32leb128.read(new DataView(data.buffer)); 35 | assertEquals(result, -2147483648); 36 | }); 37 | 38 | await step("bad", () => { 39 | const data = Uint8Array.of(255, 255, 255, 255, 255, 15); 40 | assertThrows(() => i32leb128.read(new DataView(data.buffer))); 41 | }); 42 | 43 | await step("i32 max", () => { 44 | const data = Uint8Array.of(255, 255, 255, 255, 7); 45 | assertEquals(i32leb128.read(new DataView(data.buffer)), 2147483647); 46 | }); 47 | }); 48 | 49 | await step("write", async ({ step }) => { 50 | await step("positive", () => { 51 | let data = new Uint8Array(1); 52 | i32leb128.write(127, new DataView(data.buffer)); 53 | assertEquals(data, Uint8Array.of(127)); 54 | 55 | data = new Uint8Array(2); 56 | i32leb128.write(128, new DataView(data.buffer)); 57 | assertEquals(data, Uint8Array.of(128, 1)); 58 | 59 | data = new Uint8Array(3); 60 | i32leb128.write(25565, new DataView(data.buffer)); 61 | assertEquals(data, Uint8Array.of(221, 199, 1)); 62 | 63 | data = new Uint8Array(5); 64 | i32leb128.write(2147483647, new DataView(data.buffer)); 65 | assertEquals(data, Uint8Array.of(255, 255, 255, 255, 7)); 66 | }); 67 | 68 | await step("negative", () => { 69 | let data = new Uint8Array(5); 70 | i32leb128.write(-1, new DataView(data.buffer)); 71 | assertEquals(data, Uint8Array.of(255, 255, 255, 255, 15)); 72 | 73 | data = new Uint8Array(5); 74 | i32leb128.write(-2147483648, new DataView(data.buffer)); 75 | assertEquals(data, Uint8Array.of(128, 128, 128, 128, 8)); 76 | }); 77 | 78 | await step("i32 max", () => { 79 | const data = new Uint8Array(5); 80 | i32leb128.write(2147483647, new DataView(data.buffer)); 81 | assertEquals(data, Uint8Array.of(255, 255, 255, 255, 7)); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/varint/i64_leb128.ts: -------------------------------------------------------------------------------- 1 | import { type Options, UnsizedType } from "../types/mod.ts"; 2 | import { CONTINUE_BIT, SEGMENT_BITS } from "./_common.ts"; 3 | 4 | const SEGMENT_BITS_N = BigInt(SEGMENT_BITS); 5 | const CONTINUE_BIT_N = BigInt(CONTINUE_BIT); 6 | 7 | const AB = new ArrayBuffer(8); 8 | const U32_VIEW = new Uint32Array(AB); 9 | const I64_VIEW = new BigInt64Array(AB); 10 | const U64_VIEW = new BigUint64Array(AB); 11 | 12 | export class I64Leb128 extends UnsizedType { 13 | override maxSize = 10; 14 | 15 | constructor() { 16 | super(1); 17 | } 18 | 19 | readPacked(dt: DataView, options: Options = { byteOffset: 0 }): bigint { 20 | // Copyright 2023 the Blocktopus authors. All rights reserved. MIT license. 21 | // Modified to use a `DataView` instead of a `Uint8Array` and to return i64 instead of u64 22 | 23 | U64_VIEW[0] = 0n; 24 | let intermediate = 0; 25 | let position = 0; 26 | let i = 0; 27 | 28 | let byte = dt.getUint8(options.byteOffset); 29 | do { 30 | byte = dt.getUint8(options.byteOffset); 31 | 32 | // 1. Take the lower 7 bits of the byte. 33 | // 2. Shift the bits into the correct position. 34 | // 3. Bitwise OR it with the intermediate value 35 | // QUIRK: in the 5th (and 10th) iteration of this loop it will overflow on the shift. 36 | // This causes only the lower 4 bits to be shifted into place and removing the upper 3 bits 37 | intermediate |= (byte & 0b01111111) << position; 38 | 39 | if (position === 28) { 40 | // Write to the view 41 | U32_VIEW[0] = intermediate; 42 | // set `intermediate` to the remaining 3 bits 43 | // We only want the remaining three bits because the other 4 have been "consumed" on line 21 44 | intermediate = (byte & 0b01110000) >>> 4; 45 | // set `position` to -4 because later 7 will be added, making it 3 46 | position = -4; 47 | } 48 | 49 | position += 7; 50 | i++; 51 | super.incrementOffset(options, 1); 52 | // Keep going while there is a continuation bit 53 | } while ((byte & CONTINUE_BIT) === CONTINUE_BIT); 54 | 55 | if (i === 11 && intermediate > -1) { 56 | throw new RangeError("Maximum size reached"); 57 | } 58 | 59 | // Write the intermediate value to the "empty" slot 60 | // if the first slot is taken. Take the second slot 61 | U32_VIEW[Number(i > 4)] = intermediate; 62 | 63 | // Cast the two u32's to a i64 bigint 64 | return I64_VIEW[0]; 65 | } 66 | 67 | writePacked( 68 | value: bigint, 69 | dt: DataView, 70 | options: Options = { byteOffset: 0 }, 71 | ): void { 72 | I64_VIEW[0] = value; 73 | value = U64_VIEW[0]; 74 | 75 | while (true) { 76 | if (Number(value & CONTINUE_BIT_N) === 0) { 77 | dt.setUint8(options.byteOffset, Number(value)); 78 | super.incrementOffset(options, 1); 79 | return; 80 | } 81 | 82 | dt.setUint8( 83 | options.byteOffset, 84 | Number(value & SEGMENT_BITS_N) | CONTINUE_BIT, 85 | ); 86 | super.incrementOffset(options, 1); 87 | value >>= 7n; 88 | } 89 | } 90 | } 91 | 92 | export const i64leb128: I64Leb128 = new I64Leb128(); 93 | -------------------------------------------------------------------------------- /src/varint/i64_leb128_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "../../test_deps.ts"; 2 | import { i64leb128 } from "./i64_leb128.ts"; 3 | 4 | Deno.test("i64leb128", async ({ step }) => { 5 | await step("estimated size", () => { 6 | assertEquals(i64leb128.maxSize, 10); 7 | }); 8 | 9 | await step("read", async ({ step }) => { 10 | await step("positive", () => { 11 | assertEquals( 12 | i64leb128.read(new DataView(Uint8Array.of(0x01).buffer)), 13 | 1n, 14 | ); 15 | assertEquals( 16 | i64leb128.read(new DataView(Uint8Array.of(0xff, 0x01).buffer)), 17 | 255n, 18 | ); 19 | assertEquals( 20 | i64leb128.read( 21 | new DataView(Uint8Array.of(0xff, 0xff, 0xff, 0xff, 0x07).buffer), 22 | ), 23 | 2147483647n, 24 | ); 25 | assertEquals( 26 | i64leb128.read( 27 | new DataView( 28 | Uint8Array.of( 29 | 0xff, 30 | 0xff, 31 | 0xff, 32 | 0xff, 33 | 0xff, 34 | 0xff, 35 | 0xff, 36 | 0xff, 37 | 0x7f, 38 | ).buffer, 39 | ), 40 | ), 41 | 9223372036854775807n, 42 | ); 43 | }); 44 | 45 | await step("negative", () => { 46 | assertEquals( 47 | i64leb128.read( 48 | new DataView( 49 | Uint8Array.of( 50 | 0xff, 51 | 0xff, 52 | 0xff, 53 | 0xff, 54 | 0xff, 55 | 0xff, 56 | 0xff, 57 | 0xff, 58 | 0xff, 59 | 0x01, 60 | ).buffer, 61 | ), 62 | ), 63 | -1n, 64 | ); 65 | assertEquals( 66 | i64leb128.read( 67 | new DataView( 68 | Uint8Array.of( 69 | 0x80, 70 | 0x80, 71 | 0x80, 72 | 0x80, 73 | 0xf8, 74 | 0xff, 75 | 0xff, 76 | 0xff, 77 | 0xff, 78 | 0x01, 79 | ).buffer, 80 | ), 81 | ), 82 | -2147483648n, 83 | ); 84 | assertEquals( 85 | i64leb128.read( 86 | new DataView( 87 | Uint8Array.of( 88 | 0x80, 89 | 0x80, 90 | 0x80, 91 | 0x80, 92 | 0x80, 93 | 0x80, 94 | 0x80, 95 | 0x80, 96 | 0x80, 97 | 0x01, 98 | ).buffer, 99 | ), 100 | ), 101 | -9223372036854775808n, 102 | ); 103 | }); 104 | 105 | await step("bad", () => { 106 | assertThrows(() => 107 | i64leb128.read( 108 | new DataView( 109 | Uint8Array.of( 110 | 0x80, 111 | 0x80, 112 | 0x80, 113 | 0x80, 114 | 0x80, 115 | 0x80, 116 | 0x80, 117 | 0x80, 118 | 0x80, 119 | 0x80, 120 | 0x01, 121 | ).buffer, 122 | ), 123 | ) 124 | ); 125 | }); 126 | }); 127 | 128 | await step("write", async ({ step }) => { 129 | await step("positive", () => { 130 | const buff = new Uint8Array(1); 131 | i64leb128.write(10n, new DataView(buff.buffer)); 132 | assertEquals(buff, Uint8Array.of(10)); 133 | }); 134 | 135 | await step("negative", () => { 136 | const buff = new Uint8Array(10); 137 | i64leb128.write(-1n, new DataView(buff.buffer)); 138 | assertEquals( 139 | buff, 140 | Uint8Array.of( 141 | 0xff, 142 | 0xff, 143 | 0xff, 144 | 0xff, 145 | 0xff, 146 | 0xff, 147 | 0xff, 148 | 0xff, 149 | 0xff, 150 | 0x01, 151 | ), 152 | ); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/varint/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./i32_leb128.ts"; 2 | export * from "./i64_leb128.ts"; 3 | -------------------------------------------------------------------------------- /test_deps.ts: -------------------------------------------------------------------------------- 1 | export * from "jsr:@std/assert@0.218"; 2 | --------------------------------------------------------------------------------