Release Helper Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
asUint8Array,
|
||||
assertBoundedBytes,
|
||||
assertBoundedItems,
|
||||
assertPositiveSafeInteger,
|
||||
type BytesLike,
|
||||
} from "./limits";
|
||||
|
||||
export type RandomSeed = string | number | bigint | BytesLike;
|
||||
|
||||
export interface SeededRandom {
|
||||
nextUint32(): number;
|
||||
nextFloat(): number;
|
||||
integer(minimum: number, maximumExclusive: number): number;
|
||||
bytes(length: number): Uint8Array<ArrayBuffer>;
|
||||
shuffle<T>(values: readonly T[]): T[];
|
||||
state(): readonly [number, number, number, number];
|
||||
}
|
||||
|
||||
export function secureRandomBytes(
|
||||
length: number,
|
||||
cryptoProvider: Pick<Crypto, "getRandomValues"> = crypto,
|
||||
maximumLength = 1024 * 1024,
|
||||
): Uint8Array<ArrayBuffer> {
|
||||
assertPositiveSafeInteger(maximumLength, "Maximum random byte length");
|
||||
if (!Number.isSafeInteger(length) || length < 0)
|
||||
throw new TypeError(
|
||||
"Random byte length must be a non-negative safe integer",
|
||||
);
|
||||
if (length > maximumLength)
|
||||
throw new RangeError(`Random byte length exceeds ${maximumLength}`);
|
||||
const output = new Uint8Array(length);
|
||||
for (let offset = 0; offset < output.length; offset += 65_536) {
|
||||
cryptoProvider.getRandomValues(
|
||||
output.subarray(offset, Math.min(output.length, offset + 65_536)),
|
||||
);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function secureRandomInt(
|
||||
minimum: number,
|
||||
maximumExclusive: number,
|
||||
cryptoProvider: Pick<Crypto, "getRandomValues"> = crypto,
|
||||
): number {
|
||||
validateIntegerRange(minimum, maximumExclusive);
|
||||
const range = maximumExclusive - minimum;
|
||||
if (range > 0x1_0000_0000)
|
||||
throw new RangeError("Secure random integer range cannot exceed 2^32");
|
||||
const limit = Math.floor(0x1_0000_0000 / range) * range;
|
||||
const sample = new Uint32Array(1);
|
||||
do cryptoProvider.getRandomValues(sample);
|
||||
while ((sample[0] ?? 0) >= limit);
|
||||
return minimum + ((sample[0] ?? 0) % range);
|
||||
}
|
||||
|
||||
export function createSeededRandom(seed: RandomSeed): SeededRandom {
|
||||
const state = seedState(seed);
|
||||
const nextUint32 = () => {
|
||||
const result = Math.imul(rotl(Math.imul(state[1] ?? 0, 5), 7), 9) >>> 0;
|
||||
const temporary = ((state[1] ?? 0) << 9) >>> 0;
|
||||
state[2] = ((state[2] ?? 0) ^ (state[0] ?? 0)) >>> 0;
|
||||
state[3] = ((state[3] ?? 0) ^ (state[1] ?? 0)) >>> 0;
|
||||
state[1] = ((state[1] ?? 0) ^ (state[2] ?? 0)) >>> 0;
|
||||
state[0] = ((state[0] ?? 0) ^ (state[3] ?? 0)) >>> 0;
|
||||
state[2] = ((state[2] ?? 0) ^ temporary) >>> 0;
|
||||
state[3] = rotl(state[3] ?? 0, 11);
|
||||
return result;
|
||||
};
|
||||
const integer = (minimum: number, maximumExclusive: number) => {
|
||||
validateIntegerRange(minimum, maximumExclusive);
|
||||
const range = maximumExclusive - minimum;
|
||||
if (range > 0x1_0000_0000)
|
||||
throw new RangeError("Seeded integer range cannot exceed 2^32");
|
||||
const limit = Math.floor(0x1_0000_0000 / range) * range;
|
||||
let sample: number;
|
||||
do sample = nextUint32();
|
||||
while (sample >= limit);
|
||||
return minimum + (sample % range);
|
||||
};
|
||||
return {
|
||||
nextUint32,
|
||||
nextFloat: () => nextUint32() / 0x1_0000_0000,
|
||||
integer,
|
||||
bytes(length) {
|
||||
assertBoundedItems(length, 1024 * 1024, "Random byte length");
|
||||
const output = new Uint8Array(length);
|
||||
let word = 0;
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (index % 4 === 0) word = nextUint32();
|
||||
output[index] = word & 0xff;
|
||||
word >>>= 8;
|
||||
}
|
||||
return output;
|
||||
},
|
||||
shuffle<T>(values: readonly T[]): T[] {
|
||||
assertBoundedItems(values.length);
|
||||
const output = [...values];
|
||||
for (let index = output.length - 1; index > 0; index -= 1) {
|
||||
const swap = integer(0, index + 1);
|
||||
[output[index], output[swap]] = [output[swap]!, output[index]!];
|
||||
}
|
||||
return output;
|
||||
},
|
||||
state: () =>
|
||||
[state[0] ?? 0, state[1] ?? 0, state[2] ?? 0, state[3] ?? 0] as const,
|
||||
};
|
||||
}
|
||||
|
||||
export function shuffleSeeded<T>(values: readonly T[], seed: RandomSeed): T[] {
|
||||
return createSeededRandom(seed).shuffle(values);
|
||||
}
|
||||
|
||||
function seedState(seed: RandomSeed): Uint32Array<ArrayBuffer> {
|
||||
let bytes: Uint8Array<ArrayBufferLike>;
|
||||
if (typeof seed === "string") bytes = new TextEncoder().encode(seed);
|
||||
else if (typeof seed === "number" || typeof seed === "bigint")
|
||||
bytes = new TextEncoder().encode(String(seed));
|
||||
else bytes = assertBoundedBytes(seed, 1024 * 1024, "Seed byte length");
|
||||
if (bytes.byteLength === 0) bytes = asUint8Array(new Uint8Array([0]));
|
||||
const state = new Uint32Array(4);
|
||||
let hash = 0x811c9dc5;
|
||||
for (let lane = 0; lane < state.length; lane += 1) {
|
||||
for (const byte of bytes) {
|
||||
hash ^= byte + lane;
|
||||
hash = Math.imul(hash, 0x01000193) >>> 0;
|
||||
hash ^= hash >>> 13;
|
||||
}
|
||||
hash = Math.imul(hash ^ (hash >>> 16), 0x85ebca6b) >>> 0;
|
||||
state[lane] = hash || 0x9e3779b9 ^ lane;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function rotl(value: number, count: number): number {
|
||||
return ((value << count) | (value >>> (32 - count))) >>> 0;
|
||||
}
|
||||
|
||||
function validateIntegerRange(minimum: number, maximumExclusive: number): void {
|
||||
if (
|
||||
!Number.isSafeInteger(minimum) ||
|
||||
!Number.isSafeInteger(maximumExclusive) ||
|
||||
maximumExclusive <= minimum
|
||||
) {
|
||||
throw new RangeError(
|
||||
"Random integer bounds must be safe integers with maximum greater than minimum",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user