Check sign flag for i{8, 16, 32, ...} conversions (#1867)

* Check sign flag for i{8, 16, 32, ...} conversions

* Ensure negative path is tested on i32 perf

* CHANGELOG

* Comment check on top-most signed bit

* toBn test array

* toNumber uses exposed toBn tests

* Re-use tests in toBn

* Re-use expectations inside bnToU8a

* Remove explicit BE (swap to non-optimal reverse)

* Adjust

* Remove defunct "only BE" comment

* typo

* Use bnToU8a tests in biToU8a

* Flatten tests

* Additional tests

* Additional tests
This commit is contained in:
Jaco
2023-08-22 20:08:30 +03:00
committed by GitHub
parent ce2122e9fa
commit 55ebc4f0d9
10 changed files with 219 additions and 769 deletions
+17 -18
View File
@@ -4,12 +4,29 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { arrayRange } from '../array/index.js';
import { TESTS } from '../bn/toU8a.spec.js';
import { perf } from '../test/index.js';
import { nToU8a } from './index.js';
const ptest = arrayRange(65536).map((v) => [v]);
describe('nToU8a', (): void => {
describe('conversion tests', (): void => {
for (let i = 0, count = TESTS.length; i < count; i++) {
const [isLe, isNegative, numarr, strval] = TESTS[i];
const bitLength = numarr.length * 8;
it(`${i}: converts from ${strval} (bitLength=${bitLength}, isLe=${isLe}, isNegative=${isNegative})`, (): void => {
expect(
nToU8a(
BigInt(strval),
{ bitLength, isLe, isNegative }
)
).toEqual(new Uint8Array(numarr));
});
}
});
it('converts null values to 0x00', (): void => {
expect(
nToU8a(null)
@@ -40,23 +57,5 @@ describe('nToU8a', (): void => {
).toEqual(new Uint8Array([0x56, 0x34, 0x12, 0x00]));
});
it('converts negative numbers', (): void => {
expect(
nToU8a(-1234n, { isNegative: true })
).toEqual(new Uint8Array([46, 251]));
});
it('converts negative numbers (BE)', (): void => {
expect(
nToU8a(-1234n, { isLe: false, isNegative: true })
).toEqual(new Uint8Array([251, 46]));
});
it('converts negative numbers (bitLength)', (): void => {
expect(
nToU8a(-1234n, { bitLength: 32, isNegative: true })
).toEqual(new Uint8Array([46, 251, 255, 255]));
});
perf('nToU8a', 250000, ptest, nToU8a);
});
+3 -2
View File
@@ -14,15 +14,16 @@ const NEG_MASK = BigInt(0xff);
function toU8a (value: bigint, isLe: boolean, isNegative: boolean): Uint8Array {
const arr: number[] = [];
const withSigned = isNegative && (value < _0n);
if (isNegative) {
if (withSigned) {
value = (value + _1n) * -_1n;
}
while (value !== _0n) {
const mod = value % DIV;
const val = Number(
isNegative
withSigned
? mod ^ NEG_MASK
: mod
);
+56 -62
View File
@@ -9,7 +9,63 @@ import { BN, bnToU8a } from './index.js';
const ptest = arrayRange(65536).map((v) => [v]);
// eslint-disable-next-line jest/no-export
export const TESTS: [isLe: boolean, isNegative: boolean, numarr: number[], strval: string][] = [
// LE, positive numbers
[true, false, [0x12], '18'],
[true, false, [0x12, 0x34], '13330'],
[true, false, [0x12, 0x34, 0x56], '5649426'],
[true, false, [0x12, 0x34, 0x56, 0x78], '2018915346'],
[true, false, [0x12, 0x34, 0x56, 0x78, 0x9a], '663443878930'],
[true, false, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], '207371629900818'],
[true, false, [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78], '159954953172672629770948536149615195154'],
// LE, positive numbers (w/ signed flag)
[true, true, [12], '12'],
[true, true, [210, 4], '1234'],
[true, true, [64, 226, 1], '123456'],
[true, true, [21, 205, 91, 7], '123456789'],
[true, true, [203, 36, 104, 12, 8], '34567890123'],
[true, true, [255, 159, 114, 78, 24, 9], '9999999999999'],
// LE, negative numbers
[true, true, [244], '-12'],
[true, true, [46, 251], '-1234'],
[true, true, [192, 29, 254], '-123456'],
[true, true, [255, 255, 255, 255], '-1'],
[true, true, [254, 255, 255, 255], '-2'],
[true, true, [235, 50, 164, 248], '-123456789'],
[true, true, [0, 0, 0, 128], '-2147483648'],
[true, true, [0, 0, 0, 240], '-268435456'],
[true, true, [65, 86, 129, 173, 254], '-5678999999'],
[true, true, [1, 96, 141, 177, 231, 246], '-9999999999999'],
[true, true, [1, 0, 156, 88, 76, 73, 31, 242], '-999999999999999999'],
// BE
[false, false, [0x12], '18'],
[false, false, [0x12, 0x34], '4660'],
[false, false, [0x12, 0x34, 0x56], '1193046'],
[false, false, [0x12, 0x34, 0x56, 0x78], '305419896'],
[false, true, [0xf2, 0x34, 0x56, 0x78], '-231451016'],
[false, false, [0x12, 0x34, 0x56, 0x78, 0x9a], '78187493530'],
[false, false, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], '20015998343868'],
[false, false, [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78], '24197857161011715162171839636988778104']
];
describe('bnToU8a', (): void => {
describe('conversion tests', (): void => {
for (let i = 0, count = TESTS.length; i < count; i++) {
const [isLe, isNegative, numarr, strval] = TESTS[i];
const bitLength = numarr.length * 8;
it(`${i}: converts from ${strval} (bitLength=${bitLength}, isLe=${isLe}, isNegative=${isNegative})`, (): void => {
expect(
bnToU8a(
new BN(strval),
{ bitLength, isLe, isNegative }
)
).toEqual(new Uint8Array(numarr));
});
}
});
it('converts null values to 0x00', (): void => {
expect(
bnToU8a(null)
@@ -40,67 +96,5 @@ describe('bnToU8a', (): void => {
).toEqual(new Uint8Array([0x56, 0x34, 0x12, 0x00]));
});
describe('signed', (): void => {
it('converts negative numbers (BE)', (): void => {
expect(
bnToU8a(new BN(-1234), { isLe: false, isNegative: true })
).toEqual(new Uint8Array([251, 46]));
});
it('converts negative numbers (LE, i8)', (): void => {
expect(
bnToU8a(new BN(-12), { isNegative: true })
).toEqual(new Uint8Array([244]));
});
it('converts negative numbers (LE, i16)', (): void => {
expect(
bnToU8a(new BN(-1234), { isNegative: true })
).toEqual(new Uint8Array([46, 251]));
});
it('converts negative numbers (LE, i24)', (): void => {
expect(
bnToU8a(new BN(-123456), { isNegative: true })
).toEqual(new Uint8Array([192, 29, 254]));
});
it('converts negative numbers (LE, i32)', (): void => {
expect(
bnToU8a(new BN(-123456789), { isNegative: true })
).toEqual(new Uint8Array([235, 50, 164, 248]));
});
it('converts negative numbers (LE, i40)', (): void => {
expect(
bnToU8a(new BN(-5678999999), { isNegative: true })
).toEqual(new Uint8Array([65, 86, 129, 173, 254]));
});
it('converts negative numbers (LE, i48)', (): void => {
expect(
bnToU8a(new BN(-9999999999999), { isNegative: true })
).toEqual(new Uint8Array([1, 96, 141, 177, 231, 246]));
});
it('converts negative numbers (LE, i64)', (): void => {
expect(
bnToU8a(new BN('-999999999999999999'), { isNegative: true })
).toEqual(new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242]));
});
it('converts negative numbers (LE, bitLength)', (): void => {
expect(
bnToU8a(new BN(-1234), { bitLength: 32, isNegative: true })
).toEqual(new Uint8Array([46, 251, 255, 255]));
});
it('converts negative numbers (LE, bitLength, check)', (): void => {
expect(
bnToU8a(new BN(-123456), { bitLength: 32, isNegative: true })
).toEqual(new Uint8Array([192, 29, 254, 255]));
});
});
perf('bnToU8a', 250000, ptest, bnToU8a);
});
+22 -242
View File
@@ -3,6 +3,7 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { TESTS } from '../bn/toU8a.spec.js';
import { perf } from '../test/index.js';
import { u8aToBigInt } from './index.js';
@@ -16,238 +17,41 @@ describe('u8aToBigInt', (): void => {
).toBe('3412');
});
describe('LE', (): void => {
describe('unsigned', (): void => {
it('converts values (u8)', (): void => {
describe('conversion tests', (): void => {
for (let i = 0, count = TESTS.length; i < count; i++) {
const [isLe, isNegative, numarr, strval] = TESTS[i];
it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12]),
{ isLe: true }
).toString(16)
).toBe('12');
});
it('converts values (u16)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34]),
{ isLe: true }
).toString(16)
).toBe('3412');
});
it('converts values (u24)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56]),
{ isLe: true }
).toString(16)
).toBe('563412');
});
it('converts values (u32)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412');
});
it('converts values (i32)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0xf2, 0x34, 0x56, 0x78]),
{ isLe: false, isNegative: true }
).toString(16)
).toBe('-dcba988');
});
it('converts values (u40)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]),
{ isLe: true }
).toString(16)
).toBe('9a78563412');
});
it('converts values (u48)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]),
{ isLe: true }
).toString(16)
).toBe('bc9a78563412');
});
it('converts values (u128)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412785634127856341278563412');
new Uint8Array(numarr),
{ isLe, isNegative }
).toString()
).toBe(strval);
});
}
});
describe('length tests', (): void => {
[true, false].forEach((isLe) => {
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
it(`converts values with bitLength=${i * 8}, isLe=${isLe}`, (): void => {
expect(
u8aToBigInt(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: true }
{ isLe }
).toString(16)
).toBe(tstr.slice(0, i).reverse().join(''));
).toBe(
isLe
? tstr.slice(0, i).reverse().join('')
: tstr.slice(0, i).join('')
);
});
}
});
describe('signed', (): void => {
it('converts values (i8)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([244]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-12');
});
it('converts values (i16)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([46, 251]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-1234');
});
it('converts values (i24)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([192, 29, 254]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-123456');
});
it('converts values (i32)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([235, 50, 164, 248]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-123456789');
});
it('converts values (i40)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([65, 86, 129, 173, 254]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-5678999999');
});
it('converts values (i48)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([1, 96, 141, 177, 231, 246]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-9999999999999');
});
it('converts values (i64)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-999999999999999999');
});
});
});
describe('BE', (): void => {
it('converts values (u8)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12]),
{ isLe: false }
).toString(16)
).toBe('12');
});
it('converts values (u16)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34]),
{ isLe: false }
).toString(16)
).toBe('1234');
});
it('converts values (u24)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56]),
{ isLe: false }
).toString(16)
).toBe('123456');
});
it('converts values (u32)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678');
});
it('converts values (u40)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]),
{ isLe: false }
).toString(16)
).toBe('123456789a');
});
it('converts values (u48)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]),
{ isLe: false }
).toString(16)
).toBe('123456789abc');
});
it('converts values (u128)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678123456781234567812345678');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBigInt(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: false }
).toString(16)
).toBe(tstr.slice(0, i).join(''));
});
}
});
describe('empty creation', (): void => {
@@ -288,26 +92,6 @@ describe('u8aToBigInt', (): void => {
});
});
describe('negative', (): void => {
it('handles negative numbers (little-endian)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([46, 251]),
{ isLe: true, isNegative: true }
)
).toBe(-1234n);
});
it('handles negative numbers (big-endian)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([251, 46]),
{ isLe: false, isNegative: true }
)
).toBe(-1234n);
});
});
it('handles overflows correctly (little-endian)', (): void => {
expect(
u8aToBigInt(
@@ -317,12 +101,8 @@ describe('u8aToBigInt', (): void => {
).toBe(256n);
});
perf('u8aToBigInt (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true }));
perf('u8aToBigInt (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true }));
perf('u8aToBigInt (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
// perf('BigInt (constructor)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString());
// perf('BigInt (constructor -> string)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString());
});
+3 -2
View File
@@ -16,13 +16,14 @@ const U64_MAX = BigInt('0x10000000000000000');
* @summary Creates a BigInt from a Uint8Array object.
*/
export function u8aToBigInt (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): bigint {
// BE is not the optimal path - LE is the SCALE default
// slice + reverse is expensive, however SCALE is LE by default so this is the path
// we are most interested in (the BE is added for the sake of being comprehensive)
const u8a = isLe
? value
: value.slice().reverse();
const count = u8a.length;
if (isNegative) {
if (isNegative && count && (u8a[count - 1] & 0x80)) {
switch (count) {
case 0:
return BigInt(0);
+22 -243
View File
@@ -3,7 +3,7 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// import { BN } from '../bn/index.js';
import { TESTS } from '../bn/toU8a.spec.js';
import { perf } from '../test/index.js';
import { u8aToBn } from './index.js';
@@ -17,238 +17,41 @@ describe('u8aToBn', (): void => {
).toBe('3412');
});
describe('LE', (): void => {
describe('unsigned', (): void => {
it('converts values (u8)', (): void => {
describe('conversion tests', (): void => {
for (let i = 0, count = TESTS.length; i < count; i++) {
const [isLe, isNegative, numarr, strval] = TESTS[i];
it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => {
expect(
u8aToBn(
new Uint8Array([0x12]),
{ isLe: true }
).toString(16)
).toBe('12');
});
it('converts values (u16)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34]),
{ isLe: true }
).toString(16)
).toBe('3412');
});
it('converts values (u24)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56]),
{ isLe: true }
).toString(16)
).toBe('563412');
});
it('converts values (u32)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412');
});
it('converts values (u40)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]),
{ isLe: true }
).toString(16)
).toBe('9a78563412');
});
it('converts values (u48)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]),
{ isLe: true }
).toString(16)
).toBe('bc9a78563412');
});
it('converts values (u128)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412785634127856341278563412');
new Uint8Array(numarr),
{ isLe, isNegative }
).toString()
).toBe(strval);
});
}
});
describe('length tests', (): void => {
[true, false].forEach((isLe) => {
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
it(`converts values with bitLength=${i * 8}, isLe=${isLe}`, (): void => {
expect(
u8aToBn(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: true }
{ isLe }
).toString(16)
).toBe(tstr.slice(0, i).reverse().join(''));
).toBe(
isLe
? tstr.slice(0, i).reverse().join('')
: tstr.slice(0, i).join('')
);
});
}
});
describe('signed', (): void => {
it('converts values (i8)', (): void => {
expect(
u8aToBn(
new Uint8Array([244]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-12');
});
it('converts values (i16)', (): void => {
expect(
u8aToBn(
new Uint8Array([46, 251]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-1234');
});
it('converts values (i24)', (): void => {
expect(
u8aToBn(
new Uint8Array([192, 29, 254]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-123456');
});
it('converts values (i32)', (): void => {
expect(
u8aToBn(
new Uint8Array([235, 50, 164, 248]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-123456789');
});
it('converts values (i40)', (): void => {
expect(
u8aToBn(
new Uint8Array([65, 86, 129, 173, 254]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-5678999999');
});
it('converts values (i48)', (): void => {
expect(
u8aToBn(
new Uint8Array([1, 96, 141, 177, 231, 246]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-9999999999999');
});
it('converts values (i64)', (): void => {
expect(
u8aToBn(
new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242]),
{ isLe: true, isNegative: true }
).toString()
).toBe('-999999999999999999');
});
});
});
describe('BE', (): void => {
it('converts values (u8)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12]),
{ isLe: false }
).toString(16)
).toBe('12');
});
it('converts values (u16)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34]),
{ isLe: false }
).toString(16)
).toBe('1234');
});
it('converts values (u24)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56]),
{ isLe: false }
).toString(16)
).toBe('123456');
});
it('converts values (u32)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678');
});
it('converts values (i32)', (): void => {
expect(
u8aToBn(
new Uint8Array([0xf2, 0x34, 0x56, 0x78]),
{ isLe: false, isNegative: true }
).toString(16)
).toBe('-dcba988');
});
it('converts values (u40)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]),
{ isLe: false }
).toString(16)
).toBe('123456789a');
});
it('converts values (u48)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]),
{ isLe: false }
).toString(16)
).toBe('123456789abc');
});
it('converts values (u128)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678123456781234567812345678');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBn(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: false }
).toString(16)
).toBe(tstr.slice(0, i).join(''));
});
}
});
describe('empty creation', (): void => {
@@ -289,26 +92,6 @@ describe('u8aToBn', (): void => {
});
});
describe('negative', (): void => {
it('handles negative numbers (little-endian)', (): void => {
expect(
u8aToBn(
new Uint8Array([46, 251]),
{ isLe: true, isNegative: true }
).toNumber()
).toBe(-1234);
});
it('handles negative numbers (big-endian)', (): void => {
expect(
u8aToBn(
new Uint8Array([251, 46]),
{ isLe: false, isNegative: true }
).toNumber()
).toBe(-1234);
});
});
it('handles overflows correctly (little-endian)', (): void => {
expect(
u8aToBn(
@@ -318,12 +101,8 @@ describe('u8aToBn', (): void => {
).toBe(256);
});
perf('u8aToBn (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true }));
perf('u8aToBn (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true }));
perf('u8aToBn (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
// perf('BN (constructor)', 1_000_000, [[12345678]], (v: number) => new BN(v));
// perf('BN (constructor -> string)', 1_000_000, [[12345678]], (v: number) => new BN(v).toString());
});
+47 -74
View File
@@ -24,104 +24,77 @@ import { BN } from '../bn/bn.js';
* ```
*/
export function u8aToBn (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): BN {
const count = value.length;
// slice + reverse is expensive, however SCALE is LE by default so this is the path
// we are most interested in (the BE is added for the sake of being comprehensive)
const u8a = isLe
? value
: value.slice().reverse();
const count = u8a.length;
// shortcut for <= u48 values - in this case the manual conversion
// here seems to be more efficient than passing the full array
if (isNegative) {
if (isLe) {
// Most common case i{8, 16, 32} default LE SCALE-encoded
// For <= 32, we also optimize the xor to a single op
switch (count) {
case 0:
return new BN(0);
case 1:
return new BN(((value[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
return new BN((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
case 3:
return new BN((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1);
case 4:
// for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to
// 32-bit, in the case where the top-most bit is set this yields a negative value
return new BN((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
case 5:
return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1);
case 6:
return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1);
default:
return new BN(value, 'le').fromTwos(value.length * 8);
}
}
if (count === 0) {
return new BN(0);
} else if (count > 6) {
return new BN(value, 'be').fromTwos(value.length * 8);
}
let result = 0;
for (let i = 0; i < count; i++) {
result = (result * 0x1_00) + (value[i] ^ 0xff);
}
return new BN((result * -1) - 1);
}
if (isLe) {
// Most common case - u{8, 16, 32} default LE SCALE-encoded
//
// There are some slight benefits in unrolling this specific loop,
// however it comes with diminishing returns since here the actual
// `new BN` does seem to take up the bulk of the time
if (isNegative && count && (u8a[count - 1] & 0x80)) {
// Most common case i{8, 16, 32} default LE SCALE-encoded
// For <= 32, we also optimize the xor to a single op
switch (count) {
case 0:
return new BN(0);
case 1:
return new BN(value[0]);
return new BN(((u8a[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
return new BN(value[0] + (value[1] << 8));
return new BN((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
case 3:
return new BN(value[0] + (value[1] << 8) + (value[2] << 16));
return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1);
case 4:
// for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to
// 32-bit, in the case where the top-most bit is set this yields a negative value
return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00));
return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
case 5:
return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00));
return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1);
case 6:
return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00));
return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1);
default:
return new BN(value, 'le');
return new BN(u8a, 'le').fromTwos(count * 8);
}
}
if (count === 0) {
return new BN(0);
} else if (count > 6) {
return new BN(value, 'be');
// Most common case - u{8, 16, 32} default LE SCALE-encoded
//
// There are some slight benefits in unrolling this specific loop,
// however it comes with diminishing returns since here the actual
// `new BN` does seem to take up the bulk of the time
switch (count) {
case 0:
return new BN(0);
case 1:
return new BN(u8a[0]);
case 2:
return new BN(u8a[0] + (u8a[1] << 8));
case 3:
return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16));
case 4:
// for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to
// 32-bit, in the case where the top-most bit is set this yields a negative value
return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00));
case 5:
return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00));
case 6:
return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00));
default:
return new BN(u8a, 'le');
}
let result = 0;
for (let i = 0; i < count; i++) {
result = (result * 0x1_00) + value[i];
}
return new BN(result);
}
+16 -104
View File
@@ -3,114 +3,26 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { TESTS } from '../bn/toU8a.spec.js';
import { perf } from '../test/index.js';
import { u8aToNumber } from './index.js';
const TESTS_NUM = TESTS.filter(([,, numarr]) => numarr.length <= 6);
describe('u8aToNumber', (): void => {
describe('unsigned', (): void => {
it('converts values (u8)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12])
)
).toBe(18);
});
describe('conversion tests', (): void => {
for (let i = 0, count = TESTS_NUM.length; i < count; i++) {
const [isLe, isNegative, numarr, strval] = TESTS_NUM[i];
it('converts values (u16)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12, 0x34])
)
).toBe(13330);
});
it('converts values (u24)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12, 0x34, 0x56])
)
).toBe(5649426);
});
it('converts values (u32)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12, 0x34, 0x56, 0x78])
)
).toBe(2018915346);
});
it('converts values (u40)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a])
)
).toBe(663443878930);
});
it('converts values (u48)', (): void => {
expect(
u8aToNumber(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc])
)
).toBe(207371629900818);
});
});
describe('signed', (): void => {
it('converts values (i8)', (): void => {
expect(
u8aToNumber(
new Uint8Array([244]),
{ isNegative: true }
)
).toBe(-12);
});
it('converts values (i16)', (): void => {
expect(
u8aToNumber(
new Uint8Array([46, 251]),
{ isNegative: true }
)
).toBe(-1234);
});
it('converts values (i24)', (): void => {
expect(
u8aToNumber(
new Uint8Array([192, 29, 254]),
{ isNegative: true }
)
).toBe(-123456);
});
it('converts values (i32)', (): void => {
expect(
u8aToNumber(
new Uint8Array([235, 50, 164, 248]),
{ isNegative: true }
)
).toBe(-123456789);
});
it('converts values (i40)', (): void => {
expect(
u8aToNumber(
new Uint8Array([65, 86, 129, 173, 254]),
{ isNegative: true }
)
).toBe(-5678999999);
});
it('converts values (i48)', (): void => {
expect(
u8aToNumber(
new Uint8Array([1, 96, 141, 177, 231, 246]),
{ isNegative: true }
)
).toBe(-9999999999999);
});
it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => {
expect(
u8aToNumber(
new Uint8Array(numarr),
{ isLe, isNegative }
).toString()
).toBe(strval);
});
}
});
describe('empty creation', (): void => {
@@ -132,6 +44,6 @@ describe('u8aToNumber', (): void => {
});
});
perf('u8aToNumber (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true }));
perf('u8aToNumber (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true }));
perf('u8aToNumber (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToNumber);
});
+26 -22
View File
@@ -1,44 +1,48 @@
// Copyright 2017-2023 @polkadot/util authors & contributors
// SPDX-License-Identifier: Apache-2.0
interface ToNumberOptions {
/**
* @description Number is signed, apply two's complement
*/
isNegative?: boolean;
}
import type { ToBnOptions } from '../types.js';
/**
* @name u8aToNumber
* @summary Creates a number from a Uint8Array object. This only operates on LE values as used in SCALE.
* @summary Creates a number from a Uint8Array object.
*/
export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumberOptions = {}): number {
const count = value.length;
export function u8aToNumber (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): number {
// slice + reverse is expensive, however SCALE is LE by default so this is the path
// we are most interested in (the BE is added for the sake of being comprehensive)
const u8a = isLe
? value
: value.slice().reverse();
const count = u8a.length;
if (isNegative) {
// When the value is a i{8, 16, 24, 32, 40, 40} values and the top-most bit
// indicates a signed value, we use a two's complement conversion. If one of these
// flags are not set, we just do a normal unsigned conversion (the same shortcut
// applies in both the u8aTo{BigInt, Bn} conversions as well)
if (isNegative && count && (u8a[count - 1] & 0x80)) {
switch (count) {
case 0:
return 0;
case 1:
return (((value[0] ^ 0x0000_00ff) * -1) - 1);
return (((u8a[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
return ((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
return ((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
case 3:
return ((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1);
return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1);
case 4:
// for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to
// 32-bit, in the case where the top-most bit is set this yields a negative value
return ((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
case 5:
return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1);
return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1);
case 6:
return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1);
return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1);
default:
throw new Error('Value more than 48-bits cannot be reliably converted');
@@ -50,24 +54,24 @@ export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumber
return 0;
case 1:
return value[0];
return u8a[0];
case 2:
return value[0] + (value[1] << 8);
return u8a[0] + (u8a[1] << 8);
case 3:
return value[0] + (value[1] << 8) + (value[2] << 16);
return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16);
case 4:
// for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to
// 32-bit, in the case where the top-most bit is set this yields a negative value
return value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00);
return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00);
case 5:
return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00);
return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00);
case 6:
return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00);
return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00);
default:
throw new Error('Value more than 48-bits cannot be reliably converted');