Skip to content

Commit debdd00

Browse files
committed
refactor(randomInt): reduce repetitive code, update calls in testing, add more doc comments.
1 parent ec27277 commit debdd00

File tree

3 files changed

+162
-126
lines changed

3 files changed

+162
-126
lines changed

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/CryptoAPI.kt

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
*/
1313
package elide.runtime.intrinsics.js.node
1414

15-
1615
import org.graalvm.polyglot.Value
17-
import java.math.BigInteger
1816
import elide.annotations.API
1917
import elide.runtime.intrinsics.js.node.crypto.RandomIntCallback
2018
import elide.vm.annotations.Polyglot
@@ -34,31 +32,43 @@ import elide.vm.annotations.Polyglot
3432
*/
3533
@Polyglot public fun randomUUID(options: Value? = null): String
3634

35+
/**
36+
* ## Crypto: `randomInt`
37+
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
38+
*
39+
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
40+
*
41+
* @param min Lower bound (inclusive)
42+
* @param max Upper bound (exclusive)
43+
* @param callback Callback to receive the generated safe integer or an error.
44+
* @return Unit
45+
*/
46+
@Polyglot public fun randomInt(min: Long, max: Long, callback: RandomIntCallback)
47+
3748
/**
3849
* ## Crypto: randomInt
3950
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
4051
*
4152
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
4253
*
43-
* @param min
44-
* @param max
45-
* @param callback
46-
* @return A randomly generated integer between `min` (inclusive) and `max` (exclusive) or nothing if a callback was provided.
54+
* @param min Lower bound (inclusive)
55+
* @param max Upper bound (exclusive)
56+
* @return A randomly generated safe integer between `min` (inclusive) and `max` (exclusive).
4757
*/
48-
@Polyglot public fun randomInt(min: Long, max: Long, callback: RandomIntCallback? = null): Any
58+
@Polyglot public fun randomInt(min: Long, max: Long): Long
4959

5060
/**
5161
* ## Crypto: randomInt
5262
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
5363
*
5464
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
5565
*
56-
* @param min
57-
* @param max
58-
* @param callback
59-
* @return A randomly generated integer between `min` (inclusive) and `max` (exclusive) or nothing if a callback was provided.
66+
* @param min Lower bound (inclusive)
67+
* @param max Upper bound (exclusive)
68+
* @param callback Callback to receive the generated safe integer or an error.
69+
* @return Unit
6070
*/
61-
@Polyglot public fun randomInt(min: Value, max: Value, callback: Value?): Any
71+
@Polyglot public fun randomInt(min: Value, max: Value, callback: Value)
6272

6373
/**
6474
* ## Crypto: randomInt
@@ -68,18 +78,30 @@ import elide.vm.annotations.Polyglot
6878
*
6979
* @param max
7080
* @param callback
71-
* @return A randomly generated integer between `0` (inclusive) and `max` (exclusive) or nothing if a callback was provided.
81+
* @return A randomly generated safe integer between `0` (inclusive) and `max` (exclusive) or nothing if a callback was provided.
7282
*/
73-
@Polyglot public fun randomInt(max: Value, callback: Value?): Any
83+
@Polyglot public fun randomInt(max: Value, callback: Value?)
7484

7585
/**
7686
* ## Crypto: randomInt
7787
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
7888
*
7989
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
8090
*
81-
* @param max
82-
* @return A randomly generated integer between `0` (inclusive) and `max` (exclusive).
91+
* @param min Lower bound (inclusive)
92+
* @param max Upper bound (exclusive)
93+
* @return A randomly generated safe integer between `min` (inclusive) and `max` (exclusive).
94+
*/
95+
@Polyglot public fun randomInt(min: Value, max: Value): Long
96+
97+
/**
98+
* ## Crypto: randomInt
99+
* Generates a cryptographically secure random integer between `0` (inclusive) and the specified `max` (exclusive) value.
100+
*
101+
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
102+
*
103+
* @param max Upper bound (exclusive)
104+
* @return A randomly generated safe integer between `0` (inclusive) and `max` (exclusive).
83105
*/
84-
@Polyglot public fun randomInt(max: Value): Any
106+
@Polyglot public fun randomInt(max: Value): Long
85107
}

