Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-terms-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-ccc/core": minor
---

Add `bytesLen` and `bytesLenUnsafe` utilities
71 changes: 71 additions & 0 deletions packages/core/src/hex/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, test } from "vitest";
import { bytesLen, bytesLenUnsafe, hexFrom } from "./index.js";

describe("hexFrom", () => {
const cases: [string, Parameters<typeof hexFrom>[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<typeof bytesLen>[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);
}),
);
});
50 changes: 50 additions & 0 deletions packages/core/src/hex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
Hanssen0 marked this conversation as resolved.

/**
* 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);
}