utils/arrayHelpers.js

import BufferView from './BufferView.js';
/* eslint-disable no-unused-vars */
import Writer from './Writer.js';
/* eslint-enable no-unused-vars */

/**
 * Deeply compares two array elements.
 * @param {object} lhs Left object to compare.
 * @param {object} rhs Right object to compare.
 * @returns {number} 1 if lhs is greater than rhs; -1 if lhs is less than rhs; 0 if lhs and rhs are equal.
 */
const deepCompare = (lhs, rhs) => {
	if (!Array.isArray(lhs) && !(lhs instanceof Object.getPrototypeOf(Uint8Array))) {
		if (lhs === rhs)
			return 0;

		return lhs > rhs ? 1 : -1;
	}

	if (lhs.length !== rhs.length)
		return lhs.length > rhs.length ? 1 : -1;

	for (let i = 0; i < lhs.length; ++i) {
		const compareResult = deepCompare(lhs[i], rhs[i]);
		if (0 !== compareResult)
			return compareResult;
	}

	return 0;
};

const readArrayImpl = (bufferInput, FactoryClass, accessor, shouldContinue) => {
	const view = new BufferView(bufferInput);
	const elements = [];
	let previousElement = null;
	let i = 0;
	while (shouldContinue(i, view)) {
		const element = FactoryClass.deserialize(view.buffer);

		if (0 >= element.size)
			throw RangeError('element size has invalid size');

		if (accessor && previousElement && 0 <= deepCompare(accessor(previousElement), accessor(element)))
			throw RangeError('elements in array are not sorted');

		elements.push(element);
		view.shiftRight(element.size);

		previousElement = element;
		++i;
	}

	return elements;
};

const writeArrayImpl = (output, elements, count, accessor) => {
	for (let i = 0; i < count; ++i) {
		const element = elements[i];
		if (accessor && 0 < i && 0 <= deepCompare(accessor(elements[i - 1]), accessor(element)))
			throw RangeError('array passed to write array is not sorted');

		output.write(element.serialize());
	}
};

const sum = numbers => numbers.reduce((a, b) => a + b, 0);

/**
 * Calculates aligned size.
 * @param {number} size Size.
 * @param {number} alignment Alignment.
 * @returns {number} Size rounded up to alignment.
 */
const alignUp = (size, alignment) => Math.floor((size + alignment - 1) / alignment) * alignment;

/**
 * Calculates size of variable size objects.
 * @param {Array<object>} elements Serializable elements.
 * @param {number} alignment Alignment used for calculations.
 * @param {boolean} skipLastElementPadding \c true if last element should not be aligned.
 * @returns {number} Computed size.
 */
const size = (elements, alignment = 0, skipLastElementPadding = false) => {
	if (!alignment)
		return sum(elements.map(e => e.size));

	if (!skipLastElementPadding)
		return sum(elements.map(e => alignUp(e.size, alignment)));

	return sum(elements.slice(0, -1).map(e => alignUp(e.size, alignment))) + sum(elements.slice(-1).map(e => e.size));
};

/**
 * Reads array of objects.
 * @param {Uint8Array} bufferInput Buffer input.
 * @param {{deserialize: function}} FactoryClass Factory used to deserialize objects.
 * @param {function|undefined} accessor Optional accessor used to check objects order.
 * @returns {Array<object>} Array of deserialized objects.
 */
const readArray = (bufferInput, FactoryClass, accessor = undefined) =>
	// note: this method is used only for '__FILL__' type arrays
	// this loop assumes properly sliced buffer is passed and that there's no additional data.
	readArrayImpl(bufferInput, FactoryClass, accessor, (_, view) => 0 < view.buffer.length);

/**
 * Reads array of deterministic number of objects.
 * @param {Uint8Array} bufferInput Buffer input.
 * @param {{deserialize: function}} FactoryClass Factory used to deserialize objects.
 * @param {number} count Number of object to deserialize.
 * @param {function|undefined} accessor Optional accessor used to check objects order.
 * @returns {Array<object>} Array of deserialized objects.
 */
const readArrayCount = (bufferInput, FactoryClass, count, accessor = undefined) =>
	readArrayImpl(bufferInput, FactoryClass, accessor, index => count > index);

/**
 * Reads array of variable size objects.
 * @param {Uint8Array} bufferInput Buffer input.
 * @param {{deserialize: function}} FactoryClass Factory used to deserialize objects.
 * @param {number} alignment Alignment used to make sure each object is at boundary.
 * @param {boolean} skipLastElementPadding \c true if last element is not aligned/padded.
 * @returns {Array<object>} Array of deserialized objects.
 */
const readVariableSizeElements = (bufferInput, FactoryClass, alignment, skipLastElementPadding = false) => {
	const view = new BufferView(bufferInput);
	const elements = [];
	while (0 < view.buffer.length) {
		const element = FactoryClass.deserialize(view.buffer);

		if (0 >= element.size)
			throw RangeError('element size has invalid size');

		elements.push(element);

		const alignedSize = (skipLastElementPadding && element.size >= view.buffer.length)
			? element.size
			: alignUp(element.size, alignment);
		if (alignedSize > view.buffer.length)
			throw RangeError('unexpected buffer length');

		view.shiftRight(alignedSize);
	}

	return elements;
};

/**
 * Writes array of objects.
 * @param {{write: function}} output Output sink.
 * @param {Array<object>} elements Serializable elements.
 * @param {function|undefined} accessor Optional accessor used to check objects order.
 */
const writeArray = (output, elements, accessor = undefined) => {
	writeArrayImpl(output, elements, elements.length, accessor);
};

/**
 * Writes array of deterministic number of objects.
 * @param {{write: function}} output Output sink.
 * @param {Array<object>} elements Serializable elements.
 * @param {number} count Number of objects to write.
 * @param {function|undefined} accessor Optional accessor used to check objects order.
 */
const writeArrayCount = (output, elements, count, accessor = undefined) => {
	writeArrayImpl(output, elements, count, accessor);
};

/**
 * Writes array of variable size objects.
 * @param {{write: function}} output Output sink.
 * @param {Array<object>} elements Serializable elements.
 * @param {number} alignment Alignment used to make sure each object is at boundary.
 * @param {boolean} skipLastElementPadding \c true if last element should not be aligned/padded.
 */
const writeVariableSizeElements = (output, elements, alignment, skipLastElementPadding = false) => {
	elements.forEach((element, index) => {
		output.write(element.serialize());
		if (!skipLastElementPadding || elements.length - 1 !== index) {
			const alignedSize = alignUp(element.size, alignment);
			if (alignedSize - element.size)
				output.write(new Uint8Array(alignedSize - element.size));
		}
	});
};

export {
	deepCompare,
	alignUp,
	size,
	readArray,
	readArrayCount,
	readVariableSizeElements,
	writeArray,
	writeArrayCount,
	writeVariableSizeElements
};