Skip to content

Commit

Permalink
feat: implement isomorphic utils for nodejs and browser (#7060)
Browse files Browse the repository at this point in the history
* feat: implement isomorphic utils for nodes and browser

* fix: avoid async import

* chore: revise toHexString() comment as in PR review

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
  • Loading branch information
2 people authored and philknows committed Sep 3, 2024
1 parent 9758d48 commit b792bc6
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 35 deletions.
33 changes: 3 additions & 30 deletions packages/utils/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {toBufferLE, toBigIntLE, toBufferBE, toBigIntBE} from "bigint-buffer";
type Endianness = "le" | "be";

const hexByByte: string[] = [];
/**
* @deprecated Use toHex() instead.
*/
export function toHexString(bytes: Uint8Array): string {
let hex = "0x";
for (const byte of bytes) {
Expand Down Expand Up @@ -45,33 +48,3 @@ export function bytesToBigInt(value: Uint8Array, endianness: Endianness = "le"):
}
throw new Error("endianness must be either 'le' or 'be'");
}

export function toHex(buffer: Uint8Array | Parameters<typeof Buffer.from>[0]): string {
if (Buffer.isBuffer(buffer)) {
return "0x" + buffer.toString("hex");
} else if (buffer instanceof Uint8Array) {
return "0x" + Buffer.from(buffer.buffer, buffer.byteOffset, buffer.length).toString("hex");
} else {
return "0x" + Buffer.from(buffer).toString("hex");
}
}

// Shared buffer to convert root to hex
const rootBuf = Buffer.alloc(32);

/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

rootBuf.set(root);
return `0x${rootBuf.toString("hex")}`;
}

export function fromHex(hex: string): Uint8Array {
const b = Buffer.from(hex.replace("0x", ""), "hex");
return new Uint8Array(b.buffer, b.byteOffset, b.length);
}
66 changes: 66 additions & 0 deletions packages/utils/src/bytes/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export function toHex(bytes: Uint8Array): string {
const charCodes = new Array<number>(bytes.length * 2 + 2);
charCodes[0] = 48;
charCodes[1] = 120;

for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
const first = (byte & 0xf0) >> 4;
const second = byte & 0x0f;

// "0".charCodeAt(0) = 48
// "a".charCodeAt(0) = 97 => delta = 87
charCodes[2 + 2 * i] = first < 10 ? first + 48 : first + 87;
charCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87;
}
return String.fromCharCode(...charCodes);
}

const rootCharCodes = new Array<number>(32 * 2 + 2);
// "0".charCodeAt(0)
rootCharCodes[0] = 48;
// "x".charCodeAt(0)
rootCharCodes[1] = 120;

/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

for (let i = 0; i < root.length; i++) {
const byte = root[i];
const first = (byte & 0xf0) >> 4;
const second = byte & 0x0f;

// "0".charCodeAt(0) = 48
// "a".charCodeAt(0) = 97 => delta = 87
rootCharCodes[2 + 2 * i] = first < 10 ? first + 48 : first + 87;
rootCharCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87;
}
return String.fromCharCode(...rootCharCodes);
}

export function fromHex(hex: string): Uint8Array {
if (typeof hex !== "string") {
throw new Error(`hex argument type ${typeof hex} must be of type string`);
}

if (hex.startsWith("0x")) {
hex = hex.slice(2);
}

if (hex.length % 2 !== 0) {
throw new Error(`hex string length ${hex.length} must be multiple of 2`);
}

const byteLen = hex.length / 2;
const bytes = new Uint8Array(byteLen);
for (let i = 0; i < byteLen; i++) {
const byte = parseInt(hex.slice(i * 2, (i + 1) * 2), 16);
bytes[i] = byte;
}
return bytes;
}
14 changes: 14 additions & 0 deletions packages/utils/src/bytes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {toHex as browserToHex, toRootHex as browserToRootHex, fromHex as browserFromHex} from "./browser.js";
import {toHex as nodeToHex, toRootHex as nodeToRootHex, fromHex as nodeFromHex} from "./nodejs.js";

let toHex = browserToHex;
let toRootHex = browserToRootHex;
let fromHex = browserFromHex;

