/** * Enhanced Base32 implementation compatible with RFC 4648 * Optimized for WeChat Mini Program environment */ // RFC 4648 standard Base32 alphabet and padding const DEFAULT_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const PADDING = '='; // Pre-computed lookup tables let ENCODE_TABLE = [...DEFAULT_ALPHABET]; let DECODE_TABLE = new Map(ENCODE_TABLE.map((char, index) => [char, index])); // Valid padding lengths for Base32 const VALID_PADDING_LENGTHS = new Set([0, 1, 3, 4, 6]); /** * Custom error class for Base32 operations */ class Base32Error extends Error { constructor(message, type = 'GENERAL_ERROR') { super(message); this.name = 'Base32Error'; this.type = type; } } /** * String to UTF-8 bytes conversion (WeChat Mini Program compatible) * @private */ function stringToUint8Array(str) { const arr = []; for (let i = 0; i < str.length; i++) { let code = str.charCodeAt(i); if (code < 0x80) { arr.push(code); } else if (code < 0x800) { arr.push(0xc0 | (code >> 6)); arr.push(0x80 | (code & 0x3f)); } else if (code < 0xd800 || code >= 0xe000) { arr.push(0xe0 | (code >> 12)); arr.push(0x80 | ((code >> 6) & 0x3f)); arr.push(0x80 | (code & 0x3f)); } else { // Handle surrogate pairs i++; code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); arr.push(0xf0 | (code >> 18)); arr.push(0x80 | ((code >> 12) & 0x3f)); arr.push(0x80 | ((code >> 6) & 0x3f)); arr.push(0x80 | (code & 0x3f)); } } return new Uint8Array(arr); } /** * UTF-8 bytes to string conversion (WeChat Mini Program compatible) * @private */ function uint8ArrayToString(bytes) { let result = ''; for (let i = 0; i < bytes.length; i++) { const byte = bytes[i]; if (byte < 0x80) { result += String.fromCharCode(byte); } else if (byte < 0xe0) { result += String.fromCharCode( ((byte & 0x1f) << 6) | (bytes[++i] & 0x3f) ); } else if (byte < 0xf0) { result += String.fromCharCode( ((byte & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f) ); } else { const codePoint = ( ((byte & 0x07) << 18) | ((bytes[++i] & 0x3f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f) ); result += String.fromCodePoint(codePoint); } } return result; } /** * Sets a custom alphabet for Base32 encoding/decoding * @param {string} alphabet - The custom Base32 alphabet (32 unique characters) * @throws {Base32Error} If the alphabet is invalid */ function setCustomAlphabet(alphabet) { if (typeof alphabet !== 'string' || alphabet.length !== 32 || new Set(alphabet).size !== 32) { throw new Base32Error( 'Invalid custom alphabet: must be 32 unique characters', 'INVALID_ALPHABET' ); } ENCODE_TABLE = [...alphabet]; DECODE_TABLE = new Map(ENCODE_TABLE.map((char, index) => [char, index])); } /** * Resets to the default Base32 alphabet */ function resetToDefaultAlphabet() { ENCODE_TABLE = [...DEFAULT_ALPHABET]; DECODE_TABLE = new Map(ENCODE_TABLE.map((char, index) => [char, index])); } /** * Validates padding length and position * @private */ function validatePadding(input) { const paddingMatch = input.match(/=+$/); if (!paddingMatch) return true; const paddingLength = paddingMatch[0].length; if (!VALID_PADDING_LENGTHS.has(paddingLength)) { throw new Base32Error( `Invalid padding length: ${paddingLength}`, 'INVALID_PADDING' ); } if (input.indexOf('=') !== input.length - paddingLength) { throw new Base32Error( 'Padding character in invalid position', 'INVALID_PADDING_POSITION' ); } return true; } /** * Encodes a string or Uint8Array to Base32 * @param {string|Uint8Array} input - The input to encode * @param {Object} [options] - Encoding options * @param {boolean} [options.noPadding=false] - Whether to omit padding * @param {boolean} [options.strict=false] - Whether to enable strict mode * @param {string} [options.alphabet] - Optional custom Base32 alphabet * @returns {string} The Base32 encoded string * @throws {Base32Error} If the input is invalid */ function encode(input, options = {}) { try { if (options.alphabet) { setCustomAlphabet(options.alphabet); } const data = typeof input === 'string' ? stringToUint8Array(input) : input; if (!(data instanceof Uint8Array)) { throw new Base32Error( 'Input must be a string or Uint8Array', 'INVALID_INPUT_TYPE' ); } let bits = 0; let value = 0; let output = ''; for (let i = 0; i < data.length; i++) { value = (value << 8) | data[i]; bits += 8; while (bits >= 5) { output += ENCODE_TABLE[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += ENCODE_TABLE[(value << (5 - bits)) & 31]; } if (!options.noPadding) { const pad = 8 - (output.length % 8); if (pad !== 8) { output += PADDING.repeat(pad); } } return output; } catch (error) { if (error instanceof Base32Error) throw error; throw new Base32Error('Encoding failed: ' + error.message, 'ENCODING_ERROR'); } finally { if (options.alphabet) { resetToDefaultAlphabet(); } } } /** * Decodes a Base32 string to Uint8Array * @param {string} input - The Base32 string to decode * @param {Object} [options] - Decoding options * @param {boolean} [options.strict=false] - Whether to enable strict mode * @param {boolean} [options.returnString=false] - Whether to return a string instead of Uint8Array * @param {string} [options.alphabet] - Optional custom Base32 alphabet * @returns {Uint8Array|string} The decoded data * @throws {Base32Error} If the input is invalid */ function decode(input, options = {}) { try { if (options.alphabet) { setCustomAlphabet(options.alphabet); } if (typeof input !== 'string') { throw new Base32Error('Input must be a string', 'INVALID_INPUT_TYPE'); } const upperInput = input.toUpperCase(); if (options.strict) { validatePadding(upperInput); } const cleanInput = upperInput.replace(/=+$/, ''); if (!/^[A-Z2-7]*$/.test(cleanInput)) { throw new Base32Error('Invalid Base32 character', 'INVALID_CHARACTER'); } let bits = 0; let value = 0; const output = []; for (let i = 0; i < cleanInput.length; i++) { const charValue = DECODE_TABLE.get(cleanInput[i]); value = (value << 5) | charValue; bits += 5; if (bits >= 8) { output.push((value >>> (bits - 8)) & 255); bits -= 8; } } const result = new Uint8Array(output); return options.returnString ? uint8ArrayToString(result) : result; } catch (error) { if (error instanceof Base32Error) throw error; throw new Base32Error('Decoding failed: ' + error.message, 'DECODING_ERROR'); } finally { if (options.alphabet) { resetToDefaultAlphabet(); } } } /** * Validates if a string is a valid Base32 encoding * @param {string} input - The string to validate * @param {Object} [options] - Validation options * @param {boolean} [options.strict=false] - Whether to enable strict mode * @returns {boolean} True if the string is valid Base32 */ function isValid(input, options = {}) { try { if (typeof input !== 'string') return false; const upperInput = input.toUpperCase(); if (options.strict) { validatePadding(upperInput); } const cleanInput = upperInput.replace(/=+$/, ''); return /^[A-Z2-7]*$/.test(cleanInput); } catch { return false; } } /** * Encodes a string as Base32 without padding * @param {string|Uint8Array} input - The input to encode * @returns {string} The Base32 encoded string without padding */ function encodeNoPadding(input) { return encode(input, { noPadding: true }); } /** * Normalizes a Base32 string * @param {string} input - The Base32 string to normalize * @returns {string} The normalized Base32 string */ function normalize(input) { if (!isValid(input)) { throw new Base32Error('Invalid Base32 string', 'INVALID_INPUT'); } const base = input.toUpperCase().replace(/=+$/, ''); const padding = 8 - (base.length % 8); return padding === 8 ? base : base + PADDING.repeat(padding); } /** * Calculates the length of encoded Base32 string * @param {number} inputLength - Length of input in bytes * @param {Object} [options] - Options * @param {boolean} [options.noPadding=false] - Whether padding will be omitted * @returns {number} Length of Base32 encoded string */ function encodedLength(inputLength, options = {}) { const baseLength = Math.ceil(inputLength * 8 / 5); if (options.noPadding) return baseLength; return Math.ceil(baseLength / 8) * 8; } module.exports = { encode, decode, encodeNoPadding, isValid, normalize, encodedLength, Base32Error, setCustomAlphabet, resetToDefaultAlphabet };