Skip to main content

bento_kit/id/
random.rs

1//! Random number / string helpers.
2//!
3//! Direct ports of `randomInt` and `randomString` from
4//! [`du-node-utils/lib/id.js`](https://github.com/imcooder/du-node-utils/blob/main/lib/id.js).
5//!
6//! These are **not** cryptographically secure (matching the Node behavior,
7//! which uses `Math.random()`). For tokens and secrets, use a CSPRNG via
8//! the [`rand`](https://docs.rs/rand) crate's `OsRng` directly.
9
10use rand::Rng;
11
12/// Alphabet used by [`random_string`], identical to the Node version's
13/// keyboard-order 36-char set.
14pub(crate) const RANDOM_STRING_ALPHABET: &[u8] = b"0123456789qwertyuioplkjhgfdsazxcvbnm";
15
16/// Base36 alphabet used by the internal random-string helper, mirroring
17/// the output of Node's `Math.random().toString(36)`.
18pub(crate) const BASE36_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
19
20/// Generate a random integer in the half-open range `[min, max)`.
21///
22/// Mirrors `id.randomInt(min, max)` from the Node library.
23///
24/// # Panics
25///
26/// Panics if `min >= max` (consistent with `rand::Rng::gen_range`).
27///
28/// ```
29/// let n = bento_kit::id::random_int(0, 10);
30/// assert!((0..10).contains(&n));
31/// ```
32pub fn random_int(min: i64, max: i64) -> i64 {
33    rand::thread_rng().gen_range(min..max)
34}
35
36/// Generate a random string of length `len` using the same 36-char
37/// alphabet as Node's `id.randomString(len)`:
38/// `0-9` plus `qwertyuioplkjhgfdsazxcvbnm` (keyboard order).
39///
40/// ```
41/// let s = bento_kit::id::random_string(20);
42/// assert_eq!(s.len(), 20);
43/// ```
44pub fn random_string(len: usize) -> String {
45    pick_chars(RANDOM_STRING_ALPHABET, len)
46}
47
48/// Internal helper matching Node's private `generateRandomString(length)`,
49/// which produces a base36 string. Used to build session prefixes and
50/// session-ID random suffixes.
51pub(crate) fn generate_random_string(len: usize) -> String {
52    pick_chars(BASE36_ALPHABET, len)
53}
54
55fn pick_chars(alphabet: &[u8], len: usize) -> String {
56    let mut rng = rand::thread_rng();
57    (0..len)
58        .map(|_| {
59            let idx = rng.gen_range(0..alphabet.len());
60            alphabet[idx] as char
61        })
62        .collect()
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn random_int_is_in_range() {
71        for _ in 0..1000 {
72            let n = random_int(5, 10);
73            assert!((5..10).contains(&n), "{n} out of range");
74        }
75    }
76
77    #[test]
78    fn random_int_negative_range() {
79        for _ in 0..200 {
80            let n = random_int(-10, -1);
81            assert!((-10..-1).contains(&n));
82        }
83    }
84
85    #[test]
86    fn random_string_has_expected_length() {
87        for len in [0_usize, 1, 8, 32, 256] {
88            assert_eq!(random_string(len).len(), len);
89        }
90    }
91
92    #[test]
93    fn random_string_uses_node_alphabet() {
94        let s = random_string(500);
95        for c in s.chars() {
96            assert!(
97                RANDOM_STRING_ALPHABET.contains(&(c as u8)),
98                "char {c:?} not in alphabet"
99            );
100        }
101    }
102
103    #[test]
104    fn generate_random_string_is_base36() {
105        let s = generate_random_string(500);
106        for c in s.chars() {
107            assert!(
108                c.is_ascii_digit() || c.is_ascii_lowercase(),
109                "char {c:?} is not base36"
110            );
111        }
112    }
113}