From a065fa11c7cdc2362d94e716b3549fff93ff2f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=A1=E3=83=BC=E3=81=9A=28=EF=BD=A58=EF=BD=A5=29?= =?UTF-8?q?=E3=81=91=E3=83=BC=E3=81=8D?= <31585494+MineCake147E@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:57:12 +0900 Subject: [PATCH] Enhance: Ability to choose random number generator algorithm for `Math:gen_rng` (#731) * Updated `vite.config.js` so that it matches the `tsconfig.json` * Implemented unbiased variant of `Math:rnd` and `Math:gen_rng_unbiased` * Updated `aiscript.api.md` * * Refactored `Math:rnd` to use crypto * Refactored `Math:rnd_unbiased` to use crypto * Refactored `Math:gen_rng_unbiased` to use `SeedRandomWrapper` * Updated `aiscript.api.md` * * Removed `Math:rnd_unbiased` * * Updated `CHANGELOG.md` * Updated `std-math.md` * Implemented `Math:gen_rng_chacha20` * * Refactored `Math:gen_rng_unbiased` to use ChaCha20 * Refactored `Math:rnd` to use `crypto` * Refactored `Math:rnd_unbiased` to use `crypto` * + Added tests for `Math:gen_rng_unbiased` * Reduced potential random test failure * * `Math:gen_rng` now has 2nd parameter `algorithm` * *Updated `CHANGELOG.md` * Updated `std-math.md` * Updated `package-lock.json` * * Reverted `CHANGELOG.md` to match upstream * Added `random algorithms.md` * Updated std-math.md * Added initial support for option objects in `Math:gen_rng` * * Changed implementation of ChaCha20 * `Math:gen_rng` no longer accepts `str` options * Updated `std-math.md` * Updated `std-math.md` * Fixed `Math:gen_rng` returned `Promise` instead of `VNativeFn | VNull` * * Fixed ChaCha20 generating wrong values * Fixed potential overflow in ChaCha20 * * `Math:gen_rng`: `options` no longer accepts anything but `obj` or `undefined` * Invalid type for `seed` now throws exception in `Math:gen_rng` --- docs/std-math.md | 14 ++- src/interpreter/lib/std.ts | 40 ++++++--- src/utils/random/CryptoGen.ts | 29 ++++++ src/utils/random/chacha20.ts | 153 ++++++++++++++++++++++++++++++++ src/utils/random/genrng.ts | 48 ++++++++++ src/utils/random/randomBase.ts | 92 +++++++++++++++++++ src/utils/random/seedrandom.ts | 76 ++++++++++++++++ unreleased/random algorithms.md | 4 + 8 files changed, 441 insertions(+), 15 deletions(-) create mode 100644 src/utils/random/CryptoGen.ts create mode 100644 src/utils/random/chacha20.ts create mode 100644 src/utils/random/genrng.ts create mode 100644 src/utils/random/randomBase.ts create mode 100644 src/utils/random/seedrandom.ts create mode 100644 unreleased/random algorithms.md diff --git a/docs/std-math.md b/docs/std-math.md index a25b7bec..ffa2f845 100644 --- a/docs/std-math.md +++ b/docs/std-math.md @@ -120,8 +120,20 @@ _x_ +1の自然対数を計算します。 _min_ および _max_ を渡した場合、_min_ <= x, x <= _max_ の整数、 渡していない場合は 0 <= x, x < 1 の 小数が返されます。 -### @Math:gen_rng(_seed_: num | str): fn +### @Math:gen_rng(_seed_: num | str, _options_?: obj): @(_min_?: num, _max_?: num) シードから乱数生成機を生成します。 +生成された乱数生成器は、_min_ および _max_ を渡した場合、_min_ <= x, x <= _max_ の整数、 +渡していない場合は 0 <= x, x < 1 の浮動小数点数を返します。 +_options_ に渡したオブジェクトを通じて、内部の挙動を指定できます。 +`options.algorithm`の指定による挙動の変化は下記の通りです。 +| `options.algorithm` | 内部の乱数生成アルゴリズム | 範囲指定整数生成アルゴリズム | +|--|--|--| +| `rc4` | RC4 | Rejection Sampling | +| `rc4_legacy` | RC4 | 浮動小数点数演算による範囲制限​(0.19.0以前のアルゴリズム) | +| 無指定 または 'chacha20' | ChaCha20 | Rejection Sampling | + +> [!CAUTION] +> `rc4_legacy`等、浮動小数点数演算を伴う範囲指定整数生成アルゴリズムでは、演算時の丸め誤差により、指定した _max_ の値より大きな値が生成される可能性があります。 ## その他 ### @Math:clz32(_x_: num): num diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts index a0aed95c..c7e64d64 100644 --- a/src/interpreter/lib/std.ts +++ b/src/interpreter/lib/std.ts @@ -1,11 +1,12 @@ /* eslint-disable no-empty-pattern */ import { v4 as uuid } from 'uuid'; -import seedrandom from 'seedrandom'; import { NUM, STR, FN_NATIVE, FALSE, TRUE, ARR, NULL, BOOL, OBJ, ERROR } from '../value.js'; import { assertNumber, assertString, assertBoolean, valToJs, jsToVal, assertFunction, assertObject, eq, expectAny, assertArray, reprValue } from '../util.js'; import { AiScriptRuntimeError, AiScriptUserError } from '../../error.js'; import { AISCRIPT_VERSION } from '../../constants.js'; import { textDecoder } from '../../const.js'; +import { CryptoGen } from '../../utils/random/CryptoGen.js'; +import { GenerateChaCha20Random, GenerateLegacyRandom, GenerateRC4Random } from '../../utils/random/genrng.js'; import type { Value } from '../value.js'; export const std: Record = { @@ -453,23 +454,34 @@ export const std: Record = { 'Math:rnd': FN_NATIVE(([min, max]) => { if (min && min.type === 'num' && max && max.type === 'num') { - return NUM(Math.floor(Math.random() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); + const res = CryptoGen.instance.generateRandomIntegerInRange(min.value, max.value); + return res === null ? NULL : NUM(res); } - return NUM(Math.random()); + return NUM(CryptoGen.instance.generateNumber0To1()); }), - 'Math:gen_rng': FN_NATIVE(([seed]) => { + 'Math:gen_rng': FN_NATIVE(async ([seed, options]) => { expectAny(seed); - if (seed.type !== 'num' && seed.type !== 'str') return NULL; - - const rng = seedrandom(seed.value.toString()); - - return FN_NATIVE(([min, max]) => { - if (min && min.type === 'num' && max && max.type === 'num') { - return NUM(Math.floor(rng() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); - } - return NUM(rng()); - }); + let algo = 'chacha20'; + if (options?.type === 'obj') { + const v = options.value.get('algorithm'); + if (v?.type !== 'str') throw new AiScriptRuntimeError('`options.algorithm` must be string.'); + algo = v.value; + } + else if (options?.type !== undefined) { + throw new AiScriptRuntimeError('`options` must be an object if specified.'); + } + if (seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') throw new AiScriptRuntimeError('`seed` must be either number or string if specified.'); + switch (algo) { + case 'rc4_legacy': + return GenerateLegacyRandom(seed); + case 'rc4': + return GenerateRC4Random(seed); + case 'chacha20': + return await GenerateChaCha20Random(seed); + default: + throw new AiScriptRuntimeError('`options.algorithm` must be one of these: `chacha20`, `rc4`, or `rc4_legacy`.'); + } }), //#endregion diff --git a/src/utils/random/CryptoGen.ts b/src/utils/random/CryptoGen.ts new file mode 100644 index 00000000..c167c82f --- /dev/null +++ b/src/utils/random/CryptoGen.ts @@ -0,0 +1,29 @@ +import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; + +export class CryptoGen extends RandomBase { + private static _instance: CryptoGen = new CryptoGen(); + public static get instance() : CryptoGen { + return CryptoGen._instance; + } + + private constructor() { + super(); + } + + protected generateBigUintByBytes(bytes: number): bigint { + let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); + if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; + u8a = this.generateBytes(u8a.subarray(0, bytes)); + return readBigUintLittleEndian(u8a.buffer) ?? 0n; + } + + public generateBigUintByBits(bits: number): bigint { + if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; + const bytes = Math.ceil(bits / 8); + const wastedBits = BigInt(bytes * 8 - bits); + return this.generateBigUintByBytes(bytes) >> wastedBits; + } + public generateBytes(array: Uint8Array): Uint8Array { + return crypto.getRandomValues(array); + } +} diff --git a/src/utils/random/chacha20.ts b/src/utils/random/chacha20.ts new file mode 100644 index 00000000..631777fd --- /dev/null +++ b/src/utils/random/chacha20.ts @@ -0,0 +1,153 @@ +import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; + +// translated from https://github.com/skeeto/chacha-js/blob/master/chacha.js +const chacha20BlockSize = 64; +const CHACHA_ROUNDS = 20; +const CHACHA_KEYSIZE = 32; +const CHACHA_IVSIZE = 8; +function rotate(v: number, n: number): number { return (v << n) | (v >>> (32 - n)); } +function quarterRound(x: Uint32Array, a: number, b: number, c: number, d: number): void { + if (x.length < 16) return; + let va = x[a]; + let vb = x[b]; + let vc = x[c]; + let vd = x[d]; + if (va === undefined || vb === undefined || vc === undefined || vd === undefined) return; + va = (va + vb) | 0; + vd = rotate(vd ^ va, 16); + vc = (vc + vd) | 0; + vb = rotate(vb ^ vc, 12); + va = (va + vb) | 0; + vd = rotate(vd ^ va, 8); + vc = (vc + vd) | 0; + vb = rotate(vb ^ vc, 7); + x[a] = va; + x[b] = vb; + x[c] = vc; + x[d] = vd; +} +function generateChaCha20(dst: Uint32Array, state: Uint32Array) : void { + if (dst.length < 16 || state.length < 16) return; + dst.set(state); + for (let i = 0; i < CHACHA_ROUNDS; i += 2) { + quarterRound(dst, 0, 4, 8, 12); + quarterRound(dst, 1, 5, 9, 13); + quarterRound(dst, 2, 6, 10, 14); + quarterRound(dst, 3, 7, 11, 15); + quarterRound(dst, 0, 5, 10, 15); + quarterRound(dst, 1, 6, 11, 12); + quarterRound(dst, 2, 7, 8, 13); + quarterRound(dst, 3, 4, 9, 14); + } + for (let i = 0; i < 16; i++) { + let d = dst[i]; + const s = state[i]; + if (d === undefined || s === undefined) throw new Error('generateChaCha20: Something went wrong!'); + d = (d + s) | 0; + dst[i] = d; + } +} +export class ChaCha20 extends RandomBase { + private keynonce: Uint32Array; + private state: Uint32Array; + private buffer: Uint8Array; + private filledBuffer: Uint8Array; + private counter: bigint; + constructor(seed?: Uint8Array | undefined) { + const keyNonceBytes = CHACHA_IVSIZE + CHACHA_KEYSIZE; + super(); + let keynonce: Uint8Array; + if (typeof seed === 'undefined') { + keynonce = crypto.getRandomValues(new Uint8Array(keyNonceBytes)); + } else { + keynonce = seed; + if (keynonce.byteLength > keyNonceBytes) keynonce = seed.subarray(0, keyNonceBytes); + if (keynonce.byteLength < keyNonceBytes) { + const y = new Uint8Array(keyNonceBytes); + y.set(keynonce); + keynonce = y; + } + } + const key = keynonce.subarray(0, CHACHA_KEYSIZE); + const nonce = keynonce.subarray(CHACHA_KEYSIZE, CHACHA_KEYSIZE + CHACHA_IVSIZE); + const kn = new Uint8Array(16 * 4); + kn.set([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); + kn.set(key, 4 * 4); + kn.set(nonce, 14 * 4); + this.keynonce = new Uint32Array(kn.buffer); + this.state = new Uint32Array(16); + this.buffer = new Uint8Array(chacha20BlockSize); + this.counter = 0n; + this.filledBuffer = new Uint8Array(0); + } + private fillBuffer(): void { + this.buffer.fill(0); + this.buffer = this.fillBufferDirect(this.buffer); + this.filledBuffer = this.buffer; + } + private fillBufferDirect(buffer: Uint8Array): Uint8Array { + if ((buffer.length % chacha20BlockSize) !== 0) throw new Error('ChaCha20.fillBufferDirect should always be called with the buffer with the length a multiple-of-64!'); + buffer.fill(0); + let counter = this.counter; + const state = this.state; + const counterState = new BigUint64Array(state.buffer); + let dst = buffer; + while (dst.length > 0) { + const dbuf = dst.subarray(0, state.byteLength); + const dst32 = new Uint32Array(dbuf.buffer); + state.set(this.keynonce); + counterState[6] = BigInt.asUintN(64, counter); + generateChaCha20(dst32, state); + dst = dst.subarray(dbuf.length); + counter = BigInt.asUintN(64, counter + 1n); + } + this.counter = counter; + return buffer; + } + + protected generateBigUintByBytes(bytes: number): bigint { + let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); + if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; + u8a = this.generateBytes(u8a.subarray(0, bytes)); + return readBigUintLittleEndian(u8a.buffer) ?? 0n; + } + + public generateBigUintByBits(bits: number): bigint { + if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; + const bytes = Math.ceil(bits / 8); + const wastedBits = BigInt(bytes * 8 - bits); + return this.generateBigUintByBytes(bytes) >> wastedBits; + } + + public generateBytes(array: Uint8Array): Uint8Array { + if (array.length < 1) return array; + array.fill(0); + let dst = array; + if (dst.length <= this.filledBuffer.length) { + dst.set(this.filledBuffer.subarray(0, dst.length)); + this.filledBuffer = this.filledBuffer.subarray(dst.length); + return array; + } else { + while (dst.length > 0) { + if (this.filledBuffer.length === 0) { + if (dst.length >= chacha20BlockSize) { + const df64 = dst.subarray(0, dst.length - (dst.length % chacha20BlockSize)); + this.fillBufferDirect(df64); + dst = dst.subarray(df64.length); + continue; + } + this.fillBuffer(); + } + if (dst.length <= this.filledBuffer.length) { + dst.set(this.filledBuffer.subarray(0, dst.length)); + this.filledBuffer = this.filledBuffer.subarray(dst.length); + return array; + } + dst.set(this.filledBuffer); + dst = dst.subarray(this.filledBuffer.length); + this.fillBuffer(); + } + return array; + } + } +} diff --git a/src/utils/random/genrng.ts b/src/utils/random/genrng.ts new file mode 100644 index 00000000..f6a6a4ad --- /dev/null +++ b/src/utils/random/genrng.ts @@ -0,0 +1,48 @@ +import seedrandom from 'seedrandom'; +import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js'; +import { SeedRandomWrapper } from './seedrandom.js'; +import { ChaCha20 } from './chacha20.js'; +import type { VNativeFn, VNull, Value } from '../../interpreter/value.js'; +import { textEncoder } from '../../const.js'; + +export function GenerateLegacyRandom(seed: Value | undefined) : VNativeFn | VNull { + if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL; + const rng = seedrandom(seed.value.toString()); + return FN_NATIVE(([min, max]) => { + if (min && min.type === 'num' && max && max.type === 'num') { + return NUM(Math.floor(rng() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); + } + return NUM(rng()); + }); +} + +export function GenerateRC4Random(seed: Value | undefined) : VNativeFn | VNull { + if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL; + const rng = new SeedRandomWrapper(seed.value); + return FN_NATIVE(([min, max]) => { + if (min && min.type === 'num' && max && max.type === 'num') { + const result = rng.generateRandomIntegerInRange(min.value, max.value); + return typeof result === 'number' ? NUM(result) : NULL; + } + return NUM(rng.generateNumber0To1()); + }); +} + +export async function GenerateChaCha20Random(seed: Value | undefined) : Promise { + if (!seed || seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') return NULL; + let actualSeed : Uint8Array | undefined = undefined; + if (seed.type === 'num') + { + actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(new Float64Array([seed.value])))); + } else if (seed.type === 'str') { + actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(textEncoder.encode(seed.value)))); + } + const rng = new ChaCha20(actualSeed); + return FN_NATIVE(([min, max]) => { + if (min && min.type === 'num' && max && max.type === 'num') { + const result = rng.generateRandomIntegerInRange(min.value, max.value); + return typeof result === 'number' ? NUM(result) : NULL; + } + return NUM(rng.generateNumber0To1()); + }); +} diff --git a/src/utils/random/randomBase.ts b/src/utils/random/randomBase.ts new file mode 100644 index 00000000..3123fd07 --- /dev/null +++ b/src/utils/random/randomBase.ts @@ -0,0 +1,92 @@ +export const safeIntegerBits = Math.ceil(Math.log2(Number.MAX_SAFE_INTEGER)); +export const bigSafeIntegerBits = BigInt(safeIntegerBits); +export const bigMaxSafeIntegerExclusive = 1n << bigSafeIntegerBits; +export const fractionBits = safeIntegerBits - 1; +export const bigFractionBits = BigInt(fractionBits); + +export abstract class RandomBase { + protected abstract generateBigUintByBytes(bytes: number): bigint; + public abstract generateBigUintByBits(bits: number): bigint; + public abstract generateBytes(array: Uint8Array): Uint8Array; + + public generateNumber0To1(): number { + let res = this.generateBigUintByBits(safeIntegerBits); + let exponent = 1022; + let remainingFractionBits = safeIntegerBits - bitsToRepresent(res); + while (remainingFractionBits > 0 && exponent >= safeIntegerBits) { + exponent -= remainingFractionBits; + res <<= BigInt(remainingFractionBits); + res |= this.generateBigUintByBits(remainingFractionBits); + remainingFractionBits = safeIntegerBits - bitsToRepresent(res); + } + if (remainingFractionBits > 0) { + const shift = Math.min(exponent - 1, remainingFractionBits); + res <<= BigInt(shift); + res |= this.generateBigUintByBits(shift); + exponent = Math.max(exponent - shift, 0); + } + return (Number(res) * 0.5 ** safeIntegerBits) * (0.5 ** (1022 - exponent)); + } + + public generateUniform(maxInclusive: bigint): bigint { + if (maxInclusive < 1) return 0n; + const log2 = maxInclusive.toString(2).length; + const bytes = Math.ceil(log2 / 8); + const wastedBits = BigInt(bytes * 8 - log2); + let result: bigint; + do { + result = this.generateBigUintByBytes(bytes) >> wastedBits; + } while (result > maxInclusive); + return result; + } + + public generateRandomIntegerInRange(min: number, max: number): number | null { + const ceilMin = Math.ceil(min); + const floorMax = Math.floor(max); + const signedScale = floorMax - ceilMin; + if (signedScale === 0) return ceilMin; + const scale = Math.abs(signedScale); + const scaleSign = Math.sign(signedScale); + if (!Number.isSafeInteger(scale) || !Number.isSafeInteger(ceilMin) || !Number.isSafeInteger(floorMax)) { + return null; + } + const bigScale = BigInt(scale); + return Number(this.generateUniform(bigScale)) * scaleSign + ceilMin; + } +} + +export function bitsToRepresent(num: bigint): number { + if (num === 0n) return 0; + return num.toString(2).length; +} + +function readSmallBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null { + if (buffer.byteLength === 0) return null; + if (buffer.byteLength < 8) { + const array = new Uint8Array(8); + array.set(new Uint8Array(buffer)); + return new DataView(array.buffer).getBigUint64(0, true); + } + return new DataView(buffer).getBigUint64(0, true); +} + +export function readBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null { + if (buffer.byteLength === 0) return null; + if (buffer.byteLength <= 8) { + return readSmallBigUintLittleEndian(buffer); + } + const dataView = new DataView(buffer); + let pos = 0n; + let res = 0n; + let index = 0; + for (; index < dataView.byteLength - 7; index += 8, pos += 64n) { + const element = dataView.getBigUint64(index, true); + res |= element << pos; + } + if (index < dataView.byteLength) { + const array = new Uint8Array(8); + array.set(new Uint8Array(buffer, index)); + res |= new DataView(array.buffer).getBigUint64(0, true) << pos; + } + return res; +} diff --git a/src/utils/random/seedrandom.ts b/src/utils/random/seedrandom.ts new file mode 100644 index 00000000..5c6b3d5f --- /dev/null +++ b/src/utils/random/seedrandom.ts @@ -0,0 +1,76 @@ +import seedrandom from 'seedrandom'; +import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; + +const seedRandomBlockSize = Int32Array.BYTES_PER_ELEMENT; + +export class SeedRandomWrapper extends RandomBase { + private rng: seedrandom.PRNG; + private buffer: Uint8Array; + private filledBuffer: Uint8Array; + constructor(seed: string | number) { + super(); + this.rng = seedrandom(seed.toString()); + this.buffer = new Uint8Array(seedRandomBlockSize); + this.filledBuffer = new Uint8Array(0); + } + private fillBuffer(): void { + this.buffer.fill(0); + this.buffer = this.fillBufferDirect(this.buffer); + this.filledBuffer = this.buffer; + } + private fillBufferDirect(buffer: Uint8Array): Uint8Array { + if ((buffer.length % seedRandomBlockSize) !== 0) throw new Error(`SeedRandomWrapper.fillBufferDirect should always be called with the buffer with the length a multiple-of-${seedRandomBlockSize}!`); + const length = buffer.length / seedRandomBlockSize; + const dataView = new DataView(buffer.buffer); + let byteOffset = 0; + for (let index = 0; index < length; index++, byteOffset += seedRandomBlockSize) { + dataView.setInt32(byteOffset, this.rng.int32(), false); + } + return buffer; + } + protected generateBigUintByBytes(bytes: number): bigint { + let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); + if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; + u8a = this.generateBytes(u8a.subarray(0, bytes)); + return readBigUintLittleEndian(u8a.buffer) ?? 0n; + } + + public generateBigUintByBits(bits: number): bigint { + if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; + const bytes = Math.ceil(bits / 8); + const wastedBits = BigInt(bytes * 8 - bits); + return this.generateBigUintByBytes(bytes) >> wastedBits; + } + + public generateBytes(array: Uint8Array): Uint8Array { + if (array.length < 1) return array; + array.fill(0); + let dst = array; + if (dst.length <= this.filledBuffer.length) { + dst.set(this.filledBuffer.subarray(0, dst.length)); + this.filledBuffer = this.filledBuffer.subarray(dst.length); + return array; + } else { + while (dst.length > 0) { + if (this.filledBuffer.length === 0) { + if (dst.length >= seedRandomBlockSize) { + const df64 = dst.subarray(0, dst.length - (dst.length % seedRandomBlockSize)); + this.fillBufferDirect(df64); + dst = dst.subarray(df64.length); + continue; + } + this.fillBuffer(); + } + if (dst.length <= this.filledBuffer.length) { + dst.set(this.filledBuffer.subarray(0, dst.length)); + this.filledBuffer = this.filledBuffer.subarray(dst.length); + return array; + } + dst.set(this.filledBuffer); + dst = dst.subarray(this.filledBuffer.length); + this.fillBuffer(); + } + return array; + } + } +} diff --git a/unreleased/random algorithms.md b/unreleased/random algorithms.md new file mode 100644 index 00000000..4a922fd1 --- /dev/null +++ b/unreleased/random algorithms.md @@ -0,0 +1,4 @@ +- 関数`Math:gen_rng`に第二引数`algorithm`をオプション引数として追加。 + - アルゴリズムを`chacha20`、`rc4`、`rc4_legacy`から選べるようになりました。 + - **Breaking Change** `algorithm`を指定しない場合、`chacha20`が選択されます。 +- Fix: **Breaking Change** `Math:rnd`が範囲外の値を返す可能性があるのをアルゴリズムの変更により修正。