/**
*** Copyright (c) 2016-2019, Jaguar0625, gimre, BloodyRookie, Tech Bureau, Corp.
*** Copyright (c) 2020-present, Jaguar0625, gimre, BloodyRookie.
*** All rights reserved.
***
*** This file is part of Catapult.
***
*** Catapult is free software: you can redistribute it and/or modify
*** it under the terms of the GNU Lesser General Public License as published by
*** the Free Software Foundation, either version 3 of the License, or
*** (at your option) any later version.
***
*** Catapult is distributed in the hope that it will be useful,
*** but WITHOUT ANY WARRANTY; without even the implied warranty of
*** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
*** GNU Lesser General Public License for more details.
***
*** You should have received a copy of the GNU Lesser General Public License
*** along with Catapult. If not, see .
**/
import { Serializer } from './Serializer';
/**
* Generator utility class.
*/
export class GeneratorUtils {
/**
* Convert a UInt8Array input into an array of 2 numbers.
* Numbers in the returned array are cast to UInt32.
* @param {Uint8Array} input A uint8 array.
* @returns {number[]} The uint64 representation of the input.
*/
public static bufferToUint64(input: Uint8Array): number[] {
const view = new DataView(input.slice(0, 8).reverse().buffer);
return [view.getUint32(4), view.getUint32(0)];
}
/**
* Read 4 bytes as a uint32 value from buffer bytes starting at given index.
* @param {Uint8Array} bytes A uint8 array.
* @param {number} index Index.
* @returns {number} 32bits integer.
*/
public static readUint32At(bytes: Uint8Array, index: number): number {
return (bytes[index] + (bytes[index + 1] << 8) + (bytes[index + 2] << 16) + (bytes[index + 3] << 24)) >>> 0;
}
/**
* Convert uint value into buffer
* @param {number} uintValue A uint8 array.
* @param {number} bufferSize Buffer size.
* @returns {Uint8Array}
*/
public static uintToBuffer(uintValue: number, bufferSize: number): Uint8Array {
const buffer = new ArrayBuffer(bufferSize);
const dataView = new DataView(buffer);
try {
if (1 === bufferSize) {
dataView.setUint8(0, uintValue);
} else if (2 === bufferSize) {
dataView.setUint16(0, uintValue, true);
} else if (4 === bufferSize) {
dataView.setUint32(0, uintValue, true);
} else {
throw new Error('Unexpected bufferSize ' + bufferSize);
}
return new Uint8Array(buffer);
} catch (e) {
throw new Error(`Converting uint value ` + uintValue + ` into buffer with error: ` + e);
}
}
/**
* Convert uint value into buffer
* @param {number} uintValue A uint8 array.
* @returns {Uint8Array}
*/
public static uint8ToBuffer(uintValue: number): Uint8Array {
return GeneratorUtils.uintToBuffer(uintValue, 1);
}
/**
* Convert uint value into buffer
* @param {number} uintValue A uint8 array.
* @returns {Uint8Array}
*/
public static uint16ToBuffer(uintValue: number): Uint8Array {
return GeneratorUtils.uintToBuffer(uintValue, 2);
}
/**
* Convert uint value into buffer
* @param {number} uintValue A uint8 array.
* @returns {Uint8Array}
*/
public static uint32ToBuffer(uintValue: number): Uint8Array {
return GeneratorUtils.uintToBuffer(uintValue, 4);
}
/**
* It validates that a value is not undefined or null
* @param value the value
* @param message the message in the exception if the value is null or undefined.
*/
public static notNull(value: any, message: string): void {
if (value === undefined || value === null) {
throw new Error(message);
}
}
/**
* Convert uint8 array buffer into number
* @param {Uint8Array} buffer A uint8 array.
* @returns {number}
*/
public static bufferToUint(buffer: Uint8Array, size: number): number {
const dataView = new DataView(buffer.buffer);
try {
if (1 === size) {
return dataView.getUint8(0);
} else if (2 === size) {
return dataView.getUint16(0, true);
} else if (4 === size) {
return dataView.getUint32(0, true);
}
throw new Error('Unexpected size ' + size);
} catch (e) {
throw new Error(`Converting buffer into number with error:` + e);
}
}
/**
* Convert uint8 array buffer into number
* @param {Uint8Array} buffer A uint8 array.
* @returns {number}
*/
public static bufferToUint8(buffer: Uint8Array): number {
return GeneratorUtils.bufferToUint(buffer, 1);
}
/**
* Convert uint8 array buffer into number
* @param {Uint8Array} buffer A uint8 array.
* @returns {number}
*/
public static bufferToUint16(buffer: Uint8Array): number {
return GeneratorUtils.bufferToUint(buffer, 2);
}
/**
* Convert uint8 array buffer into number
* @param {Uint8Array} buffer A uint8 array.
* @returns {number}
*/
public static bufferToUint32(buffer: Uint8Array): number {
return GeneratorUtils.bufferToUint(buffer, 4);
}
/**
* Convert unit64 into buffer
* @param {number} uintValue Uint64 (number[]).
* @returns {Uint8Array}
*/
public static uint64ToBuffer(uintValue: number[] | number): Uint8Array {
const uint32Array = new Uint32Array(GeneratorUtils.fromUint(uintValue));
return new Uint8Array(uint32Array.buffer);
}
/**
* Concatenate two arrays
* @param {Uint8Array} array1 A Uint8Array.
* @param {Uint8Array} array2 A Uint8Array.
* @returns {Uint8Array}
*/
public static concatTypedArrays(array1: Uint8Array, array2: Uint8Array): Uint8Array {
const newArray = new Uint8Array(array1.length + array2.length);
newArray.set(array1);
newArray.set(array2, array1.length);
return newArray;
}
/** Converts an unsigned byte to a signed byte with the same binary representation.
* @param {number} input An unsigned byte.
* @returns {number} A signed byte with the same binary representation as the input.
*
*/
public static uint8ToInt8 = (input: number): number => {
if (0xff < input) {
throw Error(`input '` + input + `' is out of range`);
}
return (input << 24) >> 24;
};
/** Get bytes by given sub array size.
* @param {Uint8Array} binary Binary bytes array.
* @param {number} size Subarray size.
* @returns {Uint8Array}
*
*/
public static getBytes(binary: Uint8Array, size: number): Uint8Array {
if (size > binary.length) {
throw new RangeError();
}
const bytes = binary.slice(0, size);
return bytes;
}
/**
* Gets the padding size that rounds up \a size to the next multiple of \a alignment.
* @param size Inner element size
* @param alignment Next multiple alignment
*/
public static getPaddingSize(size: number, alignment: number): number {
if (alignment === 0) {
return 0;
}
return 0 === size % alignment ? 0 : alignment - (size % alignment);
}
/**
* Adds the padding to the reported size according to the alignment
* @param size the size
* @param alignment the alignment
*/
public static getSizeWithPadding(size: number, alignment: number): number {
return size + GeneratorUtils.getPaddingSize(size, alignment);
}
/**
* Tries to compact a uint64 into a simple numeric.
* @param {module:coders/uint64~uint64} uint64 A uint64 value.
* @returns {number|module:coders/uint64~uint64}
* A numeric if the uint64 is no greater than Number.MAX_SAFE_INTEGER or the original uint64 value otherwise.
*/
public static compact(uint64: number[] | number): number {
if (Array.isArray(uint64)) {
const low = uint64[0];
const high = uint64[1];
// don't compact if the value is >= 2^53
if (0x00200000 <= high) {
throw new Error('Cannot compact number: ' + uint64);
}
// multiply because javascript bit operations operate on 32bit values
return high * 0x100000000 + low;
} else {
return uint64;
}
}
/**
* Converts a numeric unsigned integer into a uint64.
* @param {number} number The unsigned integer.
* @returns {module:coders/uint64~uint64} The uint64 representation of the input.
*/
public static fromUint(number: number | number[]): number[] {
if (Array.isArray(number)) {
return number;
}
return [(number & 0xffffffff) >>> 0, (number / 0x100000000) >>> 0];
}
/**
* It loads a static list of entities from the payload
* @param loadFromBinary the factory function
* @param payload the payload
* @param count the amount of entities
*/
public static loadFromBinary(
loadFromBinary: (payload: Uint8Array) => T,
payload: Uint8Array,
count: number | number[],
): T[] {
const byteArray = Array.from(payload);
const values: T[] = [];
for (let i = 0; i < GeneratorUtils.compact(count); i++) {
const item = loadFromBinary(Uint8Array.from(byteArray));
const itemSize = item.getSize();
values.push(item);
byteArray.splice(0, itemSize);
}
return values;
}
/**
* Loads a list of numbers from the array based on the count and number size.
* @param payload the payload
* @param count the count
* @param itemSize the number size.
*/
public static loadFromBinaryEnums(payload: Uint8Array, count: number | number[], itemSize: number): number[] {
const byteArray = Array.from(payload);
const values: number[] = [];
for (let i = 0; i < GeneratorUtils.compact(count); i++) {
values.push(GeneratorUtils.bufferToUint(payload, 2));
byteArray.splice(0, itemSize);
}
return values;
}
/**
* It loads a static list of entities from the payload
* @param loadFromBinary the factory function
* @param payload the payload
* @param payloadSize the amount of bytes to process.
* @param alignment for the padding
*/
public static loadFromBinaryRemaining(
loadFromBinary: (payload: Uint8Array) => T,
payload: Uint8Array,
payloadSize: number,
alignment: number,
): T[] {
const byteArray = Array.from(payload);
let remainingByteSizes: number = payloadSize;
const transactions: T[] = [];
while (remainingByteSizes > 0) {
const item = loadFromBinary(Uint8Array.from(byteArray));
transactions.push(item);
const size = item.getSize();
const itemSize = size + GeneratorUtils.getPaddingSize(item.getSize(), alignment);
remainingByteSizes -= itemSize;
byteArray.splice(0, itemSize);
}
return transactions;
}
/**
* It converts a list of buffers into an Uint8Array
* @param elements the buffers to serialize
* @param alignment add padding to each element according to the alignment.
* @return the serialized buffer
*/
public static writeList(elements: Serializer[], alignment: number): Uint8Array {
return elements.reduce((newArray, item) => {
const byte = item.serialize();
const padding = new Uint8Array(GeneratorUtils.getPaddingSize(byte.length, alignment));
return GeneratorUtils.concatTypedArrays(newArray, GeneratorUtils.concatTypedArrays(byte, padding));
}, Uint8Array.from([]));
}
/**
* It serializes a list of number to a Uint8Array
* @param elements
* @param alignment
*/
public static writeListEnum(elements: number[], alignment: number): Uint8Array {
return elements.reduce((newArray, item) => {
const byte = GeneratorUtils.uint16ToBuffer(item);
const padding = new Uint8Array(GeneratorUtils.getPaddingSize(byte.length, alignment));
return GeneratorUtils.concatTypedArrays(newArray, GeneratorUtils.concatTypedArrays(byte, padding));
}, Uint8Array.from([]));
}
/**
* It generates a list of flags from an aggregated value
*
* @param enumClass the enum class holding all the possible values
* @param bitMaskValue the aggregate value
* @param the flags
*/
public static toFlags(enumClass: any, bitMaskValue: number): number[] {
const values: number[] = Object.keys(enumClass)
.map((key) => enumClass[key])
.filter((k) => parseInt(k) >= 0)
.map((k) => parseInt(k));
return values.filter((value) => (value & bitMaskValue) !== 0);
}
/**
* It converts a list of flag into an aggregated number
* @param enumClass the enum class to know the valid numbers
* @param flags the flags
*/
public static fromFlags(enumClass: any, flags: number[]): number {
const values: number[] = Object.keys(enumClass)
.map((key) => enumClass[key])
.filter((k) => parseInt(k) >= 0)
.map((k) => parseInt(k));
return flags.filter((f) => values.indexOf(f) > -1).reduce((a, b) => a + b, 0);
}
}