336 lines
No EOL
8.9 KiB
JavaScript
336 lines
No EOL
8.9 KiB
JavaScript
/**
|
|
* 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
|
|
}; |