UNPKG

@sindresorhus/base62

Version:

Encode & decode strings, bytes, and integers to Base62

178 lines (137 loc) 4.94 kB
/* eslint-disable no-bitwise */ const BASE = 62; const BASE_BIGINT = 62n; const DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const cachedEncoder = new globalThis.TextEncoder(); const cachedDecoder = new globalThis.TextDecoder(); function assertString(value, label) { if (typeof value !== 'string') { throw new TypeError(`The \`${label}\` parameter must be a string, got \`${value}\` (${typeof value}).`); } } function validateAlphabet(alphabet) { if (typeof alphabet !== 'string') { throw new TypeError(`The alphabet must be a string, got \`${alphabet}\` (${typeof alphabet}).`); } if (alphabet.length !== BASE) { throw new TypeError(`The alphabet must be exactly ${BASE} characters long, got ${alphabet.length}.`); } const uniqueCharacters = new Set(alphabet); if (uniqueCharacters.size !== BASE) { throw new TypeError(`The alphabet must contain ${BASE} unique characters, got ${uniqueCharacters.size}.`); } } export class Base62 { constructor(options = {}) { const alphabet = options.alphabet ?? DEFAULT_ALPHABET; validateAlphabet(alphabet); this.alphabet = [...alphabet]; this.indices = new Map(this.alphabet.map((character, index) => [character, index])); } #getIndex(character) { const index = this.indices.get(character); if (index === undefined) { throw new TypeError(`Unexpected character for Base62 encoding: \`${character}\`.`); } return index; } encodeString(string) { assertString(string, 'string'); return this.encodeBytes(cachedEncoder.encode(string)); } decodeString(encodedString) { assertString(encodedString, 'encodedString'); return cachedDecoder.decode(this.decodeBytes(encodedString)); } encodeBytes(bytes) { if (!(bytes instanceof Uint8Array)) { throw new TypeError('The `bytes` parameter must be an instance of Uint8Array.'); } if (bytes.length === 0) { return ''; } // Prepend 0x01 to the byte array before encoding to ensure the BigInt conversion // does not strip any leading zeros and to prevent any byte sequence from being // interpreted as a numerically zero value. let value = 1n; for (const byte of bytes) { value = (value << 8n) | BigInt(byte); } return this.encodeBigInt(value); } decodeBytes(encodedString) { assertString(encodedString, 'encodedString'); if (encodedString.length === 0) { return new Uint8Array(); } let value = this.decodeBigInt(encodedString); const byteArray = []; while (value > 0n) { byteArray.push(Number(value & 0xFFn)); value >>= 8n; } // Remove the 0x01 that was prepended during encoding. return Uint8Array.from(byteArray.reverse().slice(1)); } encodeInteger(integer) { if (!Number.isInteger(integer)) { throw new TypeError(`Expected an integer, got \`${integer}\` (${typeof integer}).`); } if (integer < 0) { throw new TypeError('The integer must be non-negative.'); } if (integer === 0) { return this.alphabet[0]; } let encodedString = ''; while (integer > 0) { encodedString = this.alphabet[integer % BASE] + encodedString; integer = Math.floor(integer / BASE); } return encodedString; } decodeInteger(encodedString) { assertString(encodedString, 'encodedString'); let integer = 0; for (const character of encodedString) { integer = (integer * BASE) + this.#getIndex(character); } return integer; } encodeBigInt(bigint) { if (typeof bigint !== 'bigint') { throw new TypeError(`Expected a bigint, got \`${bigint}\` (${typeof bigint}).`); } if (bigint < 0) { throw new TypeError('The bigint must be non-negative.'); } if (bigint === 0n) { return this.alphabet[0]; } let encodedString = ''; while (bigint > 0n) { encodedString = this.alphabet[Number(bigint % BASE_BIGINT)] + encodedString; bigint /= BASE_BIGINT; } return encodedString; } decodeBigInt(encodedString) { assertString(encodedString, 'encodedString'); let bigint = 0n; for (const character of encodedString) { bigint = (bigint * BASE_BIGINT) + BigInt(this.#getIndex(character)); } return bigint; } } // Create default instance with standard alphabet const defaultBase62 = new Base62(); // Export convenience functions using the default alphabet for backward compatibility export const encodeString = string => defaultBase62.encodeString(string); export const decodeString = encodedString => defaultBase62.decodeString(encodedString); export const encodeBytes = bytes => defaultBase62.encodeBytes(bytes); export const decodeBytes = encodedString => defaultBase62.decodeBytes(encodedString); export const encodeInteger = integer => defaultBase62.encodeInteger(integer); export const decodeInteger = encodedString => defaultBase62.decodeInteger(encodedString); export const encodeBigInt = bigint => defaultBase62.encodeBigInt(bigint); export const decodeBigInt = encodedString => defaultBase62.decodeBigInt(encodedString);