if (typeof Buffer !== "undefined") {
toHex = nodeToHex;
toRootHex = nodeToRootHex;
fromHex = nodeFromHex;
}

export {toHex, toRootHex, fromHex};
33 changes: 33 additions & 0 deletions packages/utils/src/bytes/nodejs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export function toHex(buffer: Uint8Array | Parameters<typeof Buffer.from>[0]): string {
if (Buffer.isBuffer(buffer)) {
return "0x" + buffer.toString("hex");
} else if (buffer instanceof Uint8Array) {
return "0x" + Buffer.from(buffer.buffer, buffer.byteOffset, buffer.length).toString("hex");
} else {
return "0x" + Buffer.from(buffer).toString("hex");
}
}

// Shared buffer to convert root to hex
let rootBuf: Buffer | undefined;

/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

if (rootBuf === undefined) {
rootBuf = Buffer.alloc(32);
}

rootBuf.set(root);
return `0x${rootBuf.toString("hex")}`;
}

export function fromHex(hex: string): Uint8Array {
const b = Buffer.from(hex.replace("0x", ""), "hex");
return new Uint8Array(b.buffer, b.byteOffset, b.length);
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./yaml/index.js";
export * from "./assert.js";
export * from "./base64.js";
export * from "./bytes.js";
export * from "./bytes/index.js";
export * from "./command.js";
export * from "./err.js";
export * from "./errors.js";
Expand Down
38 changes: 35 additions & 3 deletions packages/utils/test/perf/bytes.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {itBench} from "@dapplion/benchmark";
import {toHex, toRootHex} from "../../src/bytes.js";
import {toHex, toRootHex} from "../../src/bytes/nodejs.js";
import {toHex as browserToHex, toRootHex as browserToRootHex} from "../../src/bytes/browser.js";
import {toHexString} from "../../src/bytes.js";

describe("bytes utils", function () {
const runsFactor = 1000;
const blockRoot = new Uint8Array(Array.from({length: 32}, (_, i) => i));

itBench({
id: "block root to RootHex using toHex",
id: "nodejs block root to RootHex using toHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toHex(blockRoot);
Expand All @@ -16,12 +18,42 @@ describe("bytes utils", function () {
});

itBench({
id: "block root to RootHex using toRootHex",
id: "nodejs block root to RootHex using toRootHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toRootHex(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using the deprecated toHexString",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toHexString(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using toHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
browserToHex(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using toRootHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
browserToRootHex(blockRoot);
}
},
runsFactor,
});
});
23 changes: 21 additions & 2 deletions packages/utils/test/unit/bytes.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {describe, it, expect} from "vitest";
import {intToBytes, bytesToInt, toHex, fromHex, toHexString} from "../../src/index.js";
import {intToBytes, bytesToInt, toHex, fromHex, toHexString, toRootHex} from "../../src/index.js";

describe("intToBytes", () => {
const zeroedArray = (length: number): number[] => Array.from({length}, () => 0);
Expand Down Expand Up @@ -48,7 +48,7 @@ describe("bytesToInt", () => {
});

describe("toHex", () => {
const testCases: {input: Buffer | Uint8Array | string; output: string}[] = [
const testCases: {input: Uint8Array; output: string}[] = [
{input: Buffer.from("Hello, World!", "utf-8"), output: "0x48656c6c6f2c20576f726c6421"},
{input: new Uint8Array([72, 101, 108, 108, 111]), output: "0x48656c6c6f"},
{input: Buffer.from([72, 101, 108, 108, 111]), output: "0x48656c6c6f"},
Expand All @@ -61,6 +61,25 @@ describe("toHex", () => {
}
});

describe("toRootHex", () => {
const testCases: {input: Uint8Array; output: string}[] = [
{
input: new Uint8Array(Array.from({length: 32}, (_, i) => i)),
output: "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
},
{
input: new Uint8Array(Array.from({length: 32}, () => 0)),
output: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
];

for (const {input, output} of testCases) {
it(`should convert root to hex string ${output}`, () => {
expect(toRootHex(input)).toBe(output);
});
}
});

describe("fromHex", () => {
const testCases: {input: string; output: Buffer | Uint8Array}[] = [
{
Expand Down

0 comments on commit b792bc6

Please sign in to comment.