packages/graalvm/src/main/kotlin/elide/runtime/node/crypto/NodeCrypto.kt

Lines changed: 105 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,69 @@ private const val F_RANDOM_INT = "randomInt"
4242
// Cached Int generator to ensure we don't create multiple instances.
4343
private val cryptoRandomGenerator by lazy { SecureRandom() }
4444

45-
// Safe integer bounds for randomInt generation based on Node.js limits:
46-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
47-
// @TODO Remove duplicate definitions
48-
private const val MAX_RANDOM_INT_RANGE = (1L shl 48) - 1 // 281_474_976_710_655L
49-
private val MAX_48_BIT_LIMIT = BigInteger.valueOf(2L).pow(48) // 2^48
45+
// The maximum range (max - min) allowed is 2^48 in Node.js.
46+
private val MAX_48_BIT_LIMIT = BigInteger.valueOf(2L).pow(48)
47+
48+
// Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
49+
private fun genRandomInt(min: Long, max: Long): Long {
50+
try {
51+
return cryptoRandomGenerator.nextLong(min, max)
52+
} catch (e: Throwable) {
53+
throw TypeError.create("Error generating random bytes for randomInt: ${e.message}")
54+
}
55+
}
56+
57+
// Safely converts a Value to a BigInteger, ensuring it is a safe integer within JS limits.
58+
private fun safeValueToBigInt(value: Value, name: String): BigInteger {
59+
if (value.isNumber) {
60+
val bigIntValue: BigInteger? = when {
61+
value.fitsInLong() -> {
62+
BigInteger.valueOf(value.asLong())
63+
}
64+
// Reject integers that exceed Long.MAX_VALUE or are less than Long.MIN_VALUE
65+
value.fitsInBigInteger() -> throw RangeError.create("The \"$name\" argument must be a safe integer. Received an integer that exceeds the max bounds ${MAX_48_BIT_LIMIT}.")
66+
// Reject non-integer numbers
67+
value.fitsInDouble() -> {
68+
throw TypeError.create("The \"$name\" argument must be a safe integer. Received a non-integer number: ${value.asDouble()}.")
69+
}
70+
else -> null // Reject non-integer (e.g. Infinity, NaN, very large BigInts)
71+
}
72+
73+
// Define JS safe integer bounds
74+
val jsMaxSafeInt = BigInteger("9007199254740991") // 2^53 - 1
75+
val jsMinSafeInt = BigInteger("-9007199254740991") // -(2^53 - 1)
76+
77+
// Final check: even if conversion works, ensure it falls within JS safe limits
78+
if (bigIntValue != null && bigIntValue >= jsMinSafeInt && bigIntValue <= jsMaxSafeInt) {
79+
return bigIntValue
80+
}
81+
}
82+
// Invalid value type, we don't want it
83+
throw TypeError.create("The \"$name\" argument must be a safe integer. Received ${value}.")
84+
}
85+
86+
// Validates that the provided min and max values are safe integers and that the range difference does not exceed 2^48.
87+
private fun genSafeRange(min: Value, max: Value): Pair<Long, Long> {
88+
// Safely convert both inputs to BigInteger
89+
val minBigInt = safeValueToBigInt(min, "min")
90+
val maxBigInt = safeValueToBigInt(max, "max")
91+
92+
// Enforce the Min <= Max rule otherwise we throw a RangeError
93+
if (minBigInt >= maxBigInt) {
94+
throw RangeError.create("The value of \"max\" is out of range. It must be greater than the value of \"min\" (${minBigInt}). Received ${maxBigInt}.")
95+
}
96+
97+
val rangeDifference = maxBigInt.subtract(minBigInt)
98+
99+
// If the range difference exceeds 2^48, we throw a RangeError. Node.js has a range limit of 2^48 for randomInt.
100+
if (rangeDifference > MAX_48_BIT_LIMIT) {
101+
println("Range difference exceeds 2^48 limit: $rangeDifference")
102+
throw RangeError.create("The value of \"max - min\" is out of range. It must be <= 281474976710655. Received ${rangeDifference}.")
103+
}
104+
105+
// Return the validated safe Long values
106+
return Pair(minBigInt.toLong(), maxBigInt.toLong())
107+
}
50108

