utils/base32.js

import charMapping from './charMapping.js';

const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const DECODED_BLOCK_SIZE = 5;
const ENCODED_BLOCK_SIZE = 8;

// region encode

const encodeBlock = (input, inputOffset, output, outputOffset) => {
	output[outputOffset + 0] = ALPHABET[input[inputOffset + 0] >> 3];
	output[outputOffset + 1] = ALPHABET[((input[inputOffset + 0] & 0x07) << 2) | (input[inputOffset + 1] >> 6)];
	output[outputOffset + 2] = ALPHABET[(input[inputOffset + 1] & 0x3E) >> 1];
	output[outputOffset + 3] = ALPHABET[((input[inputOffset + 1] & 0x01) << 4) | (input[inputOffset + 2] >> 4)];
	output[outputOffset + 4] = ALPHABET[((input[inputOffset + 2] & 0x0F) << 1) | (input[inputOffset + 3] >> 7)];
	output[outputOffset + 5] = ALPHABET[(input[inputOffset + 3] & 0x7F) >> 2];
	output[outputOffset + 6] = ALPHABET[((input[inputOffset + 3] & 0x03) << 3) | (input[inputOffset + 4] >> 5)];
	output[outputOffset + 7] = ALPHABET[input[inputOffset + 4] & 0x1F];
};

// endregion

// region decode

const Char_To_Decoded_Char_Map = (() => {
	const builder = charMapping.createBuilder();
	builder.addRange('A', 'Z', 0);
	builder.addRange('2', '7', 26);
	return builder.map;
})();

const decodeChar = c => {
	const decodedChar = Char_To_Decoded_Char_Map[c];
	if (undefined !== decodedChar)
		return decodedChar;

	throw Error(`illegal base32 character ${c}`);
};

const decodeBlock = (input, inputOffset, output, outputOffset) => {
	const bytes = new Uint8Array(ENCODED_BLOCK_SIZE);
	for (let i = 0; i < ENCODED_BLOCK_SIZE; ++i)
		bytes[i] = decodeChar(input[inputOffset + i]);

	output[outputOffset + 0] = (bytes[0] << 3) | (bytes[1] >> 2);
	output[outputOffset + 1] = ((bytes[1] & 0x03) << 6) | (bytes[2] << 1) | (bytes[3] >> 4);
	output[outputOffset + 2] = ((bytes[3] & 0x0F) << 4) | (bytes[4] >> 1);
	output[outputOffset + 3] = ((bytes[4] & 0x01) << 7) | (bytes[5] << 2) | (bytes[6] >> 3);
	output[outputOffset + 4] = ((bytes[6] & 0x07) << 5) | bytes[7];
};

// endregion

/**
 * Base32 encodes a binary buffer.
 * @param {Uint8Array} data Binary data to encode.
 * @returns {string} Base32 encoded string corresponding to the input data.
 */
const encode = data => {
	if (0 !== data.length % DECODED_BLOCK_SIZE)
		throw Error(`decoded size must be multiple of ${DECODED_BLOCK_SIZE}`);

	const output = new Array(data.length / DECODED_BLOCK_SIZE * ENCODED_BLOCK_SIZE);
	for (let i = 0; i < data.length / DECODED_BLOCK_SIZE; ++i)
		encodeBlock(data, i * DECODED_BLOCK_SIZE, output, i * ENCODED_BLOCK_SIZE);

	return output.join('');
};

/**
 * Base32 decodes a base32 encoded string.
 * @param {string} encoded Base32 encoded string to decode.
 * @returns {Uint8Array} Binary data corresponding to the input string.
 */
const decode = encoded => {
	if (0 !== encoded.length % ENCODED_BLOCK_SIZE)
		throw Error(`encoded size must be multiple of ${ENCODED_BLOCK_SIZE}`);

	const output = new Uint8Array(encoded.length / ENCODED_BLOCK_SIZE * DECODED_BLOCK_SIZE);
	for (let i = 0; i < encoded.length / ENCODED_BLOCK_SIZE; ++i)
		decodeBlock(encoded, i * ENCODED_BLOCK_SIZE, output, i * DECODED_BLOCK_SIZE);

	return output;
};

export default { encode, decode };