From 2a491808b483a237469ccee7976e402f9c1528e7 Mon Sep 17 00:00:00 2001 From: Hanssen0 <0@hanssen0.com> Date: Sat, 22 Aug 2026 06:57:40 +0800 Subject: [PATCH] feat(core)!: replace `Buffer`-based byte encoding with `uint8array-extras` --- .changeset/loose-cows-love.md | 8 +++++ packages/core/package.json | 5 +-- packages/core/src/bytes/advanced.ts | 7 +--- packages/core/src/bytes/index.test.ts | 34 +++++++++++++++++++ packages/core/src/bytes/index.ts | 47 +++++++++++++++++++++------ packages/core/src/hex/index.test.ts | 4 +-- packages/core/tsdown.config.mts | 1 + packages/playground/src/app/page.tsx | 4 +-- pnpm-lock.yaml | 14 ++------ 9 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 .changeset/loose-cows-love.md create mode 100644 packages/core/src/bytes/index.test.ts diff --git a/.changeset/loose-cows-love.md b/.changeset/loose-cows-love.md new file mode 100644 index 000000000..ef585b7bb --- /dev/null +++ b/.changeset/loose-cows-love.md @@ -0,0 +1,8 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core)!: replace `Buffer`-based byte encoding with `uint8array-extras` + +- Remove the legacy `ascii`, `binary`, `latin1`, `ucs2`, and `utf16le` encodings +- Use consistent strict validation for implicit and explicit hex input \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 2f7f68cfa..e0de20cf7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -63,9 +63,9 @@ "@noble/hashes": "^2.2.0", "bech32": "^2.0.0", "bs58check": "^4.0.0", - "buffer": "^6.0.3", "ethers": "^6.17.0", "isomorphic-ws": "^5.0.0", + "uint8array-extras": "^1.5.0", "ws": "^8.21.0" }, "packageManager": "pnpm@11.8.0", @@ -79,6 +79,7 @@ ], "base-x": "5.0.1", "bs58": "6.0.0", - "bs58check": "4.0.0" + "bs58check": "4.0.0", + "uint8array-extras": "1.5.0" } } diff --git a/packages/core/src/bytes/advanced.ts b/packages/core/src/bytes/advanced.ts index e59140310..79581e032 100644 --- a/packages/core/src/bytes/advanced.ts +++ b/packages/core/src/bytes/advanced.ts @@ -4,11 +4,6 @@ */ export type BytesFromEncoding = | "utf8" // UTF-8 encoding - | "utf16le" // UTF-16 Little Endian encoding - | "latin1" // Latin-1 (ISO-8859-1) encoding | "base64" // Base64 encoding | "base64url" // Base64 URL encoding - | "hex" // Hexadecimal encoding - | "ascii" // ASCII encoding - | "binary" // Binary encoding - | "ucs2"; // UCS-2 (alias of UTF-16LE) encoding + | "hex"; // Hexadecimal encoding diff --git a/packages/core/src/bytes/index.test.ts b/packages/core/src/bytes/index.test.ts new file mode 100644 index 000000000..f0dead498 --- /dev/null +++ b/packages/core/src/bytes/index.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { bytesFrom, bytesTo } from "./index.js"; + +describe("bytes", () => { + it("encodes and decodes UTF-8", () => { + const value = "Hello, 世界"; + + expect(bytesTo(bytesFrom(value, "utf8"), "utf8")).toBe(value); + }); + + it("encodes and decodes Base64", () => { + const bytes = Uint8Array.from([0xfb, 0xff]); + + expect(bytesTo(bytes, "base64")).toBe("+/8="); + expect(bytesFrom("+/8=", "base64")).toEqual(bytes); + }); + + it("encodes and decodes unpadded Base64URL", () => { + const bytes = Uint8Array.from([0xfb, 0xff]); + + expect(bytesTo(bytes, "base64url")).toBe("-_8"); + expect(bytesFrom("-_8", "base64url")).toEqual(bytes); + expect(bytesFrom("+/8=", "base64url")).toEqual(bytes); + }); + + it("preserves hexadecimal input behavior", () => { + expect(bytesFrom("abc")).toEqual(Uint8Array.from([0x0a, 0xbc])); + expect(bytesFrom("abc", "hex")).toEqual(Uint8Array.from([0x0a, 0xbc])); + expect(bytesFrom("0xabc", "hex")).toEqual(Uint8Array.from([0x0a, 0xbc])); + expect(bytesFrom("ABcd")).toEqual(Uint8Array.from([0xab, 0xcd])); + expect(() => bytesFrom("xyz")).toThrow(); + expect(() => bytesFrom("abzz", "hex")).toThrow(); + }); +}); diff --git a/packages/core/src/bytes/index.ts b/packages/core/src/bytes/index.ts index 20b90505f..730c7552e 100644 --- a/packages/core/src/bytes/index.ts +++ b/packages/core/src/bytes/index.ts @@ -1,5 +1,12 @@ -import { Buffer } from "buffer/index.js"; -import { BytesFromEncoding } from "./advanced.js"; +import { + base64ToUint8Array, + hexToUint8Array, + stringToUint8Array, + uint8ArrayToBase64, + uint8ArrayToHex, + uint8ArrayToString, +} from "uint8array-extras"; +import type { BytesFromEncoding } from "./advanced.js"; /** * @public @@ -91,7 +98,18 @@ export function bytesConcat(...args: BytesLike[]): Bytes { */ export function bytesTo(val: BytesLike, encoding: BytesFromEncoding): string { - return Buffer.from(bytesFrom(val)).toString(encoding); + const bytes = bytesFrom(val); + + switch (encoding) { + case "utf8": + return uint8ArrayToString(bytes); + case "base64": + return uint8ArrayToBase64(bytes); + case "base64url": + return uint8ArrayToBase64(bytes, { urlSafe: true }); + case "hex": + return uint8ArrayToHex(bytes); + } } /** @@ -133,17 +151,13 @@ export function bytesFrom( } if (typeof bytes === "string") { - if (encoding !== undefined) { - return Buffer.from(bytes, encoding); + if (encoding !== undefined && encoding !== "hex") { + return bytesFromString(bytes, encoding); } const str = bytes.startsWith("0x") ? bytes.slice(2) : bytes; const paddedStr = str.length % 2 === 0 ? str : `0${str}`; - const data = Buffer.from(paddedStr, "hex"); - if (data.length * 2 !== paddedStr.length) { - throw new Error(`Invalid bytes ${bytes}`); - } - return data; + return hexToUint8Array(paddedStr); } const bytesArr = Array.from(bytes); @@ -153,6 +167,19 @@ export function bytesFrom( return new Uint8Array(bytes); } +function bytesFromString( + value: string, + encoding: Exclude, +): Bytes { + switch (encoding) { + case "utf8": + return stringToUint8Array(value); + case "base64": + case "base64url": + return base64ToUint8Array(value); + } +} + /** * Compares two byte-like values for equality. * @public diff --git a/packages/core/src/hex/index.test.ts b/packages/core/src/hex/index.test.ts index 93417a700..58f948b81 100644 --- a/packages/core/src/hex/index.test.ts +++ b/packages/core/src/hex/index.test.ts @@ -21,7 +21,7 @@ describe("hexFrom", () => { ); test("throws for invalid hex string", () => { - expect(() => hexFrom("0xzz")).toThrow("Invalid bytes 0xzz"); + expect(() => hexFrom("0xzz")).toThrow(); }); test("throws for invalid byte values", () => { @@ -48,7 +48,7 @@ describe("bytesLen", () => { ); test("throws for invalid hex string", () => { - expect(() => bytesLen("0xzz")).toThrow("Invalid bytes 0xzz"); + expect(() => bytesLen("0xzz")).toThrow(); }); test("throws for invalid byte values", () => { diff --git a/packages/core/tsdown.config.mts b/packages/core/tsdown.config.mts index 6d0a39add..2419cea9a 100644 --- a/packages/core/tsdown.config.mts +++ b/packages/core/tsdown.config.mts @@ -25,6 +25,7 @@ const bundleDeps = [ "bs58check", "bs58", // By bs58check "base-x", // By bs58 - bs58check + "uint8array-extras", ] as string[]; export default defineConfig( diff --git a/packages/playground/src/app/page.tsx b/packages/playground/src/app/page.tsx index 43be36993..de204f23e 100644 --- a/packages/playground/src/app/page.tsx +++ b/packages/playground/src/app/page.tsx @@ -90,7 +90,7 @@ async function shareToNostr( ...sent .map((relay) => { console.log(relay); - const bytes = ccc.bytesFrom(relay, "ascii"); + const bytes = ccc.bytesFrom(relay, "utf8"); return [[1, bytes.length], bytes]; }) .flat(), @@ -129,7 +129,7 @@ async function getFromNEvent( eventId = ccc.hexFrom(value).slice(2); } if (type === 1) { - const relay = ccc.bytesTo(value, "ascii"); + const relay = ccc.bytesTo(value, "utf8"); if (!relays.includes(relay)) { relays.push(relay); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba3b9370d..431e566f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,15 +343,15 @@ importers: bs58check: specifier: ^4.0.0 version: 4.0.0 - buffer: - specifier: ^6.0.3 - version: 6.0.3 ethers: specifier: ^6.17.0 version: 6.17.0 isomorphic-ws: specifier: ^5.0.0 version: 5.0.0(ws@8.21.3) + uint8array-extras: + specifier: ^1.5.0 + version: 1.5.0 ws: specifier: ^8.21.0 version: 8.21.3 @@ -5410,9 +5410,6 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -13771,11 +13768,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - builtin-modules@3.3.0: {} busboy@1.6.0: