Skip to content

Commit

Permalink
common: add byte handling functions
Browse files Browse the repository at this point in the history
  • Loading branch information
arobsn committed Jul 28, 2023
1 parent 3236dd8 commit 5a79c57
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .changeset/beige-zoos-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@fleet-sdk/common": minor
---

Add byte handling functions:

- `hexToBytes()` - converts hex string to bytes;
- `bytesToHex()` - converts bytes to hex string;
- `utf8ToBytes()` - converts utf-8 string to bytes;
- `bytesToUtf8()` - converts bytes to utf-8 string;
- `concatBytes()` - concatenates various byte arrays;
82 changes: 82 additions & 0 deletions packages/common/src/utils/bytes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it, test } from "vitest";
import { bytesToHex, bytesToUtf8, concatBytes, hexToBytes, utf8ToBytes } from "./bytes";

const ui8 = (bytes: number[]) => Uint8Array.from(bytes);

describe("Hex <> Bytes serialization", () => {
it("Should convert hex to bytes", () => {
expect(hexToBytes("deadbeef")).to.be.deep.equal(ui8([0xde, 0xad, 0xbe, 0xef]));
expect(hexToBytes("cafe123456")).to.be.deep.equal(ui8([0xca, 0xfe, 0x12, 0x34, 0x56]));
});

it("Should convert bytes to hex", () => {
expect(bytesToHex(ui8([0xde, 0xad, 0xbe, 0xef]))).to.be.equal("deadbeef");
expect(bytesToHex(ui8([0xca, 0xfe, 0x12, 0x34, 0x56]))).to.be.equal("cafe123456");
});

it("Should roundtrip", () => {
expect(bytesToHex(hexToBytes("deadbeef"))).to.be.deep.equal("deadbeef");
expect(hexToBytes(bytesToHex(ui8([0xca, 0xfe, 0x12, 0x34, 0x56])))).to.be.deep.equal(
ui8([0xca, 0xfe, 0x12, 0x34, 0x56])
);
});

test("Hex to byte with invalid inputs", () => {
expect(() => hexToBytes("non hex string")).to.throw("Invalid byte sequence");
expect(() => hexToBytes("0643d437ee7")).to.throw("Invalid hex padding");
expect(() => hexToBytes(1 as unknown as string)).to.throw(
"Expected an object of type 'string', got 'number'."
);
});

test("Bytes to hex with invalid inputs", () => {
const invalidBytes = [1, -2, 2, -55] as unknown as Uint8Array;
expect(() => bytesToHex(invalidBytes)).to.throw(
"Expected an instance of 'Uint8Array', got 'Array'."
);
});
});

describe("UTF-8 <> bytes serialization", () => {
it("Should roundtrip", () => {
expect(bytesToUtf8(utf8ToBytes("this is a regular string"))).to.be.equal(
"this is a regular string"
);
});

test("utf8 to bytes with invalid inputs", () => {
const notAString = true as unknown as string;
expect(() => utf8ToBytes(notAString)).to.throw(
"Expected an object of type 'string', got 'boolean'."
);
});

test("bytes to utf8 with invalid inputs", () => {
const invalidBytes = {} as unknown as Uint8Array;
expect(() => bytesToUtf8(invalidBytes)).to.throw(
"Expected an instance of 'Uint8Array', got 'Object'."
);
});
});

describe("Bytes concatenation", () => {
it("Should concat bytes", () => {
expect(concatBytes(ui8([0xde, 0xad]), ui8([0xbe, 0xef]))).to.be.deep.equal(
ui8([0xde, 0xad, 0xbe, 0xef])
);

expect(concatBytes(ui8([0xde, 0xad, 0xbe, 0xef]), ui8([]))).to.be.deep.equal(
ui8([0xde, 0xad, 0xbe, 0xef])
);

expect(concatBytes(ui8([]), ui8([0xde, 0xad, 0xbe, 0xef]))).to.be.deep.equal(
ui8([0xde, 0xad, 0xbe, 0xef])
);
});

it("Should fail with invalid inputs", () => {
expect(() => concatBytes({} as unknown as Uint8Array, ui8([]))).to.throw(
"Expected an instance of 'Uint8Array', got 'Object'."
);
});
});
59 changes: 59 additions & 0 deletions packages/common/src/utils/bytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { assert, assertInstanceOf, assertTypeOf } from ".";

export type StringEncoding = "hex" | "utf8";

const HEXES = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));

export function hexToBytes(hex: string): Uint8Array {
assertTypeOf(hex, "string");
assert(hex.length % 2 === 0, "Invalid hex padding.");

const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
const j = i * 2;
const hexByte = hex.slice(j, j + 2);
const byte = parseInt(hexByte, 16);
assert(!isNaN(byte) && byte >= 0, "Invalid byte sequence.");

bytes[i] = byte;
}

return bytes;
}

export function bytesToHex(bytes: Uint8Array): string {
assertInstanceOf(bytes, Uint8Array);

let hex = "";
for (let i = 0; i < bytes.length; i++) {
hex += HEXES[bytes[i]];
}

return hex;
}

export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));

let pad = 0;
for (const bytes of arrays) {
assertInstanceOf(bytes, Uint8Array);

r.set(bytes, pad);
pad += bytes.length;
}

return r;
}

export function utf8ToBytes(str: string): Uint8Array {
assertTypeOf(str, "string");

return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
}

export function bytesToUtf8(bytes: Uint8Array): string {
assertInstanceOf(bytes, Uint8Array);

return new TextDecoder().decode(bytes);
}

0 comments on commit 5a79c57

Please sign in to comment.