diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8f7e379 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +open_collective: denosaurs +github: denosaurs diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..17da22e --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,35 @@ +name: check + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Setup latest deno version + uses: denolib/setup-deno@v2 + with: + deno-version: v1.x + + - name: Run deno fmt + run: deno fmt --check + + - name: Run deno lint + run: deno lint --unstable + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Setup latest deno version + uses: denolib/setup-deno@v2 + with: + deno-version: v1.x + + - name: Run deno test + run: deno test --allow-none diff --git a/.github/workflows/depsbot.yml b/.github/workflows/depsbot.yml new file mode 100644 index 0000000..705242d --- /dev/null +++ b/.github/workflows/depsbot.yml @@ -0,0 +1,21 @@ +name: depsbot + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 0 */2 * *" + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Run depsbot + uses: denosaurs/depsbot@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a30e17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 the denosaurs team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..85a89e2 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# byte_type + +[![Tags](https://img.shields.io/github/release/denosaurs/byte_type)](https://github.com/denosaurs/byte_type/releases) +[![CI Status](https://img.shields.io/github/workflow/status/denosaurs/byte_type/check)](https://github.com/denosaurs/byte_type/actions) +[![Dependencies](https://img.shields.io/github/workflow/status/denosaurs/byte_type/depsbot?label=dependencies)](https://github.com/denosaurs/depsbot) +[![License](https://img.shields.io/github/license/denosaurs/byte_type)](https://github.com/denosaurs/byte_type/blob/master/LICENSE) + +`byte_type` is a small helper module for working with different raw types +represented as a bunch of bytes. + +## Maintainers + +- Elias Sjögreen ([@eliassjogreen](https://github.com/eliassjogreen)) + +## Other + +### Contribution + +Pull request, issues and feedback are very welcome. Code style is formatted with +`deno fmt` and commit messages are done following Conventional Commits spec. + +### Licence + +Copyright 2021, the denosaurs team. All rights reserved. MIT license. diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..78bb939 --- /dev/null +++ b/mod.ts @@ -0,0 +1,593 @@ +import { order } from "./util.ts"; + +export type InnerType = T extends Type ? I : never; + +export interface Type { + readonly size: number; + readonly endian?: boolean; + + read(view: DataView, offset: number): T; + write(view: DataView, offset: number, value: T): void; +} + +export class I8 implements Type { + readonly size = 1; + + read(view: DataView, offset: number): number { + return view.getInt8(offset); + } + + write(view: DataView, offset: number, value: number) { + view.setInt8(offset, value); + return view.buffer; + } +} + +export class U8 implements Type { + readonly size = 1; + + read(view: DataView, offset: number): number { + return view.getUint8(offset); + } + + write(view: DataView, offset: number, value: number) { + view.setUint8(offset, value); + return view.buffer; + } +} + +export class I16 implements Type { + readonly size = 2; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getInt16(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setInt16(offset, value, this.endian); + return view.buffer; + } +} + +export class U16 implements Type { + readonly size = 2; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getUint16(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setUint16(offset, value, this.endian); + return view.buffer; + } +} + +export class I32 implements Type { + readonly size = 4; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getInt32(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setInt32(offset, value, this.endian); + return view.buffer; + } +} + +export class U32 implements Type { + readonly size = 4; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getUint32(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setUint32(offset, value, this.endian); + return view.buffer; + } +} + +export class I64 implements Type { + readonly size = 8; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): bigint { + return view.getBigInt64(offset, this.endian); + } + + write(view: DataView, offset: number, value: bigint) { + view.setBigInt64(offset, value, this.endian); + return view.buffer; + } +} + +export class U64 implements Type { + readonly size = 8; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): bigint { + return view.getBigUint64(offset, this.endian); + } + + write(view: DataView, offset: number, value: bigint) { + view.setBigUint64(offset, value, this.endian); + return view.buffer; + } +} + +export class F32 implements Type { + readonly size = 4; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getFloat32(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setFloat32(offset, value, this.endian); + return view.buffer; + } +} + +export class F64 implements Type { + readonly size = 8; + readonly endian; + + constructor(endian?: boolean) { + this.endian = endian; + } + + read(view: DataView, offset: number): number { + return view.getFloat64(offset, this.endian); + } + + write(view: DataView, offset: number, value: number) { + view.setFloat64(offset, value, this.endian); + return view.buffer; + } +} + +export class Bool implements Type { + readonly size = 1; + + read(view: DataView, offset: number): boolean { + return view.getInt8(offset) === 1; + } + + write(view: DataView, offset: number, value: boolean) { + view.setInt8(offset, value ? 1 : 0); + return view.buffer; + } +} + +export class Struct< + T extends Record>, + V extends Record = { [K in keyof T]: InnerType }, +> implements Type { + readonly size: number; + types: T; + + constructor(types: T) { + this.types = types; + this.size = 0; + + for (const type of Object.values(this.types)) { + this.size += type.size; + } + } + + read(view: DataView, offset: number): V { + const object: Record = {}; + + for (const [key, type] of Object.entries(this.types)) { + object[key] = type.read(view, offset); + offset += type.size; + } + + return object as V; + } + + write(view: DataView, offset: number, value: V) { + for (const [key, val] of Object.entries(order(value, this.types))) { + this.types[key].write(view, offset, val); + offset += this.types[key].size; + } + } + + get( + view: DataView, + offset: number, + key: K, + ): InnerType | undefined { + for (const [entry, type] of Object.entries(this.types)) { + const value = type.read(view, offset); + offset += type.size; + + if (entry === key) { + return value as InnerType; + } + } + } + + set( + view: DataView, + offset: number, + key: K, + value: InnerType, + ) { + for (const [entry, type] of Object.entries(this.types)) { + if (entry === key) { + type.write(view, offset, value); + return; + } + + offset += type.size; + } + } +} + +export class FixedArray, V> implements Type { + readonly size: number; + type: T; + + constructor(type: T, length: number) { + this.type = type; + this.size = length * type.size; + } + + read(view: DataView, offset: number): V[] { + const array = []; + + for (let i = offset; i < this.size + offset; i += this.type.size) { + array.push(this.type.read(view, i)); + } + + return array; + } + + write(view: DataView, offset: number, value: V[]) { + for (let i = 0; i < value.length; i++) { + this.type.write(view, offset, value[i]); + offset += this.type.size; + } + } + + get(view: DataView, offset: number, index: number): V { + return this.type.read(view, offset + index * this.type.size); + } + + set(view: DataView, offset: number, index: number, value: V) { + this.type.write(view, offset + index * this.type.size, value); + } +} + +export class Tuple< + T extends [...Type[]], + V extends [...unknown[]] = { [I in keyof T]: InnerType }, +> implements Type { + readonly size: number; + types: T; + + constructor(types: T) { + this.types = types; + this.size = 0; + + for (const type of types) { + this.size += type.size; + } + } + + read(view: DataView, offset: number): V { + const tuple = []; + + for (const type of this.types) { + tuple.push(type.read(view, offset)); + offset += type.size; + } + + return tuple as V; + } + + write(view: DataView, offset: number, value: V) { + let i = 0; + for (const type of this.types) { + type.write(view, offset, value[i++]); + offset += type.size; + } + } + + get(view: DataView, offset: number, index: I): V[I] { + for (let i = 0; i < this.types.length; i++) { + const type = this.types[i]; + const value = type.read(view, offset); + offset += type.size; + + if (index === i) { + return value as V[I]; + } + } + + throw new RangeError("Index is out of range"); + } + + set( + view: DataView, + offset: number, + index: I, + value: V[I], + ) { + for (let i = 0; i < this.types.length; i++) { + const type = this.types[i]; + if (index === i) { + type.write(view, offset, value); + return; + } + offset += type.size; + } + + throw new RangeError("Index is out of range"); + } +} + +export class FixedString implements Type { + readonly size: number; + type: Type; + + constructor(length: number, type: Type = u16) { + this.size = length * type.size; + this.type = type; + } + + read(view: DataView, offset: number): string { + const array = []; + + for (let i = offset; i < this.size + offset; i += this.type.size) { + array.push(this.type.read(view, i)); + } + + return array.join(""); + } + + write(view: DataView, offset: number, value: string) { + for (let i = 0; i < value.length; i++) { + this.type.write(view, offset, value.charCodeAt(i)); + offset += this.type.size; + } + } +} + +export class BitFlags8< + T extends Record, + V extends Record = { [K in keyof T]: boolean }, +> implements Type { + readonly size = 1; + flags: T; + + constructor(flags: T) { + this.flags = flags; + } + + read(view: DataView, offset: number): V { + const flags = view.getUint8(offset); + const ret: Record = {}; + + for (const [key, flag] of Object.entries(this.flags)) { + ret[key] = ((flags & flag) === flag); + } + + return ret as V; + } + + write(view: DataView, offset: number, value: V) { + let flags = 0; + + for (const [key, enabled] of Object.entries(value)) { + if (enabled) { + flags |= this.flags[key]; + } + } + + view.setUint8(offset, flags); + } +} + +export class BitFlags16< + T extends Record, + V extends Record = { [K in keyof T]: boolean }, +> implements Type { + readonly size = 1; + readonly endian; + flags: T; + + constructor(flags: T, endian?: boolean) { + this.flags = flags; + this.endian = endian; + } + + read(view: DataView, offset: number): V { + const flags = view.getUint16(offset, this.endian); + const ret: Record = {}; + + for (const [key, flag] of Object.entries(this.flags)) { + ret[key] = ((flags & flag) === flag); + } + + return ret as V; + } + + write(view: DataView, offset: number, value: V) { + let flags = 0; + + for (const [key, enabled] of Object.entries(value)) { + if (enabled) { + flags |= this.flags[key]; + } + } + + view.setUint16(offset, flags, this.endian); + } +} + +export class BitFlags32< + T extends Record, + V extends Record = { [K in keyof T]: boolean }, +> implements Type { + readonly size = 1; + readonly endian; + flags: T; + + constructor(flags: T, endian?: boolean) { + this.flags = flags; + this.endian = endian; + } + + read(view: DataView, offset: number): V { + const flags = view.getUint32(offset, this.endian); + const ret: Record = {}; + + for (const [key, flag] of Object.entries(this.flags)) { + ret[key] = ((flags & flag) === flag); + } + + return ret as V; + } + + write(view: DataView, offset: number, value: V) { + let flags = 0; + + for (const [key, enabled] of Object.entries(value)) { + if (enabled) { + flags |= this.flags[key]; + } + } + + view.setUint32(offset, flags, this.endian); + } +} + +export class BitFlags64< + T extends Record, + V extends Record = { [K in keyof T]: boolean }, +> implements Type { + readonly size = 1; + readonly endian; + flags: T; + + constructor(flags: T, endian?: boolean) { + this.flags = flags; + this.endian = endian; + } + + read(view: DataView, offset: number): V { + const flags = view.getBigUint64(offset, this.endian); + const ret: Record = {}; + + for (const [key, flag] of Object.entries(this.flags)) { + ret[key] = ((flags & flag) === flag); + } + + return ret as V; + } + + write(view: DataView, offset: number, value: V) { + let flags = 0n; + + for (const [key, enabled] of Object.entries(value)) { + if (enabled) { + flags |= this.flags[key]; + } + } + + view.setBigUint64(offset, flags, this.endian); + } +} + +export class Expect< + V, + T extends Type, +> implements Type { + readonly size; + type: T; + expected: V; + + constructor(type: T, expected: V) { + this.size = type.size; + this.type = type; + this.expected = expected; + } + + read(view: DataView, offset: number): V { + const value = this.type.read(view, offset); + + if (value !== this.expected) { + throw new TypeError(`Expected ${this.expected} found ${value}`); + } + + return value; + } + + write(view: DataView, offset: number) { + this.type.write(view, offset, this.expected); + } +} + +export const i8 = new I8(); +export const u8 = new U8(); +export const i16 = new I16(); +export const i16le = new I16(true); +export const i16be = new I16(false); +export const u16 = new U16(); +export const u16le = new U16(true); +export const u16be = new U16(false); +export const i32 = new I32(); +export const i32le = new I32(true); +export const i32be = new I32(false); +export const u32 = new U32(); +export const u32le = new U32(true); +export const u32be = new U32(false); +export const i64 = new I64(); +export const i64le = new I64(true); +export const i64be = new I64(false); +export const u64 = new U64(); +export const u64le = new U64(true); +export const u64be = new U64(false); +export const f32 = new F32(); +export const f32le = new F32(true); +export const f32be = new F32(false); +export const f64 = new F64(); +export const f64le = new F64(true); +export const f64be = new F64(false); +export const bool = new Bool(); diff --git a/util.ts b/util.ts new file mode 100644 index 0000000..3f68473 --- /dev/null +++ b/util.ts @@ -0,0 +1,12 @@ +export function order< + V extends Record, + T extends Record = { [K in keyof V]: unknown }, +>(object: V, order: T): T { + const reordered: Record = {}; + + for (const key of Object.keys(order)) { + reordered[key] = object[key]; + } + + return reordered as T; +}