/** * Hexadecimal encoding, decoding, and random value helpers. * * @stability stable * @since 4.0.0 */ import * as Result from "../Result.ts" import { EncodingError } from "./EncodingError.ts" /** * Encodes the given value into a hex `string`. * * **When to use** * * Use to encode text or bytes as lowercase hexadecimal text. * * **Example** (Encoding hex strings and bytes) * * ```ts import.meta.vitest * import { Hex } from "effect/encoding" * * // Encode a string to hex * Hex.encode("hello") // => "68656c6c6f" * * // Encode binary data to hex * const bytes = new Uint8Array([72, 101, 108, 108, 111]) * Hex.encode(bytes) // => "48656c6c6f" * ``` * * @stability stable * @category encoding * @since 4.0.0 */ export const encode: (input: Uint8Array | string) => string = (input) => typeof input === "string" ? hexEncodeUint8Array(encoder.encode(input)) : hexEncodeUint8Array(input) /** * Generates a random lowercase hexadecimal string, optimized for lengths that * are multiples of 8. * * **Details** * * `length` is not validated. The function generates `length >>> 3` random * 8-character words, so non-negative lengths below `2 ** 32` are rounded down * to a multiple of 8 and other values follow JavaScript's unsigned 32-bit * coercion rules. * * This function uses `Math.random()` and is not cryptographically secure. For * security-sensitive values, use the `Crypto.Crypto` service's `randomBytes` * method and encode the result with {@link encode}. * * @stability stable * @category encoding * @since 4.0.0 */ export const random = (length: number): string => { switch (length) { case 16: return random16() case 32: return random32() default: { let result = "" for (let i = length >>> 3; i > 0; i--) { result += random8() } return result } } } const hexCharCodes = Uint8Array.from("0123456789abcdef", (c) => c.charCodeAt(0)) const randomWord = (): number => (Math.random() * 0x100000000) >>> 0 // Trace and span identifiers are the common lengths. A single // String.fromCharCode call produces a flat string, which avoids rope // flattening when the identifier is later serialized. const random8 = (): string => { const a = randomWord() return String.fromCharCode( hexCharCodes[a >>> 28], hexCharCodes[(a >>> 24) & 15], hexCharCodes[(a >>> 20) & 15], hexCharCodes[(a >>> 16) & 15], hexCharCodes[(a >>> 12) & 15], hexCharCodes[(a >>> 8) & 15], hexCharCodes[(a >>> 4) & 15], hexCharCodes[a & 15] ) } const random16 = (): string => { const a = randomWord() const b = randomWord() return String.fromCharCode( hexCharCodes[a >>> 28], hexCharCodes[(a >>> 24) & 15], hexCharCodes[(a >>> 20) & 15], hexCharCodes[(a >>> 16) & 15], hexCharCodes[(a >>> 12) & 15], hexCharCodes[(a >>> 8) & 15], hexCharCodes[(a >>> 4) & 15], hexCharCodes[a & 15], hexCharCodes[b >>> 28], hexCharCodes[(b >>> 24) & 15], hexCharCodes[(b >>> 20) & 15], hexCharCodes[(b >>> 16) & 15], hexCharCodes[(b >>> 12) & 15], hexCharCodes[(b >>> 8) & 15], hexCharCodes[(b >>> 4) & 15], hexCharCodes[b & 15] ) } const random32 = (): string => { const a = randomWord() const b = randomWord() const c = randomWord() const d = randomWord() return String.fromCharCode( hexCharCodes[a >>> 28], hexCharCodes[(a >>> 24) & 15], hexCharCodes[(a >>> 20) & 15], hexCharCodes[(a >>> 16) & 15], hexCharCodes[(a >>> 12) & 15], hexCharCodes[(a >>> 8) & 15], hexCharCodes[(a >>> 4) & 15], hexCharCodes[a & 15], hexCharCodes[b >>> 28], hexCharCodes[(b >>> 24) & 15], hexCharCodes[(b >>> 20) & 15], hexCharCodes[(b >>> 16) & 15], hexCharCodes[(b >>> 12) & 15], hexCharCodes[(b >>> 8) & 15], hexCharCodes[(b >>> 4) & 15], hexCharCodes[b & 15], hexCharCodes[c >>> 28], hexCharCodes[(c >>> 24) & 15], hexCharCodes[(c >>> 20) & 15], hexCharCodes[(c >>> 16) & 15], hexCharCodes[(c >>> 12) & 15], hexCharCodes[(c >>> 8) & 15], hexCharCodes[(c >>> 4) & 15], hexCharCodes[c & 15], hexCharCodes[d >>> 28], hexCharCodes[(d >>> 24) & 15], hexCharCodes[(d >>> 20) & 15], hexCharCodes[(d >>> 16) & 15], hexCharCodes[(d >>> 12) & 15], hexCharCodes[(d >>> 8) & 15], hexCharCodes[(d >>> 4) & 15], hexCharCodes[d & 15] ) } /** * Decodes a hexadecimal string into bytes safely. * * **When to use** * * Use to decode hexadecimal text into bytes without throwing on invalid input. * * **Details** * * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or * `Result.fail` with an `EncodingError` when the input has an odd length or * contains invalid hex characters. * * **Example** (Decoding hex bytes) * * ```ts import.meta.vitest * import { Result } from "effect" * import { Hex } from "effect/encoding" * * Hex.decode("48656c6c6f") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111])) * ``` * * @stability stable * @category decoding * @since 4.0.0 */ export const decode = (str: string): Result.Result => { const bytes = new TextEncoder().encode(str) if (bytes.length % 2 !== 0) { return Result.fail( new EncodingError({ module: "Hex", kind: "Decode", input: str, message: `Length must be a multiple of 2, but is ${bytes.length}` }) ) } try { const length = bytes.length / 2 const result = new Uint8Array(length) for (let i = 0; i < length; i++) { const a = fromHexChar(bytes[i * 2]) const b = fromHexChar(bytes[i * 2 + 1]) result[i] = (a << 4) | b } return Result.succeed(result) } catch (e) { return Result.fail( new EncodingError({ module: "Hex", kind: "Decode", input: str, message: e instanceof Error ? e.message : "Invalid input" }) ) } } /** * Decodes a hexadecimal string into a UTF-8 string safely. * * **When to use** * * Use to decode hexadecimal text into UTF-8 text without throwing on invalid * input. * * **Details** * * Returns `Result.succeed` with the decoded text when decoding succeeds, or * `Result.fail` with an `EncodingError` when the input is not valid hex. * * **Example** (Decoding hex strings) * * ```ts import.meta.vitest * import { Result } from "effect" * import { Hex } from "effect/encoding" * * Hex.decodeString("68656c6c6f") // => Result.succeed("hello") * ``` * * @stability stable * @category decoding * @since 4.0.0 */ export const decodeString = (str: string) => Result.map(decode(str), (_) => decoder.decode(_)) const encoder = new TextEncoder() const decoder = new TextDecoder() const byteToHex: Array = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")) const hexEncodeUint8Array = (bytes: Uint8Array): string => { let result = "" for (let i = 0; i < bytes.length; i++) { result += byteToHex[bytes[i]] } return result } const fromHexChar = (byte: number) => { if (48 <= byte && byte <= 57) { return byte - 48 } if (97 <= byte && byte <= 102) { return byte - 97 + 10 } if (65 <= byte && byte <= 70) { return byte - 65 + 10 } throw new TypeError("Invalid input") }