Skip to content

Commit c1b849b

Browse files
authored
feat: add random integer generation operation (#2151)
1 parent 1ef4afc commit c1b849b

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

src/core/config/Categories.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@
554554
"P-list Viewer",
555555
"Disassemble x86",
556556
"Pseudo-Random Number Generator",
557+
"Pseudo-Random Integer Generator",
557558
"Generate De Bruijn Sequence",
558559
"Generate UUID",
559560
"Analyse UUID",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @author cktgh [chankaitung@gmail.com]
3+
* @copyright Crown Copyright 2026
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import forge from "node-forge";
10+
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
11+
import { DELIM_OPTIONS } from "../lib/Delim.mjs";
12+
13+
/**
14+
* Pseudo-Random Integer Generator operation
15+
*/
16+
class PseudoRandomIntegerGenerator extends Operation {
17+
18+
// in theory 2**53 is the max range, but we use Number.MAX_SAFE_INTEGER (2**53 - 1) as it is more consistent.
19+
static MAX_RANGE = Number.MAX_SAFE_INTEGER;
20+
// arbitrary choice
21+
static BUFFER_SIZE = 1024;
22+
23+
/**
24+
* PseudoRandomIntegerGenerator constructor
25+
*/
26+
constructor() {
27+
super();
28+
29+
this.name = "Pseudo-Random Integer Generator";
30+
this.module = "Ciphers";
31+
this.description = "A cryptographically-secure pseudo-random number generator (PRNG).<br><br>Generates random integers within a specified range using the browser's built-in <code>crypto.getRandomValues()</code> method if available.<br><br>The supported range of integers is from <code>-(2^53 - 1)</code> to <code>(2^53 - 1)</code>.";
32+
this.infoURL = "https://wikipedia.org/wiki/Pseudorandom_number_generator";
33+
this.inputType = "string";
34+
this.outputType = "string";
35+
this.args = [
36+
{
37+
"name": "Number of Integers",
38+
"type": "number",
39+
"value": 1,
40+
"min": 1
41+
},
42+
{
43+
"name": "Min Value",
44+
"type": "number",
45+
"value": 0,
46+
"min": Number.MIN_SAFE_INTEGER,
47+
"max": Number.MAX_SAFE_INTEGER
48+
},
49+
{
50+
"name": "Max Value",
51+
"type": "number",
52+
"value": 99,
53+
"min": Number.MIN_SAFE_INTEGER,
54+
"max": Number.MAX_SAFE_INTEGER
55+
},
56+
{
57+
"name": "Delimiter",
58+
"type": "option",
59+
"value": DELIM_OPTIONS
60+
},
61+
{
62+
"name": "Output",
63+
"type": "option",
64+
"value": ["Raw", "Hex", "Decimal"]
65+
}
66+
];
67+
68+
// not using BigUint64Array to avoid BigInt handling overhead
69+
this.randomBuffer = new Uint32Array(PseudoRandomIntegerGenerator.BUFFER_SIZE);
70+
this.randomBufferOffset = PseudoRandomIntegerGenerator.BUFFER_SIZE;
71+
}
72+
73+
/**
74+
* @param {string} input
75+
* @param {Object[]} args
76+
* @returns {string}
77+
*/
78+
run(input, args) {
79+
const [numInts, minInt, maxInt, delimiter, outputType] = args;
80+
81+
if (minInt === null || maxInt === null) return "";
82+
83+
const min = Math.ceil(minInt);
84+
const max = Math.floor(maxInt);
85+
const delim = Utils.charRep(delimiter || "Space");
86+
87+
if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max)) {
88+
throw new OperationError("Min and Max must be between `-(2^53 - 1)` and `2^53 - 1`.");
89+
}
90+
if (min > max) {
91+
throw new OperationError("Min cannot be larger than Max.");
92+
}
93+
const range = max - min + 1; // inclusive range
94+
if (range > PseudoRandomIntegerGenerator.MAX_RANGE) {
95+
throw new OperationError("Range between Min and Max cannot be larger than `2^53`");
96+
}
97+
98+
// as large as possible while divisible by range
99+
const rejectionThreshold = PseudoRandomIntegerGenerator.MAX_RANGE - (PseudoRandomIntegerGenerator.MAX_RANGE % range);
100+
const output = [];
101+
for (let i = 0; i < numInts; i++) {
102+
const result = this._generateRandomValue(rejectionThreshold);
103+
const intValue = min + (result % range);
104+
105+
switch (outputType) {
106+
case "Hex":
107+
output.push(intValue.toString(16));
108+
break;
109+
case "Decimal":
110+
output.push(intValue.toString(10));
111+
break;
112+
case "Raw":
113+
default:
114+
output.push(Utils.chr(intValue));
115+
}
116+
}
117+
118+
if (outputType === "Raw") {
119+
return output.join("");
120+
}
121+
return output.join(delim);
122+
}
123+
124+
/**
125+
* Generate a random value, result will be less than the rejection threshold (exclusive).
126+
*
127+
* @param {number} rejectionThreshold
128+
* @returns {number}
129+
*/
130+
_generateRandomValue(rejectionThreshold) {
131+
let result;
132+
do {
133+
if (this.randomBufferOffset + 2 > this.randomBuffer.length) {
134+
this._resetRandomBuffer();
135+
}
136+
// stitching a 53 bit number; not using BigUint64Array to avoid BigInt handling overhead
137+
result = (this.randomBuffer[this.randomBufferOffset++] & 0x1f_ffff) * 0x1_0000_0000 +
138+
this.randomBuffer[this.randomBufferOffset++];
139+
} while (result >= rejectionThreshold);
140+
141+
return result;
142+
}
143+
144+
/**
145+
* Fill random buffer with new random values and rseet the offset.
146+
*/
147+
_resetRandomBuffer() {
148+
if (isWorkerEnvironment() && self.crypto) {
149+
self.crypto.getRandomValues(this.randomBuffer);
150+
} else {
151+
const bytes = forge.random.getBytesSync(this.randomBuffer.length * 4);
152+
for (let j = 0; j < this.randomBuffer.length; j++) {
153+
this.randomBuffer[j] = (bytes.charCodeAt(j * 4) << 24) |
154+
(bytes.charCodeAt(j * 4 + 1) << 16) |
155+
(bytes.charCodeAt(j * 4 + 2) << 8) |
156+
bytes.charCodeAt(j * 4 + 3);
157+
}
158+
}
159+
this.randomBufferOffset = 0;
160+
}
161+
162+
}
163+
164+
export default PseudoRandomIntegerGenerator;

0 commit comments

Comments
 (0)