init
This commit is contained in:
commit
2b8870a40e
51 changed files with 5845 additions and 0 deletions
336
utils/base32.js
Normal file
336
utils/base32.js
Normal file
|
@ -0,0 +1,336 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue