UNPKG

ox

Version:

Ethereum Standard Library

318 lines 10.2 kB
import * as Bytes from './Bytes.js'; import * as Errors from './Errors.js'; import * as Hex from './Hex.js'; import { decoder } from './internal/codec/utf8.js'; // Standard Base64 alphabet (RFC 4648 section 4) and URL-safe alphabet (section 5). const stdAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Char-code -> 6-bit integer lookup. Sentinel `0xff` = invalid character. // Standard alphabet plus URL-safe `-`/`_` (decoders are alphabet-agnostic). const characterToInteger = /*#__PURE__*/ (() => { const table = new Uint8Array(256).fill(0xff); for (let i = 0; i < stdAlphabet.length; i++) table[stdAlphabet.charCodeAt(i)] = i; // URL-safe alternates: `-` for `+` (62), `_` for `/` (63). table[45 /* '-' */] = 62; table[95 /* '_' */] = 63; return table; })(); // Integer -> char-code table for the standard alphabet, indexed by 6-bit value. const integerToCharacter = /*#__PURE__*/ (() => { const table = new Uint8Array(64); for (let i = 0; i < stdAlphabet.length; i++) table[i] = stdAlphabet.charCodeAt(i); return table; })(); // Phase 2 native fast-path detection: `Uint8Array.prototype.toBase64` / // `Uint8Array.fromBase64` ship in Node 22+, Safari 18+, Firefox 133+. const nativeToBase64 = Uint8Array.prototype.toBase64; const nativeFromBase64 = Uint8Array.fromBase64; /** * Encodes a {@link ox#Bytes.Bytes} to a Base64-encoded string (with optional padding and/or URL-safe characters). * * @example * ```ts twoslash * import { Base64, Bytes } from 'ox' * * const value = Base64.fromBytes( * Bytes.fromString('hello world') * ) * // @log: 'aGVsbG8gd29ybGQ=' * ``` * * @example * ### No Padding * * Turn off [padding of encoded data](https://datatracker.ietf.org/doc/html/rfc4648#section-3.2) with the `pad` option: * * ```ts twoslash * import { Base64, Bytes } from 'ox' * * const value = Base64.fromBytes( * Bytes.fromString('hello world'), * { pad: false } * ) * // @log: 'aGVsbG8gd29ybGQ' * ``` * * ### URL-safe Encoding * * Turn on [URL-safe encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5) (Base64 URL) with the `url` option: * * ```ts twoslash * import { Base64, Bytes } from 'ox' * * const value = Base64.fromBytes( * Bytes.fromString('hello wod'), * { url: true } * ) * // @log: 'aGVsbG8gd29_77-9ZA==' * ``` * * @param value - The byte array to encode. * @param options - Encoding options. * @returns The Base64 encoded string. */ export function fromBytes(value, options = {}) { const { pad = true, url = false } = options; if (nativeToBase64) { const out = nativeToBase64.call(value, { alphabet: url ? 'base64url' : 'base64', omitPadding: !pad, }); // Native `base64url` already omits padding; if pad was requested, restore it. if (url && pad) { const k = value.length % 3; if (k === 1) return `${out}==`; if (k === 2) return `${out}=`; } return out; } const encoded = new Uint8Array(Math.ceil(value.length / 3) * 4); for (let i = 0, j = 0; j < value.length; i += 4, j += 3) { const y = (value[j] << 16) + (value[j + 1] << 8) + (value[j + 2] | 0); encoded[i] = integerToCharacter[y >> 18]; encoded[i + 1] = integerToCharacter[(y >> 12) & 0x3f]; encoded[i + 2] = integerToCharacter[(y >> 6) & 0x3f]; encoded[i + 3] = integerToCharacter[y & 0x3f]; } const k = value.length % 3; const end = Math.floor(value.length / 3) * 4 + (k && k + 1); let base64 = decoder.decode(new Uint8Array(encoded.buffer, 0, end)); if (pad && k === 1) base64 += '=='; if (pad && k === 2) base64 += '='; if (url) base64 = base64.replaceAll('+', '-').replaceAll('/', '_'); return base64; } /** * Encodes a {@link ox#Hex.Hex} to a Base64-encoded string (with optional padding and/or URL-safe characters). * * @example * ```ts twoslash * import { Base64, Hex } from 'ox' * * const value = Base64.fromHex(Hex.fromString('hello world')) * // @log: 'aGVsbG8gd29ybGQ=' * ``` * * @example * ### No Padding * * Turn off [padding of encoded data](https://datatracker.ietf.org/doc/html/rfc4648#section-3.2) with the `pad` option: * * ```ts twoslash * import { Base64, Hex } from 'ox' * * const value = Base64.fromHex( * Hex.fromString('hello world'), * { pad: false } * ) * // @log: 'aGVsbG8gd29ybGQ' * ``` * * ### URL-safe Encoding * * Turn on [URL-safe encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5) (Base64 URL) with the `url` option: * * ```ts twoslash * import { Base64, Hex } from 'ox' * * const value = Base64.fromHex(Hex.fromString('hello wod'), { * url: true * }) * // @log: 'aGVsbG8gd29_77-9ZA==' * ``` * * @param value - The hex value to encode. * @param options - Encoding options. * @returns The Base64 encoded string. */ export function fromHex(value, options = {}) { return fromBytes(Bytes.fromHex(value), options); } /** * Encodes a string to a Base64-encoded string (with optional padding and/or URL-safe characters). * * @example * ```ts twoslash * import { Base64 } from 'ox' * * const value = Base64.fromString('hello world') * // @log: 'aGVsbG8gd29ybGQ=' * ``` * * @example * ### No Padding * * Turn off [padding of encoded data](https://datatracker.ietf.org/doc/html/rfc4648#section-3.2) with the `pad` option: * * ```ts twoslash * import { Base64 } from 'ox' * * const value = Base64.fromString('hello world', { * pad: false * }) * // @log: 'aGVsbG8gd29ybGQ' * ``` * * ### URL-safe Encoding * * Turn on [URL-safe encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5) (Base64 URL) with the `url` option: * * ```ts twoslash * import { Base64 } from 'ox' * * const value = Base64.fromString('hello wod', { url: true }) * // @log: 'aGVsbG8gd29_77-9ZA==' * ``` * * @param value - The string to encode. * @param options - Encoding options. * @returns The Base64 encoded string. */ export function fromString(value, options = {}) { return fromBytes(Bytes.fromString(value), options); } /** * Decodes a Base64-encoded string (with optional padding and/or URL-safe characters) to {@link ox#Bytes.Bytes}. * * @example * ```ts twoslash * import { Base64, Bytes } from 'ox' * * const value = Base64.toBytes('aGVsbG8gd29ybGQ=') * // @log: Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) * ``` * * @param value - The string, hex value, or byte array to encode. * @returns The Base64 decoded {@link ox#Bytes.Bytes}. */ export function toBytes(value) { // Strip trailing '=' padding (only at the very end). let bodyEnd = value.length; let pad = 0; while (bodyEnd > 0 && value.charCodeAt(bodyEnd - 1) === 61 /* '=' */) { bodyEnd--; pad++; } const size = bodyEnd; // Reject impossible lengths and excessive padding. if (size % 4 === 1) throw new InvalidLengthError({ length: value.length }); if (pad > 2) throw new InvalidPaddingError({ padding: pad }); if (nativeFromBase64) { try { // `base64url-or-base64` accepts both alphabets transparently. return nativeFromBase64(value.slice(0, bodyEnd), { alphabet: 'base64url-or-base64', lastChunkHandling: 'loose', }); } catch { // Fall through to the JS path so we throw the typed error class. } } // Validate characters: every char must be in the standard or URL-safe // alphabet, and '=' may not appear in the body (only stripped trailing // padding). const decoded = new Uint8Array((size >> 2) * 3 + (size % 4 ? (size % 4) - 1 : 0)); let acc = 0; let bits = 0; let n = 0; for (let i = 0; i < size; i++) { const code = value.charCodeAt(i); if (code === 61 /* '=' */) throw new InvalidCharacterError({ character: '=' }); const v = characterToInteger[code]; if (v === 0xff) throw new InvalidCharacterError({ character: value[i] }); acc = (acc << 6) | v; bits += 6; if (bits >= 8) { bits -= 8; decoded[n++] = (acc >>> bits) & 0xff; } } return decoded; } /** * Decodes a Base64-encoded string (with optional padding and/or URL-safe characters) to {@link ox#Hex.Hex}. * * @example * ```ts twoslash * import { Base64, Hex } from 'ox' * * const value = Base64.toHex('aGVsbG8gd29ybGQ=') * // @log: 0x68656c6c6f20776f726c64 * ``` * * @param value - The string, hex value, or byte array to encode. * @returns The Base64 decoded {@link ox#Hex.Hex}. */ export function toHex(value) { return Hex.fromBytes(toBytes(value)); } /** * Decodes a Base64-encoded string (with optional padding and/or URL-safe characters) to a string. * * @example * ```ts twoslash * import { Base64 } from 'ox' * * const value = Base64.toString('aGVsbG8gd29ybGQ=') * // @log: 'hello world' * ``` * * @param value - The string, hex value, or byte array to encode. * @returns The Base64 decoded string. */ export function toString(value) { return Bytes.toString(toBytes(value)); } /** Thrown when a Base64 string contains an invalid character. */ export class InvalidCharacterError extends Errors.BaseError { name = 'Base64.InvalidCharacterError'; constructor({ character }) { super(`Invalid Base64 character: "${character}".`); } } /** Thrown when a Base64 string has an impossible length. */ export class InvalidLengthError extends Errors.BaseError { name = 'Base64.InvalidLengthError'; constructor({ length }) { super(`Invalid Base64 input length \`${length}\`.`); } } /** Thrown when a Base64 string contains too many trailing `=` padding characters. */ export class InvalidPaddingError extends Errors.BaseError { name = 'Base64.InvalidPaddingError'; constructor({ padding }) { super(`Invalid Base64 padding length \`${padding}\` (must be 0, 1, or 2).`); } } //# sourceMappingURL=Base64.js.map