Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
71ae225972
|
|||
|
3855e478c0
|
|||
|
93a642f242
|
|||
|
8a641cbc02
|
|||
|
b3d5aace89
|
|||
|
8994fb7b45
|
|||
|
a5acb5ed03
|
|||
|
2a5e4b1052
|
|||
|
c18a544a2e
|
|||
|
25c07c947a
|
|||
|
4170305233
|
|||
|
a83c4ca4c0
|
|||
|
01978cb867
|
|||
|
1fe9bb00d4
|
|||
|
3de4ec28b4
|
|||
|
c33f6f781c
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -103,6 +103,9 @@ web_modules/
|
|||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
|
# Optional npm package-lock.json
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,22 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
- repo: local
|
||||||
rev: v10.0.3
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
name: prettier
|
||||||
|
entry: npx prettier --write
|
||||||
|
language: system
|
||||||
|
files: "\\.(js|jsx|ts|tsx)$"
|
||||||
|
|
||||||
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
|
name: eslint
|
||||||
|
entry: npx eslint --fix
|
||||||
|
language: system
|
||||||
files: "\\.(js|jsx|ts|tsx)$"
|
files: "\\.(js|jsx|ts|tsx)$"
|
||||||
exclude: node_modules/
|
|
||||||
|
|
||||||
# Use stylelint (local) instead of the deprecated scss-lint Ruby gem which
|
|
||||||
# cannot parse modern Sass `@use` and module syntax. This invokes the
|
|
||||||
# project's installed `stylelint` via `npx` so the devDependency is used.
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: stylelint
|
- id: stylelint
|
||||||
|
|||||||
8
.prettierrc.ts
Normal file
8
.prettierrc.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { type Config } from "prettier";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
trailingComma: "none",
|
||||||
|
printWidth: 120
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
25
LICENSE.md
Normal file
25
LICENSE.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Copyright © `2026` `Wijnand Modderman-Lenstra <maze@maze.io>`
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
24
eslint.config.ts
Normal file
24
eslint.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(["dist"]),
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
extends: [js.configs.recommended, tseslint.configs.recommended],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ts-ignore": "allow-with-description"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
3957
package-lock.json
generated
3957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hamradio/packet",
|
"name": "@hamradio/packet",
|
||||||
"version": "1.0.0",
|
"type": "module",
|
||||||
|
"version": "1.1.1",
|
||||||
"description": "Low level packet parsing library (for radio protocols)",
|
"description": "Low level packet parsing library (for radio protocols)",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"HAM radio",
|
"HAM radio",
|
||||||
@@ -12,12 +13,12 @@
|
|||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.maze.io/ham/packet.js"
|
"url": "https://git.maze.io/ham/packet.ts"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Wijnand Modderman-Lenstra",
|
"author": "Wijnand Modderman-Lenstra",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.js"
|
"require": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -39,12 +40,13 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"prettier": "3.8.1",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.57.0",
|
"typescript-eslint": "^8.57.0",
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import { Reader, Writer } from '.';
|
import { Reader, Writer } from ".";
|
||||||
|
|
||||||
describe('Reader/Writer round-trip', () => {
|
describe("Reader/Writer round-trip", () => {
|
||||||
it('writes then reads a variety of types correctly', () => {
|
it("writes then reads a variety of types correctly", () => {
|
||||||
const writer = new Writer(1024);
|
const writer = new Writer(1024);
|
||||||
|
|
||||||
// values to write
|
// values to write
|
||||||
const bBool = true;
|
const bBool = true;
|
||||||
|
const bChar = "A";
|
||||||
const bInt8 = -42;
|
const bInt8 = -42;
|
||||||
const bUint8 = 200;
|
const bUint8 = 200;
|
||||||
const bUint16 = 0xBEEF;
|
const bUint16 = 0xbeef;
|
||||||
const bUint32 = 0xDEADBEEF >>> 0;
|
const bUint32 = 0xdeadbeef >>> 0;
|
||||||
const bUint64 = 0x1122334455667788n;
|
const bUint64 = 0x1122334455667788n;
|
||||||
const bFloat32 = 3.1415927;
|
const bFloat32 = 3.1415927;
|
||||||
const bFloat64 = 1.23456789e5;
|
const bFloat64 = 1.23456789e5;
|
||||||
const bCString = 'hello';
|
const bCString = "hello";
|
||||||
const bUtf8 = 'π≈3.14';
|
const bUtf8 = "π≈3.14";
|
||||||
const words = [0x1234, 0x5678, 0x9ABC];
|
const words = [0x1234, 0x5678, 0x9abc];
|
||||||
const dwords = [0x11223344 >>> 0, 0x55667788 >>> 0];
|
const dwords = [0x11223344 >>> 0, 0x55667788 >>> 0];
|
||||||
const qwords = [0x8000000000000000n, 0xAABBCCDDEEFF0011n];
|
const qwords = [0x8000000000000000n, 0xaabbccddeeff0011n];
|
||||||
const rawBytes = new Uint8Array([1, 2, 3, 4]);
|
const rawBytes = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
|
||||||
// write sequence
|
// write sequence
|
||||||
writer.bool(bBool);
|
writer.bool(bBool);
|
||||||
|
writer.char(bChar);
|
||||||
writer.int8(bInt8);
|
writer.int8(bInt8);
|
||||||
writer.uint8(bUint8);
|
writer.uint8(bUint8);
|
||||||
writer.uint16(bUint16);
|
writer.uint16(bUint16);
|
||||||
@@ -45,6 +47,7 @@ describe('Reader/Writer round-trip', () => {
|
|||||||
|
|
||||||
// read back in same order
|
// read back in same order
|
||||||
expect(reader.bool()).toBe(bBool);
|
expect(reader.bool()).toBe(bBool);
|
||||||
|
expect(reader.char()).toBe(bChar);
|
||||||
expect(reader.int8()).toBe(bInt8);
|
expect(reader.int8()).toBe(bInt8);
|
||||||
expect(reader.uint8()).toBe(bUint8);
|
expect(reader.uint8()).toBe(bUint8);
|
||||||
expect(reader.uint16()).toBe(bUint16);
|
expect(reader.uint16()).toBe(bUint16);
|
||||||
|
|||||||
183
src/index.ts
183
src/index.ts
@@ -15,7 +15,7 @@ export class Reader {
|
|||||||
const srcBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
const srcBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||||
this.buffer = srcBuffer instanceof ArrayBuffer ? srcBuffer : new ArrayBuffer(srcBuffer.byteLength);
|
this.buffer = srcBuffer instanceof ArrayBuffer ? srcBuffer : new ArrayBuffer(srcBuffer.byteLength);
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError('Invalid buffer type. Expected ArrayBuffer, Uint8Array, or ArrayBufferView.');
|
throw new TypeError("Invalid buffer type. Expected ArrayBuffer, Uint8Array, or ArrayBufferView.");
|
||||||
}
|
}
|
||||||
this.view = new DataView(this.buffer);
|
this.view = new DataView(this.buffer);
|
||||||
this.offset = 0;
|
this.offset = 0;
|
||||||
@@ -35,6 +35,17 @@ export class Reader {
|
|||||||
return value !== 0;
|
return value !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an 8-bit character from the buffer at the current offset, and advance the offset by 1 byte.
|
||||||
|
* @returns A string containing the character read from the buffer.
|
||||||
|
*/
|
||||||
|
public char(): string {
|
||||||
|
this.checkBounds(1);
|
||||||
|
const value = this.view.getUint8(this.offset);
|
||||||
|
this.offset += 1;
|
||||||
|
return String.fromCharCode(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read an 8-bit signed integer from the buffer at the current offset, and advance the offset by 1 byte.
|
* Read an 8-bit signed integer from the buffer at the current offset, and advance the offset by 1 byte.
|
||||||
*
|
*
|
||||||
@@ -179,7 +190,7 @@ export class Reader {
|
|||||||
while (true) {
|
while (true) {
|
||||||
this.checkBounds(1);
|
this.checkBounds(1);
|
||||||
const byte = this.view.getUint8(this.offset++);
|
const byte = this.view.getUint8(this.offset++);
|
||||||
result |= (byte & 0x7F) << shift;
|
result |= (byte & 0x7f) << shift;
|
||||||
if ((byte & 0x80) === 0) {
|
if ((byte & 0x80) === 0) {
|
||||||
break; // Last byte of the varint
|
break; // Last byte of the varint
|
||||||
}
|
}
|
||||||
@@ -205,7 +216,7 @@ export class Reader {
|
|||||||
while (true) {
|
while (true) {
|
||||||
this.checkBounds(1);
|
this.checkBounds(1);
|
||||||
const byte = this.view.getUint8(this.offset++);
|
const byte = this.view.getUint8(this.offset++);
|
||||||
result |= (byte & 0x7F) << shift;
|
result |= (byte & 0x7f) << shift;
|
||||||
if ((byte & 0x80) === 0) {
|
if ((byte & 0x80) === 0) {
|
||||||
break; // Last byte of the varint
|
break; // Last byte of the varint
|
||||||
}
|
}
|
||||||
@@ -342,32 +353,36 @@ export class Reader {
|
|||||||
return new Reader(bytes, littleEndian);
|
return new Reader(bytes, littleEndian);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromString(str: string, encoding: 'utf8' | 'ascii' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf8', littleEndian: boolean = true): Reader {
|
public static fromString(
|
||||||
|
str: string,
|
||||||
|
encoding: "utf8" | "ascii" | "hex" | "base64" | "rawbase64" | "urlbase64" | "rawurlbase64" = "utf8",
|
||||||
|
littleEndian: boolean = true
|
||||||
|
): Reader {
|
||||||
let bytes: Uint8Array;
|
let bytes: Uint8Array;
|
||||||
switch (encoding) {
|
switch (encoding) {
|
||||||
case 'utf8':
|
case "utf8":
|
||||||
bytes = new TextEncoder().encode(str);
|
bytes = new TextEncoder().encode(str);
|
||||||
break;
|
break;
|
||||||
case 'ascii':
|
case "ascii":
|
||||||
bytes = new Uint8Array(str.split('').map(c => c.charCodeAt(0)));
|
bytes = new Uint8Array(str.split("").map((c) => c.charCodeAt(0)));
|
||||||
break;
|
break;
|
||||||
case 'hex':
|
case "hex":
|
||||||
bytes = new Uint8Array(str.length / 2);
|
bytes = new Uint8Array(str.length / 2);
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
bytes[i] = parseInt(str.substr(i * 2, 2), 16);
|
bytes[i] = parseInt(str.substr(i * 2, 2), 16);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'base64':
|
case "base64":
|
||||||
bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'rawbase64':
|
case "rawbase64":
|
||||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(str.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'urlbase64':
|
case "urlbase64":
|
||||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(str.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'rawurlbase64':
|
case "rawurlbase64":
|
||||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(str.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return new Reader(bytes.slice().buffer, littleEndian);
|
return new Reader(bytes.slice().buffer, littleEndian);
|
||||||
@@ -387,7 +402,9 @@ export class Reader {
|
|||||||
|
|
||||||
private checkBounds(length: number) {
|
private checkBounds(length: number) {
|
||||||
if (this.offset + length > this.view.byteLength) {
|
if (this.offset + length > this.view.byteLength) {
|
||||||
throw new RangeError(`Attempt to read beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`);
|
throw new RangeError(
|
||||||
|
`Attempt to read beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,6 +445,27 @@ export class Writer {
|
|||||||
this.offset += 1;
|
this.offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an 8-bit character to the buffer at the current offset, and advance the offset by 1 byte.
|
||||||
|
*
|
||||||
|
* @param value The character or its ASCII code to write.
|
||||||
|
*/
|
||||||
|
public char(value: string | number): void {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value.length !== 1) {
|
||||||
|
throw new Error("Input must be a single character");
|
||||||
|
}
|
||||||
|
this.checkBounds(1);
|
||||||
|
this.view.setUint8(this.offset, value.charCodeAt(0));
|
||||||
|
} else if (typeof value === "number") {
|
||||||
|
this.checkBounds(1);
|
||||||
|
this.view.setUint8(this.offset, value);
|
||||||
|
} else {
|
||||||
|
throw new Error("Input must be a string or number");
|
||||||
|
}
|
||||||
|
this.offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write an 8-bit signed integer to the buffer at the current offset, and advance the offset by
|
* Write an 8-bit signed integer to the buffer at the current offset, and advance the offset by
|
||||||
* 1 byte.
|
* 1 byte.
|
||||||
@@ -472,7 +510,7 @@ export class Writer {
|
|||||||
*/
|
*/
|
||||||
public int64(value: number | bigint): void {
|
public int64(value: number | bigint): void {
|
||||||
this.checkBounds(8);
|
this.checkBounds(8);
|
||||||
if (typeof value === 'number') {
|
if (typeof value === "number") {
|
||||||
value = BigInt(value);
|
value = BigInt(value);
|
||||||
}
|
}
|
||||||
this.view.setBigInt64(this.offset, value, this.littleEndian);
|
this.view.setBigInt64(this.offset, value, this.littleEndian);
|
||||||
@@ -523,7 +561,7 @@ export class Writer {
|
|||||||
*/
|
*/
|
||||||
public uint64(value: number | bigint): void {
|
public uint64(value: number | bigint): void {
|
||||||
this.checkBounds(8);
|
this.checkBounds(8);
|
||||||
if (typeof value === 'number') {
|
if (typeof value === "number") {
|
||||||
value = BigInt(value);
|
value = BigInt(value);
|
||||||
}
|
}
|
||||||
this.view.setBigUint64(this.offset, value, this.littleEndian);
|
this.view.setBigUint64(this.offset, value, this.littleEndian);
|
||||||
@@ -570,7 +608,7 @@ export class Writer {
|
|||||||
// Useful for pre-sizing buffers.
|
// Useful for pre-sizing buffers.
|
||||||
let remaining = value >>> 0; // Ensure unsigned
|
let remaining = value >>> 0; // Ensure unsigned
|
||||||
while (remaining >= 0x80) {
|
while (remaining >= 0x80) {
|
||||||
this.view.setUint8(this.offset++, (remaining & 0x7F) | 0x80);
|
this.view.setUint8(this.offset++, (remaining & 0x7f) | 0x80);
|
||||||
remaining >>>= 7;
|
remaining >>>= 7;
|
||||||
}
|
}
|
||||||
this.view.setUint8(this.offset++, remaining);
|
this.view.setUint8(this.offset++, remaining);
|
||||||
@@ -591,7 +629,7 @@ export class Writer {
|
|||||||
let remaining = value >>> 0; // Ensure unsigned
|
let remaining = value >>> 0; // Ensure unsigned
|
||||||
const isNegative = value < 0;
|
const isNegative = value < 0;
|
||||||
while (remaining >= 0x80 || (isNegative && remaining < 0x80)) {
|
while (remaining >= 0x80 || (isNegative && remaining < 0x80)) {
|
||||||
this.view.setUint8(this.offset++, (remaining & 0x7F) | 0x80);
|
this.view.setUint8(this.offset++, (remaining & 0x7f) | 0x80);
|
||||||
remaining >>>= 7;
|
remaining >>>= 7;
|
||||||
}
|
}
|
||||||
this.view.setUint8(this.offset++, remaining);
|
this.view.setUint8(this.offset++, remaining);
|
||||||
@@ -701,46 +739,60 @@ export class Writer {
|
|||||||
* @param encoding The encoding to use for the string conversion.
|
* @param encoding The encoding to use for the string conversion.
|
||||||
* @returns The encoded string.
|
* @returns The encoded string.
|
||||||
*/
|
*/
|
||||||
public toString(encoding: 'utf-8' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf-8'): string {
|
public toString(encoding: "utf-8" | "hex" | "base64" | "rawbase64" | "urlbase64" | "rawurlbase64" = "utf-8"): string {
|
||||||
const bytes = this.toBytes();
|
const bytes = this.toBytes();
|
||||||
switch (encoding) {
|
switch (encoding) {
|
||||||
case 'utf-8':
|
case "utf-8":
|
||||||
return new TextDecoder().decode(bytes);
|
return new TextDecoder().decode(bytes);
|
||||||
case 'hex':
|
case "hex":
|
||||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
return Array.from(bytes)
|
||||||
case 'base64':
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
case "base64":
|
||||||
return btoa(String.fromCharCode(...bytes));
|
return btoa(String.fromCharCode(...bytes));
|
||||||
case 'rawbase64':
|
case "rawbase64":
|
||||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
return btoa(String.fromCharCode(...bytes))
|
||||||
case 'urlbase64':
|
.replace(/\+/g, "-")
|
||||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_');
|
.replace(/\//g, "_")
|
||||||
case 'rawurlbase64':
|
.replace(/=+$/, "");
|
||||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
case "urlbase64":
|
||||||
|
return btoa(String.fromCharCode(...bytes))
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_");
|
||||||
|
case "rawurlbase64":
|
||||||
|
return btoa(String.fromCharCode(...bytes))
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported encoding: ${encoding}`);
|
throw new Error(`Unsupported encoding: ${encoding}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromString(value: string, encoding: 'utf-8' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf-8', littleEndian: boolean = true): Writer {
|
public static fromString(
|
||||||
|
value: string,
|
||||||
|
encoding: "utf-8" | "hex" | "base64" | "rawbase64" | "urlbase64" | "rawurlbase64" = "utf-8",
|
||||||
|
littleEndian: boolean = true
|
||||||
|
): Writer {
|
||||||
let bytes: Uint8Array;
|
let bytes: Uint8Array;
|
||||||
switch (encoding) {
|
switch (encoding) {
|
||||||
case 'utf-8':
|
case "utf-8":
|
||||||
bytes = new TextEncoder().encode(value);
|
bytes = new TextEncoder().encode(value);
|
||||||
break;
|
break;
|
||||||
case 'hex':
|
case "hex":
|
||||||
bytes = new Uint8Array(value.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
bytes = new Uint8Array(value.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||||
break;
|
break;
|
||||||
case 'base64':
|
case "base64":
|
||||||
bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'rawbase64':
|
case "rawbase64":
|
||||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'urlbase64':
|
case "urlbase64":
|
||||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
case 'rawurlbase64':
|
case "rawurlbase64":
|
||||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
bytes = Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported encoding: ${encoding}`);
|
throw new Error(`Unsupported encoding: ${encoding}`);
|
||||||
@@ -764,9 +816,48 @@ export class Writer {
|
|||||||
|
|
||||||
private checkBounds(length: number) {
|
private checkBounds(length: number) {
|
||||||
if (this.offset + length > this.view.byteLength) {
|
if (this.offset + length > this.view.byteLength) {
|
||||||
throw new RangeError(`Attempt to write beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`);
|
throw new RangeError(
|
||||||
|
`Attempt to write beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FieldType } from './types';
|
// Exporting types and utilities for external use:
|
||||||
|
export * from "./types";
|
||||||
|
export {
|
||||||
|
FieldType,
|
||||||
|
type Packet,
|
||||||
|
type Protocol,
|
||||||
|
type Dissected,
|
||||||
|
type Segment,
|
||||||
|
type Field,
|
||||||
|
type BitField
|
||||||
|
} from "./types";
|
||||||
|
export {
|
||||||
|
I8,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
U8,
|
||||||
|
U16,
|
||||||
|
U32,
|
||||||
|
F32,
|
||||||
|
F64,
|
||||||
|
isBytes,
|
||||||
|
assertBytes,
|
||||||
|
equalBytes,
|
||||||
|
constantTimeEqualBytes,
|
||||||
|
base64ToBytes,
|
||||||
|
bytesToBase64,
|
||||||
|
hexToBytes,
|
||||||
|
bytesToHex,
|
||||||
|
decodeBytes,
|
||||||
|
encodeBytes,
|
||||||
|
type TypedArray,
|
||||||
|
type HexEncoding,
|
||||||
|
type Base64Encoding,
|
||||||
|
type Base64RawEncoding,
|
||||||
|
type Base64URLEncoding,
|
||||||
|
type Base64RawURLEncoding,
|
||||||
|
type Encoding
|
||||||
|
} from "./utils";
|
||||||
|
|||||||
101
src/types.ts
101
src/types.ts
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definitions for the packet parsing library.
|
* Type definitions for the packet parsing library.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumeration of supported field types for dissecting packets.
|
* Enumeration of supported field types for dissecting packets.
|
||||||
*
|
*
|
||||||
@@ -13,62 +11,63 @@
|
|||||||
*/
|
*/
|
||||||
export const FieldType = {
|
export const FieldType = {
|
||||||
// Boolean types
|
// Boolean types
|
||||||
BOOL: 'BOOL', // Boolean value (stored as a byte, 0 for false, non-zero for true)
|
BOOL: "BOOL", // Boolean value (stored as a byte, 0 for false, non-zero for true)
|
||||||
|
|
||||||
// Number types
|
// Number types
|
||||||
BITS: 'BITS', // 1-bit values stored in a number (1 bit per value, packed into bytes)
|
BITS: "BITS", // 1-bit values stored in a number (1 bit per value, packed into bytes)
|
||||||
INT8: 'INT8', // 8-bit signed integer (1 byte)
|
CHAR: "CHAR", // 8-bit character (1 byte)
|
||||||
INT16_LE: 'INT16_LE', // 16-bit signed integer (little-endian)
|
INT8: "INT8", // 8-bit signed integer (1 byte)
|
||||||
INT16_BE: 'INT16_BE', // 16-bit signed integer (big-endian)
|
INT16_LE: "INT16_LE", // 16-bit signed integer (little-endian)
|
||||||
INT32_LE: 'INT32_LE', // 32-bit signed integer (little-endian)
|
INT16_BE: "INT16_BE", // 16-bit signed integer (big-endian)
|
||||||
INT32_BE: 'INT32_BE', // 32-bit signed integer (big-endian)
|
INT32_LE: "INT32_LE", // 32-bit signed integer (little-endian)
|
||||||
INT64_LE: 'INT64_LE', // 64-bit signed integer (little-endian)
|
INT32_BE: "INT32_BE", // 32-bit signed integer (big-endian)
|
||||||
INT64_BE: 'INT64_BE', // 64-bit signed integer (big-endian)
|
INT64_LE: "INT64_LE", // 64-bit signed integer (little-endian)
|
||||||
UINT8: 'UINT8', // 8-bit unsigned integer
|
INT64_BE: "INT64_BE", // 64-bit signed integer (big-endian)
|
||||||
UINT16_LE: 'UINT16_LE', // 16-bit unsigned integer (little-endian)
|
UINT8: "UINT8", // 8-bit unsigned integer
|
||||||
UINT16_BE: 'UINT16_BE', // 16-bit unsigned integer (big-endian)
|
UINT16_LE: "UINT16_LE", // 16-bit unsigned integer (little-endian)
|
||||||
UINT32_LE: 'UINT32_LE', // 32-bit unsigned integer (little-endian)
|
UINT16_BE: "UINT16_BE", // 16-bit unsigned integer (big-endian)
|
||||||
UINT32_BE: 'UINT32_BE', // 32-bit unsigned integer (big-endian)
|
UINT32_LE: "UINT32_LE", // 32-bit unsigned integer (little-endian)
|
||||||
UINT64_LE: 'UINT64_LE', // 64-bit unsigned integer (little-endian)
|
UINT32_BE: "UINT32_BE", // 32-bit unsigned integer (big-endian)
|
||||||
UINT64_BE: 'UINT64_BE', // 64-bit unsigned integer (big-endian)
|
UINT64_LE: "UINT64_LE", // 64-bit unsigned integer (little-endian)
|
||||||
FLOAT32_LE: 'FLOAT32_LE', // 32-bit IEEE floating point (little-endian)
|
UINT64_BE: "UINT64_BE", // 64-bit unsigned integer (big-endian)
|
||||||
FLOAT32_BE: 'FLOAT32_BE', // 32-bit IEEE floating point (big-endian)
|
FLOAT32_LE: "FLOAT32_LE", // 32-bit IEEE floating point (little-endian)
|
||||||
FLOAT64_LE: 'FLOAT64_LE', // 64-bit IEEE floating point (little-endian)
|
FLOAT32_BE: "FLOAT32_BE", // 32-bit IEEE floating point (big-endian)
|
||||||
FLOAT64_BE: 'FLOAT64_BE', // 64-bit IEEE floating point (big-endian)
|
FLOAT64_LE: "FLOAT64_LE", // 64-bit IEEE floating point (little-endian)
|
||||||
VARINT: 'VARINT', // Variable-length integer (unsigned, LEB128 encoding)
|
FLOAT64_BE: "FLOAT64_BE", // 64-bit IEEE floating point (big-endian)
|
||||||
VARSINT: 'VARSINT', // Variable-length integer (signed, LEB128 encoding)
|
VARINT: "VARINT", // Variable-length integer (unsigned, LEB128 encoding)
|
||||||
|
VARSINT: "VARSINT", // Variable-length integer (signed, LEB128 encoding)
|
||||||
|
|
||||||
// Date/time types (stored as integer timestamps)
|
// Date/time types (stored as integer timestamps)
|
||||||
DATE32_LE: 'DATE32_LE', // 32-bit integer date (e.g., seconds since epoch) little-endian
|
DATE32_LE: "DATE32_LE", // 32-bit integer date (e.g., seconds since epoch) little-endian
|
||||||
DATE32_BE: 'DATE32_BE', // 32-bit integer date big-endian
|
DATE32_BE: "DATE32_BE", // 32-bit integer date big-endian
|
||||||
DATE64_LE: 'DATE64_LE', // 64-bit integer date (e.g., ms since epoch) little-endian
|
DATE64_LE: "DATE64_LE", // 64-bit integer date (e.g., ms since epoch) little-endian
|
||||||
DATE64_BE: 'DATE64_BE', // 64-bit integer date big-endian
|
DATE64_BE: "DATE64_BE", // 64-bit integer date big-endian
|
||||||
|
|
||||||
// Array buffer types
|
// Array buffer types
|
||||||
BYTES: 'BYTES', // 8-bits per value array (Uint8Array)
|
BYTES: "BYTES", // 8-bits per value array (Uint8Array)
|
||||||
C_STRING: 'C_STRING', // Null-terminated string (C-style) (Uint8Array)
|
C_STRING: "C_STRING", // Null-terminated string (C-style) (Uint8Array)
|
||||||
UTF8_STRING: 'UTF8_STRING', // UTF-8 encoded string (Uint8Array)
|
UTF8_STRING: "UTF8_STRING", // UTF-8 encoded string (Uint8Array)
|
||||||
WORDS: 'WORDS', // 16-bits per value array (Uint16Array)
|
WORDS: "WORDS", // 16-bits per value array (Uint16Array)
|
||||||
DWORDS: 'DWORDS', // 32-bits per value array (Uint32Array)
|
DWORDS: "DWORDS", // 32-bits per value array (Uint32Array)
|
||||||
QWORDS: 'QWORDS', // 64-bits per value array (BigUint64Array)
|
QWORDS: "QWORDS", // 64-bits per value array (BigUint64Array)
|
||||||
|
|
||||||
// Aliases
|
// Aliases
|
||||||
FLAG: 'BOOL', // alternate name for boolean/flag fields
|
FLAG: "BOOL", // alternate name for boolean/flag fields
|
||||||
STRING: 'UTF8_STRING', // alias for UTF8 encoded strings
|
STRING: "UTF8_STRING" // alias for UTF8 encoded strings
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FieldType = typeof FieldType[keyof typeof FieldType];
|
export type FieldType = (typeof FieldType)[keyof typeof FieldType];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a packet, which can be dissected into segments and fields. This is a placeholder
|
* Interface for a packet, which can be dissected into segments and fields. This is a placeholder
|
||||||
* for any additional properties or methods that may be needed for representing a packet in the future.
|
* for any additional properties or methods that may be needed for representing a packet in the future.
|
||||||
*/
|
*/
|
||||||
export interface Packet {
|
export interface Packet {
|
||||||
data: string | Uint8Array | ArrayBuffer; // Raw packet data as an ArrayBuffer
|
data: string | Uint8Array | ArrayBuffer; // Raw packet data as an ArrayBuffer
|
||||||
snr?: number; // Optional signal-to-noise ratio (for radio packets)
|
snr?: number; // Optional signal-to-noise ratio (for radio packets)
|
||||||
rssi?: number; // Optional received signal strength indicator (for radio packets)
|
rssi?: number; // Optional received signal strength indicator (for radio packets)
|
||||||
parsed?: unknown; // Optional parsed representation of the packet (e.g., a structured object)
|
parsed?: unknown; // Optional parsed representation of the packet (e.g., a structured object)
|
||||||
dissected?: Dissected; // Optional dissected representation of the packet (array of segments)
|
dissected?: Dissected; // Optional dissected representation of the packet (array of segments)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to dissect the packet into segments and fields, returning an array of segments.
|
* Method to dissect the packet into segments and fields, returning an array of segments.
|
||||||
@@ -108,8 +107,9 @@ export type Dissected = Segment[];
|
|||||||
* Each field specifies the type and length of data it represents.
|
* Each field specifies the type and length of data it represents.
|
||||||
*/
|
*/
|
||||||
export interface Segment {
|
export interface Segment {
|
||||||
name: string;
|
name: string;
|
||||||
data?: ArrayBuffer; // Optional raw data for the segment (if needed for parsing / serialization)
|
data?: ArrayBuffer; // Optional raw data for the segment (if needed for parsing / serialization)
|
||||||
|
isString?: boolean; // Optional flag indicating if the segment represents a string (for special handling)
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +118,12 @@ export interface Segment {
|
|||||||
* for bit fields and array lengths.
|
* for bit fields and array lengths.
|
||||||
*/
|
*/
|
||||||
export interface Field {
|
export interface Field {
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
name: string;
|
name: string;
|
||||||
value?: unknown; // Optional value for the field (used for serialization or as a default value)
|
data?: ArrayBuffer; // Optional raw data for the field (if needed for parsing / serialization)
|
||||||
bits?: BitField[]; // Optional array of bit field definitions (for BITS type)
|
value?: unknown; // Optional value for the field (used for serialization or as a default value)
|
||||||
length?: number; // Optional length for array types (e.g., BYTES, WORDS)
|
bits?: BitField[]; // Optional array of bit field definitions (for BITS type)
|
||||||
|
length?: number; // Optional length for array types (e.g., BYTES, WORDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,6 +132,6 @@ export interface Field {
|
|||||||
*/
|
*/
|
||||||
export interface BitField {
|
export interface BitField {
|
||||||
name: string;
|
name: string;
|
||||||
size: number; // Number of bits for this field (must be > 0)
|
size: number; // Number of bits for this field (must be > 0)
|
||||||
lsb?: boolean; // Optional flag indicating if this field is the least significant bit in the byte (for BITS type)
|
lsb?: boolean; // Optional flag indicating if this field is the least significant bit in the byte (for BITS type)
|
||||||
}
|
}
|
||||||
|
|||||||
224
src/utils.test.ts
Normal file
224
src/utils.test.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
U8,
|
||||||
|
U16,
|
||||||
|
U32,
|
||||||
|
I8,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
F32,
|
||||||
|
F64,
|
||||||
|
isBytes,
|
||||||
|
assertBytes,
|
||||||
|
bytesToHex,
|
||||||
|
hexToBytes,
|
||||||
|
bytesToBase64,
|
||||||
|
base64ToBytes,
|
||||||
|
decodeBytes,
|
||||||
|
encodeBytes,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
describe('utils', () => {
|
||||||
|
const staticHexVectors = [
|
||||||
|
{ bytes: Uint8Array.from([]), hex: '' },
|
||||||
|
{ bytes: Uint8Array.from([0xbe]), hex: 'be' },
|
||||||
|
{ bytes: Uint8Array.from([0xca, 0xfe]), hex: 'cafe' },
|
||||||
|
{ bytes: Uint8Array.from(new Array(1024).fill(0x69)), hex: '69'.repeat(1024) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buf = new ArrayBuffer(8);
|
||||||
|
const v8 = new Uint8Array(buf);
|
||||||
|
for (let i = 0; i < v8.length; i++) v8[i] = i + 1;
|
||||||
|
|
||||||
|
it('U8 from ArrayBuffer', () => {
|
||||||
|
const a8 = U8(buf);
|
||||||
|
expect(a8).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(Array.from(a8)).toEqual([1,2,3,4,5,6,7,8]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('U8 from Uint8Array view', () => {
|
||||||
|
const sub = new Uint8Array(buf, 2, 4);
|
||||||
|
const s8 = U8(sub);
|
||||||
|
expect(Array.from(s8)).toEqual([3,4,5,6]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('U16 from ArrayBuffer', () => {
|
||||||
|
const a16 = U16(buf);
|
||||||
|
expect(a16).toBeInstanceOf(Uint16Array);
|
||||||
|
expect(a16.length).toBe(Math.floor(buf.byteLength / 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('U16 from Uint8Array view', () => {
|
||||||
|
const sub = new Uint8Array(buf, 2, 4);
|
||||||
|
const s16 = U16(sub);
|
||||||
|
expect(s16.length).toBe(Math.floor(sub.byteLength / 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('U32 from ArrayBuffer', () => {
|
||||||
|
const a32 = U32(buf);
|
||||||
|
expect(a32).toBeInstanceOf(Uint32Array);
|
||||||
|
expect(a32.length).toBe(Math.floor(buf.byteLength / 4));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('U32 from Uint8Array view', () => {
|
||||||
|
const sub = new Uint8Array(buf, 4, 4);
|
||||||
|
const s32 = U32(sub);
|
||||||
|
expect(s32.length).toBe(Math.floor(sub.byteLength / 4));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('I8 from ArrayBuffer and view', () => {
|
||||||
|
const buf = new ArrayBuffer(4);
|
||||||
|
const dv = new DataView(buf);
|
||||||
|
dv.setInt8(0, -5);
|
||||||
|
dv.setInt8(1, 120);
|
||||||
|
dv.setInt8(2, -128);
|
||||||
|
dv.setInt8(3, 127);
|
||||||
|
|
||||||
|
const a = I8(buf);
|
||||||
|
expect(Array.from(a)).toEqual([-5, 120, -128, 127]);
|
||||||
|
|
||||||
|
const sub = new Int8Array(buf, 1, 2);
|
||||||
|
const s = I8(sub);
|
||||||
|
expect(Array.from(s)).toEqual([120, -128]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('I16 from ArrayBuffer and misaligned view throws', () => {
|
||||||
|
const buf = new ArrayBuffer(6);
|
||||||
|
const dv = new DataView(buf);
|
||||||
|
dv.setInt16(0, -12345, true);
|
||||||
|
dv.setInt16(2, 12345, true);
|
||||||
|
dv.setInt16(4, 32767, true);
|
||||||
|
|
||||||
|
const a16 = I16(buf);
|
||||||
|
expect(a16.length).toBe(3);
|
||||||
|
expect(a16[0]).toBe(-12345);
|
||||||
|
expect(a16[1]).toBe(12345);
|
||||||
|
expect(a16[2]).toBe(32767);
|
||||||
|
|
||||||
|
const view = new Uint8Array(buf, 1, 4);
|
||||||
|
expect(() => I16(view)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('I32 from ArrayBuffer and misaligned view throws', () => {
|
||||||
|
const buf = new ArrayBuffer(12);
|
||||||
|
const dv = new DataView(buf);
|
||||||
|
dv.setInt32(0, -0x1234567, true);
|
||||||
|
dv.setInt32(4, 0x1234567, true);
|
||||||
|
|
||||||
|
const a32 = I32(buf);
|
||||||
|
expect(a32[0]).toBe(-0x1234567);
|
||||||
|
expect(a32[1]).toBe(0x1234567);
|
||||||
|
|
||||||
|
const view = new Uint8Array(buf, 1, 8);
|
||||||
|
expect(() => I32(view)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('F32/F64 from ArrayBuffer', () => {
|
||||||
|
const buf = new ArrayBuffer(16);
|
||||||
|
const dv = new DataView(buf);
|
||||||
|
dv.setFloat32(0, 3.14, true);
|
||||||
|
dv.setFloat32(4, -2.5, true);
|
||||||
|
dv.setFloat64(8, 1.23456789e3, true);
|
||||||
|
|
||||||
|
const f32 = F32(buf);
|
||||||
|
expect(f32[0]).toBeCloseTo(3.14, 5);
|
||||||
|
expect(f32[1]).toBeCloseTo(-2.5, 5);
|
||||||
|
|
||||||
|
const f64 = F64(buf);
|
||||||
|
expect(f64[1]).toBeCloseTo(1.23456789e3, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isBytes', () => {
|
||||||
|
expect(isBytes(new Uint8Array([1,2]))).toBe(true);
|
||||||
|
expect(isBytes(new Uint16Array([1,2]) as unknown)).toBe(false);
|
||||||
|
expect(isBytes(new ArrayBuffer(2) as unknown)).toBe(false);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('assertBytes', () => {
|
||||||
|
// assertBytes accepts a Uint8Array
|
||||||
|
expect(() => assertBytes(new Uint8Array([1,2]))).not.toThrow();
|
||||||
|
// assertBytes with length mismatch throws
|
||||||
|
expect(() => assertBytes(new Uint8Array([1,2]), 3)).toThrow();
|
||||||
|
// assertBytes with wrong type throws
|
||||||
|
expect(() => assertBytes(new Uint16Array([1,2]) as any)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hexToBytes / bytesToHex roundtrip', () => {
|
||||||
|
for (const v of staticHexVectors) {
|
||||||
|
expect(hexToBytes(v.hex)).toEqual(v.bytes);
|
||||||
|
expect(hexToBytes(v.hex.toUpperCase())).toEqual(v.bytes);
|
||||||
|
expect(bytesToHex(v.bytes)).toEqual(v.hex);
|
||||||
|
// encode -> decode
|
||||||
|
const h = bytesToHex(v.bytes);
|
||||||
|
expect(hexToBytes(h)).toEqual(v.bytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('base64 encodings roundtrip and variants', () => {
|
||||||
|
const hello = new Uint8Array([72,101,108,108,111]); // 'Hello'
|
||||||
|
const b64 = bytesToBase64(hello, 'base64');
|
||||||
|
expect(base64ToBytes(b64)).toEqual(hello);
|
||||||
|
|
||||||
|
const b64raw = bytesToBase64(hello, 'base64raw');
|
||||||
|
expect(base64ToBytes(b64raw)).toEqual(hello);
|
||||||
|
|
||||||
|
const b64url = bytesToBase64(hello, 'base64url');
|
||||||
|
expect(base64ToBytes(b64url)).toEqual(hello);
|
||||||
|
|
||||||
|
const b64urlraw = bytesToBase64(hello, 'base64urlraw');
|
||||||
|
expect(base64ToBytes(b64urlraw)).toEqual(hello);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodeBytes / decodeBytes round robin across encodings', () => {
|
||||||
|
const bytes = Uint8Array.from([0xde,0xad,0xbe,0xef]);
|
||||||
|
const encodings = ['hex','base64','base64raw','base64url','base64urlraw'] as const;
|
||||||
|
for (const enc of encodings) {
|
||||||
|
const s = encodeBytes(bytes, enc as any);
|
||||||
|
const out = decodeBytes(s, enc as any);
|
||||||
|
expect(out).toEqual(bytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on invalid inputs for hex/base64 decoders', () => {
|
||||||
|
expect(() => hexToBytes('z')).toThrow();
|
||||||
|
expect(() => base64ToBytes('??')).toThrow();
|
||||||
|
expect(() => decodeBytes('abc', 'unsupported' as any)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edge cases: offsets, odd lengths, url base64, empty and large roundtrips', () => {
|
||||||
|
// U16/U32 with subarray offset
|
||||||
|
const buf = new ArrayBuffer(6);
|
||||||
|
const v = new Uint8Array(buf);
|
||||||
|
v.set([1,2,3,4,5,6]);
|
||||||
|
const view = new Uint8Array(buf, 1, 4); // [2,3,4,5]
|
||||||
|
const u8 = U8(view);
|
||||||
|
expect(Array.from(u8)).toEqual([2,3,4,5]);
|
||||||
|
// Unaligned views cannot be reinterpreted as 16-bit/32-bit arrays on some
|
||||||
|
// platforms; U16/U32 will throw for misaligned offsets. Assert that behavior.
|
||||||
|
expect(() => U16(view)).toThrow();
|
||||||
|
|
||||||
|
// hexToBytes with odd length should throw
|
||||||
|
expect(() => hexToBytes('f')).toThrow();
|
||||||
|
|
||||||
|
// base64 url-safe without padding should be accepted
|
||||||
|
const sample = new Uint8Array([0x01,0x02,0x03,0xff]);
|
||||||
|
const raw = bytesToBase64(sample, 'base64raw');
|
||||||
|
// url variant
|
||||||
|
const url = bytesToBase64(sample, 'base64urlraw');
|
||||||
|
expect(base64ToBytes(raw)).toEqual(sample);
|
||||||
|
expect(base64ToBytes(url)).toEqual(sample);
|
||||||
|
|
||||||
|
// empty inputs
|
||||||
|
expect(bytesToHex(new Uint8Array([]))).toBe('');
|
||||||
|
expect(bytesToBase64(new Uint8Array([]), 'base64')).toBe('');
|
||||||
|
|
||||||
|
// large random roundtrip
|
||||||
|
const large = new Uint8Array(10000);
|
||||||
|
for (let i = 0; i < large.length; i++) large[i] = i & 0xff;
|
||||||
|
const h = bytesToHex(large);
|
||||||
|
expect(hexToBytes(h)).toEqual(large);
|
||||||
|
const b64 = bytesToBase64(large, 'base64raw');
|
||||||
|
expect(base64ToBytes(b64)).toEqual(large);
|
||||||
|
});
|
||||||
|
});
|
||||||
358
src/utils.ts
Normal file
358
src/utils.ts
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
// TypedArray is a union type of all the standard typed array types in JavaScript. This allows us to write
|
||||||
|
// functions that can accept any typed array or an ArrayBuffer and convert it to the appropriate view.
|
||||||
|
export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | Uint16Array | Int16Array | Uint32Array | Int32Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to a Uint8Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns A Uint8Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const U8 = (arr: ArrayBuffer | TypedArray): Uint8Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Uint8Array(arr);
|
||||||
|
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to an Int8Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns An Int8Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const I8 = (arr: ArrayBuffer | TypedArray): Int8Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Int8Array(arr);
|
||||||
|
return new Int8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to a Uint16Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 2 bytes per value in a Uint16Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns A Uint16Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const U16 = (arr: ArrayBuffer | TypedArray): Uint16Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Uint16Array(arr);
|
||||||
|
return new Uint16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to an Int16Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 2 bytes per value in an Int16Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns An Int16Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const I16 = (arr: ArrayBuffer | TypedArray): Int16Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Int16Array(arr);
|
||||||
|
return new Int16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to a Uint32Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 4 bytes per value in a Uint32Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns A Uint32Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const U32 = (arr: ArrayBuffer | TypedArray): Uint32Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Uint32Array(arr);
|
||||||
|
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to an Int32Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 4 bytes per value in an Int32Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns An Int32Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const I32 = (arr: ArrayBuffer | TypedArray): Int32Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Int32Array(arr);
|
||||||
|
return new Int32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to a Float32Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 4 bytes per value in a Float32Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns A Float32Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const F32 = (arr: ArrayBuffer | TypedArray): Float32Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Float32Array(arr);
|
||||||
|
return new Float32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given ArrayBuffer or TypedArray to a Float64Array. If a TypedArray is provided,
|
||||||
|
* the byte length is adjusted to account for the 8 bytes per value in a Float64Array.
|
||||||
|
*
|
||||||
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
||||||
|
* @returns A Float64Array representing the same data.
|
||||||
|
*/
|
||||||
|
export const F64 = (arr: ArrayBuffer | TypedArray): Float64Array => {
|
||||||
|
if (arr instanceof ArrayBuffer) return new Float64Array(arr);
|
||||||
|
return new Float64Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the provided value is a Uint8Array or a view of an ArrayBuffer that is a
|
||||||
|
* Uint8Array. This is used to validate that a given value can be treated as raw bytes for
|
||||||
|
* encoding/decoding operations.
|
||||||
|
*
|
||||||
|
* @param bytes The value to test.
|
||||||
|
* @returns True if the value is a Uint8Array or a view of an ArrayBuffer that is a Uint8Array, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isBytes = (bytes: unknown): boolean => {
|
||||||
|
return bytes instanceof Uint8Array || (ArrayBuffer.isView(bytes) && bytes.constructor.name === "Uint8Array");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the provided value is a Uint8Array of the specified length (if provided). Throws a
|
||||||
|
* TypeError if the assertion fails.
|
||||||
|
*
|
||||||
|
* @param bytes The value to check.
|
||||||
|
* @param length The expected length of the Uint8Array.
|
||||||
|
* @param name The name of the variable being checked (for error messages).
|
||||||
|
*/
|
||||||
|
export const assertBytes = (bytes: Uint8Array, length?: number, name: string = ""): void => {
|
||||||
|
const valid = isBytes(bytes);
|
||||||
|
const sized = (typeof length !== 'undefined') ? (bytes.byteLength === length) : true;
|
||||||
|
if (!valid || !sized) {
|
||||||
|
const expected = typeof length !== 'undefined' ? `Uint8Array of length ${length}` : 'Uint8Array';
|
||||||
|
const actual = valid ? `Uint8Array of length ${bytes.byteLength}` : typeof bytes;
|
||||||
|
throw new TypeError(`Expected ${name} to be ${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for equality of two Uint8Arrays. Returns true if the arrays are equal, false otherwise.
|
||||||
|
*
|
||||||
|
* Note: This function is not designed to be resistant to timing attacks. For security-sensitive
|
||||||
|
* comparisons (e.g., cryptographic keys, hashes), use `constantTimeEqualBytes` instead.
|
||||||
|
*
|
||||||
|
* @param a The first Uint8Array to compare.
|
||||||
|
* @param b The second Uint8Array to compare.
|
||||||
|
* @returns True if the arrays are equal, false otherwise.
|
||||||
|
*/
|
||||||
|
export const equalBytes = (a: Uint8Array, b: Uint8Array): boolean => {
|
||||||
|
if (a.byteLength !== b.byteLength) return false;
|
||||||
|
for (let i = 0; i < a.byteLength; i++) {
|
||||||
|
if (a[i] !== b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a constant-time comparison of two Uint8Arrays to prevent timing attacks. Returns true
|
||||||
|
* if the arrays are equal, false otherwise.
|
||||||
|
*
|
||||||
|
* This function should be used for security-sensitive comparisons (e.g., cryptographic keys,
|
||||||
|
* hashes) to mitigate timing attacks. For general use where performance is a concern and security
|
||||||
|
* is not an issue, `equalBytes` may be more efficient.
|
||||||
|
*
|
||||||
|
* NB: This function is not truly constant-time in JavaScript due to the nature of the language and
|
||||||
|
* runtime, but it is designed to minimize timing differences based on the content of the arrays.
|
||||||
|
*
|
||||||
|
* @param a The first Uint8Array to compare.
|
||||||
|
* @param b The second Uint8Array to compare.
|
||||||
|
* @returns True if the arrays are equal, false otherwise.
|
||||||
|
*/
|
||||||
|
export const constantTimeEqualBytes = (a: Uint8Array, b: Uint8Array): boolean => {
|
||||||
|
if (a.byteLength !== b.byteLength) return false;
|
||||||
|
let result = 0;
|
||||||
|
for (let i = 0; i < a.byteLength; i++) {
|
||||||
|
result |= a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
return result === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHexMethods = (() =>
|
||||||
|
// @ts-ignore testing for builtins
|
||||||
|
'toHex' in Uint8Array.from([]) && typeof Uint8Array.from([])['toHex'] === 'function' &&
|
||||||
|
'fromHex' in Uint8Array && typeof Uint8Array.fromHex === 'function')();
|
||||||
|
|
||||||
|
// Array where index 0xf0 (240) is mapped to string 'f0'
|
||||||
|
const hexes = Array.from({ length: 256 }, (_, i) =>
|
||||||
|
i.toString(16).padStart(2, '0')
|
||||||
|
);
|
||||||
|
|
||||||
|
export type HexEncoding = 'hex';
|
||||||
|
export type Base64Encoding = 'base64';
|
||||||
|
export type Base64RawEncoding = 'base64raw';
|
||||||
|
export type Base64URLEncoding = 'base64url';
|
||||||
|
export type Base64RawURLEncoding = 'base64urlraw';
|
||||||
|
export type Encoding = HexEncoding | Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Uint8Array of bytes to a hexadecimal string. If the environment supports built-in
|
||||||
|
* hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual
|
||||||
|
* conversion is performed.
|
||||||
|
*
|
||||||
|
* If a length is provided, the function will assert that the input bytes have the expected
|
||||||
|
* length (in bytes). The length parameter is optional and can be used to enforce that the
|
||||||
|
* input bytes match an expected size for encoding/decoding operations.
|
||||||
|
*
|
||||||
|
* @param bytes The Uint8Array of bytes to convert to a hexadecimal string.
|
||||||
|
* @param length Optional expected length of the input bytes (in bytes).
|
||||||
|
* @returns The hexadecimal string representation of the input bytes.
|
||||||
|
*/
|
||||||
|
export const bytesToHex = (bytes: Uint8Array, length?: number): string => {
|
||||||
|
assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes");
|
||||||
|
if (hasHexMethods) {
|
||||||
|
// @ts-ignore using built-in hex methods if available
|
||||||
|
return bytes.toHex();
|
||||||
|
}
|
||||||
|
let hex = '';
|
||||||
|
for (const byte of bytes) {
|
||||||
|
hex += hexes[byte];
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hexadecimal string to a Uint8Array of bytes. If the environment supports built-in
|
||||||
|
* hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual
|
||||||
|
* conversion is performed.
|
||||||
|
*
|
||||||
|
* @param hex The hexadecimal string to convert to a Uint8Array of bytes.
|
||||||
|
* @returns The Uint8Array of bytes represented by the hexadecimal string.
|
||||||
|
*/
|
||||||
|
export const hexToBytes = (hex: string): Uint8Array => {
|
||||||
|
if (typeof hex !== 'string' || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
|
||||||
|
throw new TypeError(`Expected hex string of even length, got ${hex}`);
|
||||||
|
}
|
||||||
|
if (hasHexMethods) {
|
||||||
|
// @ts-ignore using built-in hex methods if available
|
||||||
|
return Uint8Array.fromHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Uint8Array of bytes to a base64 string using the specified encoding variant. The function
|
||||||
|
* asserts that the input is a valid Uint8Array and optionally checks for an expected length (in bytes).
|
||||||
|
*
|
||||||
|
* The `encoding` parameter specifies the base64 variant to use:
|
||||||
|
* - 'base64': Standard base64 encoding with padding.
|
||||||
|
* - 'base64raw': Standard base64 encoding without padding.
|
||||||
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
||||||
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
||||||
|
*
|
||||||
|
* @param bytes The Uint8Array of bytes to convert to a base64 string.
|
||||||
|
* @param encoding The base64 encoding variant to use.
|
||||||
|
* @returns The base64 string representation of the input bytes.
|
||||||
|
*/
|
||||||
|
export const bytesToBase64 = (bytes: Uint8Array, encoding: Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding): string => {
|
||||||
|
assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes");
|
||||||
|
const binary = String.fromCharCode(...bytes);
|
||||||
|
switch (encoding) {
|
||||||
|
case 'base64':
|
||||||
|
return btoa(binary);
|
||||||
|
case 'base64raw':
|
||||||
|
return btoa(binary).replace(/=+$/, '');
|
||||||
|
case 'base64url':
|
||||||
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
case 'base64urlraw':
|
||||||
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
default:
|
||||||
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a base64 string to a Uint8Array of bytes. The function accepts both standard and URL-safe
|
||||||
|
* base64 variants, and normalizes the input by replacing URL-safe characters and adding padding if necessary.
|
||||||
|
*
|
||||||
|
* The function asserts that the input is a valid base64 string after normalization. It then decodes the
|
||||||
|
* base64 string to binary and converts it to a Uint8Array of bytes.
|
||||||
|
*
|
||||||
|
* @param b64 The base64 string to convert to a Uint8Array of bytes.
|
||||||
|
* @returns The Uint8Array of bytes represented by the base64 string.
|
||||||
|
*/
|
||||||
|
export const base64ToBytes = (b64: string): Uint8Array => {
|
||||||
|
if (typeof b64 !== "string") {
|
||||||
|
throw new TypeError(`Expected base64 string, got ${b64}`);
|
||||||
|
}
|
||||||
|
// Accept URL-safe base64 by replacing '-' with '+' and '_' with '/'
|
||||||
|
let normalized = b64.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
// Pad with '=' to make length a multiple of 4
|
||||||
|
if (normalized.length % 4 !== 0) {
|
||||||
|
normalized += "=".repeat(4 - (normalized.length % 4));
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
||||||
|
throw new TypeError(`Expected base64 string, got ${b64}`);
|
||||||
|
}
|
||||||
|
const binary = atob(normalized);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a string encoded in the specified encoding variant to a Uint8Array of bytes. The function
|
||||||
|
* supports the following encoding variants:
|
||||||
|
* - 'hex': Hexadecimal encoding (e.g., "deadbeef").
|
||||||
|
* - 'base64': Standard base64 encoding with padding.
|
||||||
|
* - 'base64raw': Standard base64 encoding without padding.
|
||||||
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
||||||
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
||||||
|
*
|
||||||
|
* The function asserts that the input string is valid for the specified encoding and then decodes
|
||||||
|
* it to a Uint8Array of bytes. If the encoding is not supported, a TypeError is thrown.
|
||||||
|
*
|
||||||
|
* @param encoded The encoded string to decode.
|
||||||
|
* @param encoding The encoding variant of the input string.
|
||||||
|
* @returns The Uint8Array of bytes represented by the encoded string.
|
||||||
|
*/
|
||||||
|
export const decodeBytes = (encoded: string, encoding: Encoding): Uint8Array => {
|
||||||
|
switch (encoding) {
|
||||||
|
case 'hex':
|
||||||
|
return hexToBytes(encoded);
|
||||||
|
case 'base64':
|
||||||
|
case 'base64raw':
|
||||||
|
case 'base64url':
|
||||||
|
case 'base64urlraw':
|
||||||
|
return base64ToBytes(encoded);
|
||||||
|
default:
|
||||||
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a Uint8Array of bytes to a string using the specified encoding variant. The function supports
|
||||||
|
* the following encoding variants:
|
||||||
|
* - 'hex': Hexadecimal encoding (e.g., "deadbeef").
|
||||||
|
* - 'base64': Standard base64 encoding with padding.
|
||||||
|
* - 'base64raw': Standard base64 encoding without padding.
|
||||||
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
||||||
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
||||||
|
*
|
||||||
|
* The function asserts that the input is a valid Uint8Array of bytes and then encodes it to a string
|
||||||
|
* using the specified encoding variant. If the encoding is not supported, a TypeError is thrown.
|
||||||
|
*
|
||||||
|
* @param bytes The Uint8Array of bytes to encode.
|
||||||
|
* @param encoding The encoding variant to use.
|
||||||
|
* @returns The encoded string.
|
||||||
|
*/
|
||||||
|
export const encodeBytes = (bytes: Uint8Array, encoding: Encoding): string => {
|
||||||
|
switch (encoding) {
|
||||||
|
case 'hex':
|
||||||
|
return bytesToHex(bytes);
|
||||||
|
case 'base64':
|
||||||
|
case 'base64raw':
|
||||||
|
case 'base64url':
|
||||||
|
case 'base64urlraw':
|
||||||
|
return bytesToBase64(bytes, encoding);
|
||||||
|
default:
|
||||||
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user