From 98597b56eb6eeb029b72ea963649155dfe1f4215 Mon Sep 17 00:00:00 2001 From: Phroi <90913182+phroi@users.noreply.github.com> Date: Mon, 12 Jan 2026 09:05:02 +0100 Subject: [PATCH] feat(core): Add `bytesLen` and `bytesLenUnsafe` utilities (#353) --- .changeset/eighty-terms-cry.md | 5 ++ packages/core/src/hex/index.test.ts | 71 +++++++++++++++++++++++++++++ packages/core/src/hex/index.ts | 50 ++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 .changeset/eighty-terms-cry.md create mode 100644 packages/core/src/hex/index.test.ts diff --git a/.changeset/eighty-terms-cry.md b/.changeset/eighty-terms-cry.md new file mode 100644 index 000000000..ba95dd46a --- /dev/null +++ b/.changeset/eighty-terms-cry.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": minor +--- + +Add `bytesLen` and `bytesLenUnsafe` utilities diff --git a/packages/core/src/hex/index.test.ts b/packages/core/src/hex/index.test.ts new file mode 100644 index 000000000..93417a700 --- /dev/null +++ b/packages/core/src/hex/index.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "vitest"; +import { bytesLen, bytesLenUnsafe, hexFrom } from "./index.js"; + +describe("hexFrom", () => { + const cases: [string, Parameters[0], string][] = [ + ["normalized hex", "0x1234", "0x1234"], + ["empty hex", "0x", "0x"], + ["odd-length prefixed hex", "0x123", "0x0123"], + ["hex without prefix", "1234", "0x1234"], + ["odd-length hex without prefix", "123", "0x0123"], + ["uppercase hex", "0xABCD", "0xabcd"], + ["Uint8Array", new Uint8Array([0x12, 0x34]), "0x1234"], + ["ArrayBuffer", new Uint8Array([0x12, 0x34, 0x56]).buffer, "0x123456"], + ["number array", [0x12, 0x34, 0x56], "0x123456"], + ]; + + cases.forEach(([name, input, expected]) => + test(`returns ${expected} for ${name}`, () => { + expect(hexFrom(input)).toBe(expected); + }), + ); + + test("throws for invalid hex string", () => { + expect(() => hexFrom("0xzz")).toThrow("Invalid bytes 0xzz"); + }); + + test("throws for invalid byte values", () => { + expect(() => hexFrom([256])).toThrow("Invalid bytes [256]"); + }); +}); + +describe("bytesLen", () => { + const cases: [string, Parameters[0], number][] = [ + ["normalized hex", "0x1234", 2], + ["empty hex", "0x", 0], + ["odd-length hex", "0x123", 2], + ["hex without prefix", "123", 2], + ["uppercase hex", "0xABCD", 2], + ["Uint8Array", new Uint8Array([1, 2, 3]), 3], + ["ArrayBuffer", new Uint8Array([1, 2, 3, 4]).buffer, 4], + ["number array", [1, 2, 3, 4, 5], 5], + ]; + + cases.forEach(([name, input, expected]) => + test(`returns ${expected} for ${name}`, () => { + expect(bytesLen(input)).toBe(expected); + }), + ); + + test("throws for invalid hex string", () => { + expect(() => bytesLen("0xzz")).toThrow("Invalid bytes 0xzz"); + }); + + test("throws for invalid byte values", () => { + expect(() => bytesLen([256])).toThrow("Invalid bytes [256]"); + }); +}); + +describe("bytesLenUnsafe", () => { + const cases: [string, `0x${string}`, number][] = [ + ["empty hex", "0x", 0], + ["even-length hex", "0x1234", 2], + ["odd-length hex", "0x123", 2], + ]; + + cases.forEach(([name, input, expected]) => + test(`returns ${expected} for ${name}`, () => { + expect(bytesLenUnsafe(input)).toBe(expected); + }), + ); +}); diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 497b729e6..22a27afd5 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -58,3 +58,53 @@ export function hexFrom(hex: HexLike): Hex { return `0x${bytesTo(bytesFrom(hex), "hex")}`; } + +/** + * Return the number of bytes occupied by `hexLike`. + * + * @param hexLike - Hex-like value + * @returns Byte length of `hexLike`. + * + * @example + * ```typescript + * bytesLen("0x48656c6c6f") // 5 + * bytesLen(new Uint8Array([1, 2, 3])) // 3 + * bytesLen(new ArrayBuffer(4)) // 4 + * bytesLen([1, 2]) // 2 + * ``` + * + * @see bytesLenUnsafe - Fast version for already-normalized Hex strings + * + * @note Prefer direct `.length`/`.byteLength` access on Uint8Array/ArrayBuffer when you already have bytes. + * Use `bytesLen()` only when you need length without performing additional operations. + * @see bytesFrom - Convert values to Bytes (Uint8Array) + */ +export function bytesLen(hexLike: HexLike): number { + if (isNormalizedHex(hexLike)) { + return bytesLenUnsafe(hexLike); + } + + return bytesFrom(hexLike).length; +} + +/** + * Fast byte length for Hex strings. + * + * This function efficiently calculates the byte length of Hex values: + * - Skips validation (caller must ensure input is valid Hex) + * - Handles odd-digit hex as if it were padded with a leading zero (e.g., "0x123" is treated as "0x0123"). + * + * @param hex - A valid Hex string (with "0x" prefix). + * @returns Byte length of the hex string. + * + * @example + * ```typescript + * bytesLenUnsafe("0x48656c6c6f") // 5 + * bytesLenUnsafe("0x123") // 2 (odd digits round up via padding) + * ``` + * + * @see bytesLen - Validated version for untrusted input + */ +export function bytesLenUnsafe(hex: Hex): number { + return Math.floor((hex.length - 1) / 2); +}