-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathbigint-encoder.js
More file actions
167 lines (147 loc) · 4.56 KB
/
bigint-encoder.js
File metadata and controls
167 lines (147 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* Encode a native `bigint` value from a list of arbitrary integer-like values.
*
* @param {Array<number|bigint|string>} parts - Slices to encode in big-endian
* format (i.e. earlier elements are higher bits)
* @param {64|128|256} size - Number of bits in the target integer type
* @param {boolean} unsigned - Whether it's an unsigned integer
*
* @returns {bigint}
*/
export function encodeBigIntFromBits(parts, size, unsigned) {
if (!(parts instanceof Array)) {
// allow a single parameter instead of an array
parts = [parts];
} else if (parts.length && parts[0] instanceof Array) {
// unpack nested array param
parts = parts[0];
}
const total = parts.length;
const sliceSize = size / total;
switch (sliceSize) {
case 32:
case 64:
case 128:
case 256:
break;
default:
throw new RangeError(
`expected slices to fit in 32/64/128/256 bits, got ${parts}`
);
}
// normalize all inputs to bigint
try {
for (let i = 0; i < parts.length; i++) {
if (typeof parts[i] !== 'bigint') {
parts[i] = BigInt(parts[i].valueOf());
}
}
} catch (e) {
throw new TypeError(`expected bigint-like values, got: ${parts} (${e})`);
}
// fast path: single value — validate and return directly without assembly
if (parts.length === 1) {
const value = parts[0];
if (unsigned && value < 0n) {
throw new RangeError(`expected a positive value, got: ${parts}`);
}
const [min, max] = calculateBigIntBoundaries(size, unsigned);
if (value < min || value > max) {
throw new TypeError(
`bigint value ${value} for ${formatIntName(
size,
unsigned
)} out of range [${min}, ${max}]`
);
}
return value;
}
// multi-part assembly: encode in big-endian fashion, shifting each slice
let result = 0n;
for (let i = 0; i < parts.length; i++) {
assertSliceFits(parts[i], sliceSize);
result |= BigInt.asUintN(sliceSize, parts[i]) << BigInt(i * sliceSize);
}
if (!unsigned) {
result = BigInt.asIntN(size, result);
}
// check boundaries
const [min, max] = calculateBigIntBoundaries(size, unsigned);
if (result >= min && result <= max) {
return result;
}
// failed to encode
throw new TypeError(
`bigint values [${parts}] for ${formatIntName(
size,
unsigned
)} out of range [${min}, ${max}]: ${result}`
);
}
/**
* Transforms a single bigint value that's supposed to represent a `size`-bit
* integer into a list of `sliceSize`d chunks.
*
* @param {bigint} value - Single bigint value to decompose
* @param {64|128|256} iSize - Number of bits represented by `value`
* @param {32|64|128} sliceSize - Number of chunks to decompose into
* @return {bigint[]}
*/
export function sliceBigInt(value, iSize, sliceSize) {
if (typeof value !== 'bigint') {
throw new TypeError(`Expected bigint 'value', got ${typeof value}`);
}
const total = iSize / sliceSize;
if (total === 1) {
return [value];
}
if (
sliceSize < 32 ||
sliceSize > 128 ||
(total !== 2 && total !== 4 && total !== 8)
) {
throw new TypeError(
`invalid bigint (${value}) and slice size (${iSize} -> ${sliceSize}) combination`
);
}
const shift = BigInt(sliceSize);
const mask = (1n << shift) - 1n;
const result = new Array(total);
for (let i = 0; i < total; i++) {
result[i] = value & mask;
value >>= shift;
}
return result;
}
export function formatIntName(precision, unsigned) {
return `${unsigned ? 'u' : 'i'}${precision}`;
}
/**
* Get min|max boundaries for an integer with a specified bits size
* @param {64|128|256} size - Number of bits in the source integer type
* @param {Boolean} unsigned - Whether it's an unsigned integer
* @return {BigInt[]}
*/
export function calculateBigIntBoundaries(size, unsigned) {
if (unsigned) {
return [0n, (1n << BigInt(size)) - 1n];
}
const boundary = 1n << BigInt(size - 1);
return [0n - boundary, boundary - 1n];
}
/**
* Asserts that a given part fits within the specified slice size.
* @param {bigint | number | string} part - The part to check.
* @param {number} sliceSize - The size of the slice in bits (e.g., 32, 64, 128)
* @returns {void}
* @throws {RangeError} If the part does not fit within the slice size.
*/
function assertSliceFits(part, sliceSize) {
const fitsSigned = BigInt.asIntN(sliceSize, part) === part;
const fitsUnsigned = BigInt.asUintN(sliceSize, part) === part;
if (!fitsSigned && !fitsUnsigned) {
throw new RangeError(
`slice value ${part} does not fit in ${sliceSize} bits`
);
}
}