Skip to content

Commit

Permalink
crypto(randomInt): make max arg inclusive
Browse files Browse the repository at this point in the history
  • Loading branch information
olalonde committed Aug 6, 2020
1 parent c7e3d0b commit c068dc4
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 27 deletions.
17 changes: 9 additions & 8 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2803,18 +2803,19 @@ request.
### `crypto.randomInt([min, ]max[, callback])`
<!-- YAML
added:
- v14.8.0
- CHANGEME
-->

* `min` {integer} Start of random range. **Default**: `0`.
* `max` {integer} End of random range (non-inclusive).
* `min` {integer} Start of random range (inclusive). **Default**: `0`.
* `max` {integer} End of random range (inclusive).
* `callback` {Function} `function(err, n) {}`.

Return a random integer `n` such that `min <= n < max`. This
Return a random integer `n` such that `min <= n <= max`. This
implementation avoids [modulo
bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias).

The maximum supported range value (`max - min`) is `2^48 - 1`.
The cardinality of the range (`max - min + 1`) must be at most `2^48 -
1`. `min` and `max` must be safe integers.

If the `callback` function is not provided, the random integer is generated
synchronously.
Expand All @@ -2823,18 +2824,18 @@ synchronously.
// Asynchronous
crypto.randomInt(3, (err, n) => {
if (err) throw err;
console.log(`Random number chosen from (0, 1, 2): ${n}`);
console.log(`Random number chosen from (0, 1, 2, 3): ${n}`);
});
```

```js
// Synchronous
const n = crypto.randomInt(3);
console.log(`Random number chosen from (0, 1, 2): ${n}`);
console.log(`Random number chosen from (0, 1, 2, 3): ${n}`);
```

```js
crypto.randomInt(1, 7, (err, n) => {
crypto.randomInt(1, 6, (err, n) => {
if (err) throw err;
console.log(`The dice rolled: ${n}`);
});
Expand Down
16 changes: 9 additions & 7 deletions lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const {
MathMin,
NumberIsNaN,
NumberIsInteger
NumberIsSafeInteger
} = primordials;

const { AsyncWrap, Providers } = internalBinding('async_wrap');
Expand Down Expand Up @@ -124,6 +124,8 @@ function randomFill(buf, offset, size, cb) {
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
const RAND_MAX = 281474976710655;

// Generates an integer in [min, max] range where both min and max are
// inclusive.
function randomInt(min, max, cb) {
// randomInt(max, cb)
// randomInt(max)
Expand All @@ -136,18 +138,18 @@ function randomInt(min, max, cb) {
if (!isSync && typeof cb !== 'function') {
throw new ERR_INVALID_CALLBACK(cb);
}
if (!NumberIsInteger(min)) {
throw new ERR_INVALID_ARG_TYPE('min', 'integer', min);
if (!NumberIsSafeInteger(min)) {
throw new ERR_INVALID_ARG_TYPE('min', 'safe integer', min);
}
if (!NumberIsInteger(max)) {
throw new ERR_INVALID_ARG_TYPE('max', 'integer', max);
if (!NumberIsSafeInteger(max)) {
throw new ERR_INVALID_ARG_TYPE('max', 'safe integer', max);
}
if (!(max > min)) {
throw new ERR_OUT_OF_RANGE('max', `> ${min}`, max);
}

// First we generate a random int between [0..range)
const range = max - min;
// First we generate a random int between [0..range]
const range = max - min + 1;

if (!(range <= RAND_MAX)) {
throw new ERR_OUT_OF_RANGE('max - min', `<= ${RAND_MAX}`, range);
Expand Down
51 changes: 39 additions & 12 deletions test/parallel/test-crypto-random.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,10 @@ assert.throws(
{
const randomInts = [];
for (let i = 0; i < 100; i++) {
crypto.randomInt(1, 4, common.mustCall((err, n) => {
crypto.randomInt(1, 3, common.mustCall((err, n) => {
assert.ifError(err);
assert.ok(n >= 1);
assert.ok(n < 4);
assert.ok(n <= 3);
randomInts.push(n);
if (randomInts.length === 100) {
assert.ok(!randomInts.includes(0));
Expand All @@ -338,10 +338,10 @@ assert.throws(
{
const randomInts = [];
for (let i = 0; i < 100; i++) {
crypto.randomInt(-10, -7, common.mustCall((err, n) => {
crypto.randomInt(-10, -8, common.mustCall((err, n) => {
assert.ifError(err);
assert.ok(n >= -10);
assert.ok(n < -7);
assert.ok(n <= -8);
randomInts.push(n);
if (randomInts.length === 100) {
assert.ok(!randomInts.includes(-11));
Expand All @@ -359,12 +359,15 @@ assert.throws(
crypto.randomInt(3, common.mustCall((err, n) => {
assert.ifError(err);
assert.ok(n >= 0);
assert.ok(n < 3);
assert.ok(n <= 3);
randomInts.push(n);
if (randomInts.length === 100) {
assert.ok(!randomInts.includes(-1));
assert.ok(randomInts.includes(0));
assert.ok(randomInts.includes(1));
assert.ok(randomInts.includes(2));
assert.ok(randomInts.includes(3));
assert.ok(!randomInts.includes(4));
}
}));
}
Expand All @@ -375,21 +378,24 @@ assert.throws(
for (let i = 0; i < 100; i++) {
const n = crypto.randomInt(3);
assert.ok(n >= 0);
assert.ok(n < 3);
assert.ok(n <= 3);
randomInts.push(n);
}

assert.ok(!randomInts.includes(-1));
assert.ok(randomInts.includes(0));
assert.ok(randomInts.includes(1));
assert.ok(randomInts.includes(2));
assert.ok(randomInts.includes(3));
assert.ok(!randomInts.includes(4));
}
{
// Synchronous API with min
const randomInts = [];
for (let i = 0; i < 100; i++) {
const n = crypto.randomInt(3, 6);
const n = crypto.randomInt(3, 5);
assert.ok(n >= 3);
assert.ok(n < 6);
assert.ok(n <= 5);
randomInts.push(n);
}

Expand All @@ -403,25 +409,46 @@ assert.throws(
assert.throws(() => crypto.randomInt(i, 100, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "min" argument must be integer.' +
message: 'The "min" argument must be safe integer.' +
`${common.invalidArgTypeHelper(i)}`,
});

assert.throws(() => crypto.randomInt(0, i, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "max" argument must be integer.' +
message: 'The "max" argument must be safe integer.' +
`${common.invalidArgTypeHelper(i)}`,
});

assert.throws(() => crypto.randomInt(i, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "max" argument must be integer.' +
message: 'The "max" argument must be safe integer.' +
`${common.invalidArgTypeHelper(i)}`,
});
});

const minInt = Number.MIN_SAFE_INTEGER;
const maxInt = Number.MAX_SAFE_INTEGER;

crypto.randomInt(minInt, minInt + 5, common.mustCall());
crypto.randomInt(maxInt - 5, maxInt, common.mustCall());

assert.throws(() => crypto.randomInt(minInt - 1, minInt + 5, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "min" argument must be safe integer.' +
`${common.invalidArgTypeHelper(minInt - 1)}`,
});

assert.throws(() => crypto.randomInt(maxInt - 5, maxInt + 1, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "max" argument must be safe integer.' +
`${common.invalidArgTypeHelper(maxInt + 1)}`,
});


assert.throws(() => crypto.randomInt(0, 0, common.mustNotCall()), {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
Expand All @@ -445,7 +472,7 @@ assert.throws(
name: 'RangeError',
message: 'The value of "max - min" is out of range. ' +
`It must be <= ${(2 ** 48) - 1}. ` +
'Received 281_474_976_710_656'
'Received 281_474_976_710_657'
});

[1, true, NaN, null, {}, []].forEach((i) => {
Expand Down

0 comments on commit c068dc4

Please sign in to comment.