import type { Result } from '../types/result.js' import { makeError, makeSuccess } from '../types/result.js' import { BaseError } from '../error/mod.js' export class Base64EncodingError extends BaseError { public readonly _tag = 'Base64EncodingError' } export class Base64DecodingError extends BaseError { public readonly _tag = 'Base64DecodingError' } /** * Encodes a string to URL-safe base64 * */ export function encodeBase64( input: string, ): Result { try { const encoder = new TextEncoder() const data = encoder.encode(input) const base64 = uint8ArrayToBase64(data) const result = base64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '') as Encoded return makeSuccess(result) } catch (cause) { return makeError( new Base64EncodingError(`failed to encode base64: ${cause}`), ) } } /** * Decodes a URL-safe base64 string * */ export function decodeBase64( encoded: string, ): Result { try { let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/') const padding = base64.length % 4 if (padding > 0) { base64 += '='.repeat(4 - padding) } const bytes = base64ToUint8Array(base64) const decoder = new TextDecoder() const result = decoder.decode(bytes) return makeSuccess(result) } catch (cause) { return makeError(new Base64DecodingError(`failed decode base64: ${cause}`)) } } /** * Converts a Uint8Array to a base64 string */ function uint8ArrayToBase64(bytes: Uint8Array): string { const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' let result = '' let i: number for (i = 0; i < bytes.length; i += 3) { const chunk = // @ts-ignore (bytes[i] << 16) | // @ts-ignore (i + 1 < bytes.length ? bytes[i + 1] << 8 : 0) | // @ts-ignore (i + 2 < bytes.length ? bytes[i + 2] : 0) result += base64Chars[(chunk >> 18) & 63] result += base64Chars[(chunk >> 12) & 63] result += i + 1 < bytes.length ? base64Chars[(chunk >> 6) & 63] : '=' result += i + 2 < bytes.length ? base64Chars[chunk & 63] : '=' } return result } /** * Converts a base64 string to a Uint8Array */ function base64ToUint8Array(base64: string): Uint8Array { const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' // Remove any non-base64 characters const raw = base64.replace(/[^A-Za-z0-9\+\/=]/g, '') const length = (raw.length * 3) / 4 - (raw.endsWith('==') ? 2 : raw.endsWith('=') ? 1 : 0) const bytes = new Uint8Array(length) const decoded = 0 let i: number let j: number for (i = 0, j = 0; i < raw.length; i += 4) { // @ts-ignore const a = base64Chars.indexOf(raw[i]) // @ts-ignore const b = base64Chars.indexOf(raw[i + 1]) // @ts-ignore const c = base64Chars.indexOf(raw[i + 2]) // @ts-ignore const d = base64Chars.indexOf(raw[i + 3]) const chunk = (a << 18) | (b << 12) | ((c & 63) << 6) | (d & 63) if (j < bytes.length) bytes[j++] = (chunk >> 16) & 0xff if (j < bytes.length) bytes[j++] = (chunk >> 8) & 0xff if (j < bytes.length) bytes[j++] = chunk & 0xff } return bytes }