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; shuffle(values: readonly T[]): T[]; state(): readonly [number, number, number, number]; } export function secureRandomBytes( length: number, cryptoProvider: Pick = crypto, maximumLength = 1024 * 1024, ): Uint8Array { 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, ): 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(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(values: readonly T[], seed: RandomSeed): T[] { return createSeededRandom(seed).shuffle(values); } function seedState(seed: RandomSeed): Uint32Array { let bytes: Uint8Array; 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", ); } }