51109
// Installs the Node crypto module into the intrinsic bindings.
52110
@Intrinsic internal class NodeCryptoModule : AbstractNodeBuiltinModule() {
@@ -80,55 +138,61 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
80138
return java.util.UUID.randomUUID().toString()
81139
}
82140

83-
@Polyglot override fun randomInt(min: Long, max: Long, callback: RandomIntCallback?): Any {
84-
if (min >= max) throw RangeError.create(
85-
"The value of \"max\" is out of range. It must be greater than the value of \"min\" ($min). Received $max"
86-
)
141+
@Polyglot override fun randomInt(min: Long, max: Long): Long {
142+
return genRandomInt(min, max)
143+
}
87144

88-
val range = max - min
145+
@Polyglot override fun randomInt(min: Long, max: Long, callback: RandomIntCallback) {
146+
val randomValue = genRandomInt(min, max)
89147

90-
if (range > MAX_RANDOM_INT_RANGE) {
91-
throw RangeError.create(
92-
"The range (max - min) is out of bounds. It must be <= $MAX_RANDOM_INT_RANGE. " +
93-
"Received min=$min, max=$max"
94-
)
148+
thread {
149+
try {
150+
callback.invoke(null, randomValue)
151+
} catch (e: Throwable) {
152+
callback.invoke(TypeError.create(e.message ?: "Unknown error"), randomValue)
153+
}
95154
}
155+
}
96156

97-
var randomValue: Long
98-
try {
99-
randomValue = cryptoRandomGenerator.nextLong(min, max)
100-
} catch (e: Throwable) {
101-
throw TypeError.create("Error generating random bytes for randomInt: ${e.message}")
102-
}
157+
@Polyglot override fun randomInt(min: Value, max: Value, callback: Value) {
158+
val (safeMin, safeMax) = genSafeRange(min, max)
103159

104-
// Handle callback asynchronously if provided, otherwise we return the value directly to reflect Node.js behavior
105-
return if (callback != null) {
106-
thread {
107-
try {
108-
callback(null, randomValue)
109-
} catch (e: Throwable) {
110-
callback(TypeError.create(e.message ?: "Unknown error"), null)
111-
}
160+
val safeCallback: RandomIntCallback = callback.let { cb ->
161+
{ err: Throwable?, value: Long? ->
162+
cb.execute(
163+
err?.let { Value.asValue(it) },
164+
value?.let { Value.asValue(it) }
165+
)
112166
}
113-
Unit
114-
} else {
115-
randomValue
116167
}
168+
169+
return randomInt(safeMin, safeMax, safeCallback)
117170
}
118171

119-
@Polyglot override fun randomInt(min: Value, max: Value, callback: Value?): Any {
172+
@Polyglot override fun randomInt(min: Value, max: Value): Long {
120173
val (safeMin, safeMax) = genSafeRange(min, max)
121-
return randomInt(safeMin, safeMax, callback as? RandomIntCallback)
174+
return randomInt(safeMin, safeMax)
122175
}
123176

124-
@Polyglot override fun randomInt(max: Value, callback: Value?): Any {
177+
@Polyglot override fun randomInt(max: Value, callback: Value?) {
125178
val (safeMin, safeMax) = genSafeRange(Value.asValue(0), max)
126-
return randomInt(safeMin, safeMax, callback as? RandomIntCallback)
179+
val safeCallback: RandomIntCallback? = callback?.let { cb ->
180+
{ err: Throwable?, value: Long? ->
181+
cb.execute(
182+
err?.let { Value.asValue(it) },
183+
value?.let { Value.asValue(it) }
184+
)
185+
}
186+
}
187+
188+
if (safeCallback != null) {
189+
randomInt(safeMin, safeMax, safeCallback)
190+
}
127191
}
128192

129-
@Polyglot override fun randomInt(max: Value): Any {
193+
@Polyglot override fun randomInt(max: Value): Long {
130194
val (safeMin, safeMax) = genSafeRange(Value.asValue(0), max)
131-
return randomInt(safeMin, safeMax, null)
195+
return randomInt(safeMin, safeMax)
132196
}
133197

134198
// ProxyObject implementation
@@ -146,7 +210,6 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
146210
F_RANDOM_INT -> ProxyExecutable { args ->
147211
// Check if last argument is a callback function
148212
val lastIsCb = args.lastOrNull()?.canExecute() == true
149-
val cb = if (lastIsCb) args.last() else null
150213

151214
when (args.size) {
152215
1 -> {
@@ -156,71 +219,19 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
156219
2 -> {
157220
if (lastIsCb) {
158221
// randomInt(max, callback)
159-
this.randomInt(args[0], cb)
222+
this.randomInt(args[0], args.last())
160223
} else {
161224
// randomInt(min, max)
162-
this.randomInt(args[0], args[1], null)
225+
this.randomInt(args[0], args[1])
163226
}
164227
}
165228
3 -> {
166229
// randomInt(min, max, callback)
167-
this.randomInt(args[0], args[1], cb)
230+
this.randomInt(args[0], args[1], args.last())
168231
}
169232
else -> throw JsError.typeError("Invalid number of arguments for crypto.randomInt: ${args.size}")
170233
}
171234
}
172235
else -> null
173236
}
174-
175-
private fun safeValueToBigInt(value: Value, name: String): BigInteger {
176-
if (value.isNumber) {
177-
val bigIntValue: BigInteger? = when {
178-
value.fitsInLong() -> {
179-
BigInteger.valueOf(value.asLong())
180-
}
181-
// Reject integers that exceed Long.MAX_VALUE or are less than Long.MIN_VALUE
182-
value.fitsInBigInteger() -> throw RangeError.create("The \"$name\" argument must be a safe integer. Received an integer that exceeds the max bounds ${MAX_48_BIT_LIMIT}.")
183-
// Reject non-integer numbers
184-
value.fitsInDouble() -> {
185-
throw TypeError.create("The \"$name\" argument must be a safe integer. Received a non-integer number: ${value.asDouble()}.")
186-
}
187-
else -> null // Reject non-integer (e.g. Infinity, NaN, very large BigInts)
188-
}
189-
190-
// Define JS safe integer bounds
191-
val jsMaxSafeInt = BigInteger("9007199254740991") // 2^53 - 1
192-
val jsMinSafeInt = BigInteger("-9007199254740991") // -(2^53 - 1)
193-
194-
// Final check: even if conversion works, ensure it falls within JS safe limits
195-
if (bigIntValue != null && bigIntValue >= jsMinSafeInt && bigIntValue <= jsMaxSafeInt) {
196-
return bigIntValue
197-
}
198-
}
199-
// Invalid value type, we don't want it
200-
throw TypeError.create("The \"$name\" argument must be a safe integer. Received ${value}.")
201-
}
202-
203-
// Validates that the provided min and max values are safe integers and that the range difference does not exceed 2^48.
204-
private fun genSafeRange(min: Value, max: Value): Pair<Long, Long> {
205-
// Safely convert both inputs to BigInteger
206-
val minBigInt = safeValueToBigInt(min, "min")
207-
val maxBigInt = safeValueToBigInt(max, "max")
208-
209-
// Enforce the Min <= Max rule otherwise we throw a RangeError
210-
if (minBigInt >= maxBigInt) {
211-
throw RangeError.create("The value of \"max\" is out of range. It must be greater than the value of \"min\" (${minBigInt}). Received ${maxBigInt}.")
212-
}
213-
214-
// Enforce the difference between (max - min) exceeds 2^48
215-
val rangeDifference = maxBigInt.subtract(minBigInt) // Since min < max, difference is non-negative
216-
217-
// If the range difference exceeds 2^48, we throw a RangeError. Node.JS has a range limit of 2^48 for randomInt.
218-
if (rangeDifference > MAX_48_BIT_LIMIT) {
219-
println("Range difference exceeds 2^48 limit: $rangeDifference")
220-
throw RangeError.create("The value of \"max - min\" is out of range. It must be <= 281474976710655. Received ${rangeDifference}.")
221-
}
222-
223-
// Return the validated safe Long values
224-
return Pair(minBigInt.toLong(), maxBigInt.toLong())
225-
}
226237
}

0 commit comments

Comments
 (0)