177 lines
4.8 KiB
JavaScript
177 lines
4.8 KiB
JavaScript
/* 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);
|