UNPKG

ox

Version:

Ethereum Standard Library

146 lines (130 loc) 4.72 kB
import * as Errors from '../../Errors.js' /** @internal */ export type Hex = `0x${string}` /** @internal */ const hexes = /*#__PURE__*/ Array.from({ length: 256 }, (_v, i) => i.toString(16).padStart(2, '0'), ) // Char-code → 4-bit nibble lookup table. Sentinel `0xff` = invalid. const nibbleTable = /*#__PURE__*/ (() => { const table = new Uint8Array(256).fill(0xff) for (let i = 0; i < 10; i++) table[48 + i] = i // '0'-'9' for (let i = 0; i < 6; i++) { table[65 + i] = 10 + i // 'A'-'F' table[97 + i] = 10 + i // 'a'-'f' } return table })() // Phase 2 native fast-path detection: `Uint8Array.prototype.toHex` lands in // Node 22+, Safari 18+, Firefox 133+. Falls back to `Buffer` on Node, then to // the JS loop. const _Buffer: typeof globalThis.Buffer | undefined = ( globalThis as typeof globalThis & { Buffer?: typeof globalThis.Buffer } ).Buffer const nativeToHex: ((this: Uint8Array) => string) | undefined = ( Uint8Array.prototype as Uint8Array & { toHex?: () => string } ).toHex const nativeFromHex: ((value: string) => Uint8Array) | undefined = ( Uint8Array as typeof Uint8Array & { fromHex?: (value: string) => Uint8Array } ).fromHex /** * Encodes a `Uint8Array` into a `0x`-prefixed lowercase hex string. Uses the * native `Uint8Array.prototype.toHex` (Node 22+, Safari 18+, Firefox 133+) or * Node's `Buffer` when available; otherwise a tight JS loop. * * @internal */ export function bytesToHex(value: Uint8Array): Hex { if (nativeToHex) return `0x${nativeToHex.call(value)}` as Hex if (_Buffer) return `0x${_Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString('hex')}` as Hex const length = value.length const parts = Array.from<string>({ length }) for (let i = 0; i < length; i++) parts[i] = hexes[value[i]!]! return `0x${parts.join('')}` as Hex } /** * Strictly decodes a `0x`-prefixed even-length hex string into a `Uint8Array`. * Uses the native `Uint8Array.fromHex` (Node 22+, Safari 18+, Firefox 133+) or * Node's `Buffer` when available; otherwise a tight JS loop. * * @internal */ export function hexToBytes(value: string): Uint8Array { if ( typeof value !== 'string' || value.length < 2 || value.charCodeAt(0) !== 48 /* '0' */ || value.charCodeAt(1) !== 120 /* 'x' */ ) throw new InvalidHexValueError(value) const body = value.length === 2 ? '' : (value as string).slice(2) if ((body.length & 1) !== 0) throw new InvalidLengthError(value as Hex) if (nativeFromHex) { try { return nativeFromHex(body) } catch { throw new InvalidHexValueError(value) } } if (_Buffer && body.length > 0) { // Buffer.from with 'hex' silently truncates on invalid chars; verify // byteLength matches expectations to detect malformed input. const expected = body.length >> 1 const buf = _Buffer.from(body, 'hex') if (buf.length !== expected) throw new InvalidHexValueError(value) // Copy out of Buffer pool: callers may rely on `.buffer` being a // standalone ArrayBuffer (e.g. WebAuthn attestationObject round-trips). const out = new Uint8Array(buf.byteLength) out.set(buf) return out } const length = body.length >> 1 const out = new Uint8Array(length) for (let i = 0, j = 0; i < length; i++) { const hi = nibbleTable[body.charCodeAt(j++)]! const lo = nibbleTable[body.charCodeAt(j++)]! if (hi === 0xff || lo === 0xff) throw new InvalidHexValueError(value) out[i] = (hi << 4) | lo } return out } /** @internal */ export function charCodeToBase16(char: number): number | undefined { const v = nibbleTable[char]! return v === 0xff ? undefined : v } /** Thrown when a value is not a valid `0x`-prefixed hex string. */ export class InvalidHexValueError extends Errors.BaseError { override readonly name = 'Hex.InvalidHexValueError' constructor(value: unknown) { super(`Value \`${value}\` is an invalid hex value.`, { metaMessages: [ 'Hex values must start with `"0x"` and contain only hexadecimal characters (0-9, a-f, A-F).', ], }) } } /** Thrown when a hex string has an odd nibble count. */ export class InvalidLengthError extends Errors.BaseError { override readonly name = 'Hex.InvalidLengthError' constructor(value: Hex) { super( `Hex value \`"${value}"\` is an odd length (${value.length - 2} nibbles).`, { metaMessages: ['It must be an even length.'], }, ) } } /** @internal */ export declare namespace bytesToHex { type ErrorType = Errors.GlobalErrorType } /** @internal */ export declare namespace hexToBytes { type ErrorType = | InvalidHexValueError | InvalidLengthError | Errors.GlobalErrorType }