utils/converter.js

import charMapping from './charMapping.js';

const CHAR_TO_NIBBLE_MAP = (() => {
	const builder = charMapping.createBuilder();
	builder.addRange('0', '9', 0);
	builder.addRange('a', 'f', 10);
	builder.addRange('A', 'F', 10);
	return builder.map;
})();

const CHAR_TO_DIGIT_MAP = (() => {
	const builder = charMapping.createBuilder();
	builder.addRange('0', '9', 0);
	return builder.map;
})();

const NIBBLE_TO_CHAR_MAP = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];

const SIGNEDNESS_AND_SIZE_TO_ARRAY_TYPE_MAPPING = {
	false: {
		1: Uint8Array,
		2: Uint16Array,
		4: Uint32Array,
		8: BigUint64Array
	},
	true: {
		1: Int8Array,
		2: Int16Array,
		4: Int32Array,
		8: BigInt64Array
	}
};

const tryParseByte = (char1, char2) => {
	const nibble1 = CHAR_TO_NIBBLE_MAP[char1];
	const nibble2 = CHAR_TO_NIBBLE_MAP[char2];
	return undefined === nibble1 || undefined === nibble2
		? undefined
		: (nibble1 << 4) | nibble2;
};

/**
 * Decodes two hex characters into a byte.
 * @param {string} char1 First hex digit.
 * @param {string} char2 Second hex digit.
 * @returns {number} Decoded byte.
 */
const toByte = (char1, char2) => {
	const byte = tryParseByte(char1, char2);
	if (undefined === byte)
		throw Error(`unrecognized hex char '${char1}${char2}'`);

	return byte;
};

/**
 * Determines whether or not a string is a hex string.
 * @param {string} input String to test.
 * @returns {boolean} \c true if the input is a hex string, \c false otherwise.
 */
const isHexString = input => {
	if (0 !== input.length % 2)
		return false;

	for (let i = 0; i < input.length; i += 2) {
		if (undefined === tryParseByte(input[i], input[i + 1]))
			return false;
	}

	return true;
};

/**
 * Converts a hex string to a uint8 array.
 * @param {string} input Hex encoded string.
 * @returns {Uint8Array} Uint8 array corresponding to the input.
 */
const hexToUint8 = input => {
	if (0 !== input.length % 2)
		throw Error(`hex string has unexpected size '${input.length}'`);

	const output = new Uint8Array(input.length / 2);
	for (let i = 0; i < input.length; i += 2)
		output[i / 2] = toByte(input[i], input[i + 1]);

	return output;
};

/**
 * Converts a uint8 array to a hex string.
 * @param {Uint8Array} input Uint8 array.
 * @returns {string} Hex encoded string corresponding to the input.
 */
const uint8ToHex = input => {
	let s = '';
	input.forEach(byte => {
		s += NIBBLE_TO_CHAR_MAP[byte >> 4];
		s += NIBBLE_TO_CHAR_MAP[byte & 0x0F];
	});

	return s;
};

/**
 * Tries to parse a string representing an unsigned integer.
 * @param {string} str String to parse.
 * @returns {number|undefined} Number represented by the input or undefined.
 */
const tryParseUint = str => {
	if ('0' === str)
		return 0;

	let value = 0;
	for (let i = 0; i < str.length; ++i) {
		const char = str[i];
		const digit = CHAR_TO_DIGIT_MAP[char];
		if (undefined === digit || (0 === value && 0 === digit))
			return undefined;

		value *= 10;
		value += digit;

		if (value > Number.MAX_SAFE_INTEGER)
			return undefined;
	}

	return value;
};

/**
 * Converts aligned bytes to little-endian number.
 * @param {Uint8Array} input Uint8 array.
 * @param {number} size Number of bytes.
 * @param {boolean} isSigned \c true if number should be treated as signed.
 * @returns {number} Value corresponding to the input.
 */
const bytesToInt = (input, size, isSigned = false) => {
	const DataType = SIGNEDNESS_AND_SIZE_TO_ARRAY_TYPE_MAPPING[isSigned][size];
	if (!DataType || 8 <= size)
		throw Error(`unsupported int size ${size}`);

	return new DataType(input.buffer, input.byteOffset, 1)[0];
};

/**
 * Converts aligned bytes to little-endian number.
 * @param {Uint8Array} input Uint8 array.
 * @param {number} size Number of bytes.
 * @param {boolean} isSigned \c true if number should be treated as signed.
 * @returns {bigint} Value corresponding to the input.
 */
const bytesToBigInt = (input, size, isSigned = false) => {
	const DataType = SIGNEDNESS_AND_SIZE_TO_ARRAY_TYPE_MAPPING[isSigned][size];
	if (!DataType || 8 > size)
		throw Error(`unsupported int size ${size}`);

	return new DataType(input.buffer, input.byteOffset, 1)[0];
};

const bytesToIntUnalignedInternal = (input, size, isSigned, createMappingFromView) => {
	const view = new DataView(input.buffer, input.byteOffset);
	const mapping = createMappingFromView(view);

	const reader = mapping[isSigned][size];
	if (!reader)
		throw Error(`unsupported int size ${size}`);

	return reader.call(view, 0, true);
};

/**
 * Converts bytes to little-endian number.
 * @param {Uint8Array} input Uint8 array.
 * @param {number} size Number of bytes.
 * @param {boolean} isSigned \c true if number should be treated as signed.
 * @returns {number} Value corresponding to the input.
 */
const bytesToIntUnaligned = (input, size, isSigned = false) => bytesToIntUnalignedInternal(input, size, isSigned, view => ({
	false: {
		1: view.getUint8,
		2: view.getUint16,
		4: view.getUint32
	},
	true: {
		1: view.getInt8,
		2: view.getInt16,
		4: view.getInt32
	}
}));

/**
 * Converts bytes to little-endian number.
 * @param {Uint8Array} input Uint8 array.
 * @param {number} size Number of bytes.
 * @param {boolean} isSigned \c true if number should be treated as signed.
 * @returns {bigint} Value corresponding to the input.
 */
const bytesToBigIntUnaligned = (input, size, isSigned = false) => bytesToIntUnalignedInternal(input, size, isSigned, view => ({
	false: {
		8: view.getBigUint64
	},
	true: {
		8: view.getBigInt64
	}
}));

/**
 * Converts an integer to bytes.
 * @param {number|bigint} value Integer value.
 * @param {number} byteSize Number of output bytes.
 * @param {boolean} isSigned \c true if the value is signed.
 * @returns {Uint8Array} Byte representation of the integer.
 */
const intToBytes = (value, byteSize, isSigned = false) => {
	const DataType = SIGNEDNESS_AND_SIZE_TO_ARRAY_TYPE_MAPPING[isSigned][byteSize];
	const typedBuffer = new DataType([value]);
	return new Uint8Array(typedBuffer.buffer);
};

export {
	toByte,
	isHexString,
	hexToUint8,
	uint8ToHex,
	tryParseUint,
	bytesToInt,
	bytesToBigInt,
	bytesToIntUnaligned,
	bytesToBigIntUnaligned,
	intToBytes
};