Skip to content

Commit d6a6355

Browse files
committed
feat: add random integer generation operation
Signed-off-by: Thomas KT Chan <[email protected]>
1 parent 2a1294f commit d6a6355

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/core/config/Categories.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@
549549
"P-list Viewer",
550550
"Disassemble x86",
551551
"Pseudo-Random Number Generator",
552+
"Pseudo-Random Integer Generator",
552553
"Generate De Bruijn Sequence",
553554
"Generate UUID",
554555
"Analyse UUID",
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @author cktgh [[email protected]]
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+
63+
// not using BigUint64Array to avoid BigInt handling overhead
64+
this.randomBuffer = new Uint32Array(PseudoRandomIntegerGenerator.BUFFER_SIZE);
65+
this.randomBufferOffset = PseudoRandomIntegerGenerator.BUFFER_SIZE;
66+
}
67+
68+
/**
69+
* @param {string} input
70+
* @param {Object[]} args
71+
* @returns {string}
72+
*/
73+
run(input, args) {
74+
const [numInts, minInt, maxInt, delimiter] = args;
75+
76+
if (minInt === null || maxInt === null) return "";
77+
78+
const min = Math.ceil(minInt);
79+
const max = Math.floor(maxInt);
80+
const delim = Utils.charRep(delimiter || "Space");
81+
82+
if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max)) {
83+
throw new OperationError("Min and Max must be between `-(2^53 - 1)` and `2^53 - 1`.");
84+
}
85+
if (min > max) {
86+
throw new OperationError("Min cannot be larger than Max.");
87+
}
88+
const range = max - min + 1; //inclusive range
89+
if (range > PseudoRandomIntegerGenerator.MAX_RANGE) {
90+
throw new OperationError("Range between Min and Max cannot be larger than `2^53`");
91+
}
92+
93+
// as large as possible while divisible by range
94+
const rejectionThreshold = PseudoRandomIntegerGenerator.MAX_RANGE - (PseudoRandomIntegerGenerator.MAX_RANGE % range);
95+
let output = [];
96+
for (let i = 0; i < numInts; i++) {
97+
const result = this._generateRandomValue(rejectionThreshold);
98+
const intValue = min + (result % range);
99+
output.push(intValue.toString());
100+
}
101+
102+
return output.join(delim);
103+
}
104+
105+
// result will be less than the rejection threshold (exclusive)
106+
_generateRandomValue(rejectionThreshold) {
107+
let result;
108+
do {
109+
if (this.randomBufferOffset + 2 > this.randomBuffer.length) {
110+
this._resetRandomBuffer();
111+
}
112+
//stitching a 53 bit number; not using BigUint64Array to avoid BigInt handling overhead
113+
result = (this.randomBuffer[this.randomBufferOffset++] & 0x1f_ffff) * 0x1_0000_0000
114+
+ this.randomBuffer[this.randomBufferOffset++];
115+
} while (result >= rejectionThreshold);
116+
117+
return result;
118+
}
119+
120+
_resetRandomBuffer() {
121+
if (isWorkerEnvironment() && self.crypto) {
122+
self.crypto.getRandomValues(this.randomBuffer);
123+
} else {
124+
const bytes = forge.random.getBytesSync(this.randomBuffer.length * 4);
125+
for (let j = 0; j < this.randomBuffer.length; j++) {
126+
this.randomBuffer[j] = (bytes.charCodeAt(j * 4) << 24) |
127+
(bytes.charCodeAt(j * 4 + 1) << 16) |
128+
(bytes.charCodeAt(j * 4 + 2) << 8) |
129+
bytes.charCodeAt(j * 4 + 3);
130+
}
131+
}
132+
this.randomBufferOffset = 0;
133+
}
134+
135+
}
136+
137+
export default PseudoRandomIntegerGenerator;

0 commit comments

Comments
 (0)