Skip to main content

mod_rand/
tier2.rs

1//! # Tier 2 — Process-unique seeds
2//!
3//! Fast pseudo-random values derived from process ID, high-resolution
4//! time, and a monotonic atomic counter, then run through several
5//! rounds of a strong scalar mixer. Good for:
6//!
7//! - Tempdir / temp-file names
8//! - Request and trace IDs
9//! - Log correlation IDs
10//! - Any application-level "good-enough" uniqueness
11//!
12//! **Not cryptographic.** An attacker who can observe outputs can
13//! often reconstruct internal state and predict subsequent values. Use
14//! [`tier3`](crate::tier3) for security-sensitive randomness.
15//!
16//! ## Uniqueness guarantees
17//!
18//! - Within a single process: every [`unique_u64`] call returns a
19//!   distinct value (guaranteed by a never-resetting atomic counter,
20//!   modulo wrap at `2^64` calls — about 584 years at 1 GHz).
21//! - Across processes on the same host: PID + nanosecond timestamp
22//!   make collisions vanishingly unlikely.
23//! - Across hosts: this tier makes no claims. Use [`tier3`](crate::tier3)
24//!   when you need values unique across machines.
25//!
26//! ## Thread safety
27//!
28//! All functions in this module are thread-safe and lock-free. The
29//! shared atomic counter ensures uniqueness even under concurrent
30//! access from any number of threads.
31//!
32//! ## Performance
33//!
34//! Target <100ns per call. Cost is dominated by a `SystemTime::now()`
35//! reading; the mixing itself is a handful of multiplies and rotates.
36
37use core::ops::{Range, RangeInclusive};
38use std::process;
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::time::{SystemTime, UNIX_EPOCH};
41
42/// Monotonic per-process counter. Initialised at first use; never
43/// resets. Wraps after 2^64 calls (584 years at one call per
44/// nanosecond — i.e., effectively never).
45static COUNTER: AtomicU64 = AtomicU64::new(0);
46
47/// Process-wide salt mixed into every output. Captured once on first
48/// use so that two processes started in the same nanosecond with the
49/// same PID (e.g., container restart) still diverge from their first
50/// call onward. Zero is a sentinel for "not yet initialised."
51static PROCESS_SALT: AtomicU64 = AtomicU64::new(0);
52
53/// Produce a process-unique `u64`.
54///
55/// Combines PID, the current `SystemTime` in nanoseconds, an atomic
56/// counter, and a per-process salt; passes the result through a
57/// strong scalar mixer (variant of MurmurHash3's finalizer / Stafford
58/// mix 13). Output is uniform-looking but not unpredictable to an
59/// adversary who has seen prior outputs.
60///
61/// # Example
62///
63/// ```
64/// use mod_rand::tier2;
65///
66/// let a = tier2::unique_u64();
67/// let b = tier2::unique_u64();
68/// assert_ne!(a, b);
69/// ```
70pub fn unique_u64() -> u64 {
71    let pid = process::id() as u64;
72    let nanos = SystemTime::now()
73        .duration_since(UNIX_EPOCH)
74        .map(|d| d.as_nanos() as u64)
75        .unwrap_or(0);
76    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
77    let salt = process_salt();
78
79    // Combine inputs via odd multipliers (each a large prime / good
80    // mixing constant), then finalize with stafford_mix13. The
81    // counter is the only field guaranteed to differ between same-
82    // process calls, so it gets the highest-quality multiplier and is
83    // XORed in last so it cannot be cancelled by the others.
84    let mixed = pid
85        .wrapping_mul(0x9E37_79B9_7F4A_7C15)
86        .wrapping_add(nanos.wrapping_mul(0xBF58_476D_1CE4_E5B9))
87        .wrapping_add(salt.wrapping_mul(0x94D0_49BB_1331_11EB))
88        ^ counter.wrapping_mul(0xDA94_2042_E4DD_58B5);
89
90    stafford_mix13(mixed)
91}
92
93/// Produce a process-unique base32 (Crockford alphabet) name of
94/// exactly `len` characters.
95///
96/// Suitable for tempdir and temp-file names, correlation IDs, and the
97/// like. The Crockford alphabet (`0-9A-Z` with `I`, `L`, `O`, `U`
98/// removed) is filesystem-safe on every platform and avoids visually
99/// ambiguous characters.
100///
101/// # Example
102///
103/// ```
104/// use mod_rand::tier2;
105///
106/// let name = tier2::unique_name(12);
107/// assert_eq!(name.len(), 12);
108/// assert!(name.chars().all(|c| c.is_ascii_alphanumeric()));
109/// ```
110pub fn unique_name(len: usize) -> String {
111    encode_base32(len)
112}
113
114/// Produce a process-unique base32 (Crockford alphabet) string of
115/// exactly `len` characters. Equivalent to [`unique_name`]; provided
116/// for symmetry with [`unique_hex`].
117///
118/// # Example
119///
120/// ```
121/// use mod_rand::tier2;
122///
123/// let s = tier2::unique_base32(16);
124/// assert_eq!(s.len(), 16);
125/// ```
126pub fn unique_base32(len: usize) -> String {
127    encode_base32(len)
128}
129
130/// Produce a process-unique lowercase hex string of exactly `len`
131/// characters.
132///
133/// # Example
134///
135/// ```
136/// use mod_rand::tier2;
137///
138/// let s = tier2::unique_hex(8);
139/// assert_eq!(s.len(), 8);
140/// assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
141/// ```
142pub fn unique_hex(len: usize) -> String {
143    const ALPHABET: &[u8; 16] = b"0123456789abcdef";
144    let mut out = String::with_capacity(len);
145    let mut state = unique_u64();
146    let mut bits_left = 64;
147    while out.len() < len {
148        if bits_left < 4 {
149            state = unique_u64();
150            bits_left = 64;
151        }
152        out.push(ALPHABET[(state & 0xF) as usize] as char);
153        state >>= 4;
154        bits_left -= 4;
155    }
156    out
157}
158
159fn encode_base32(len: usize) -> String {
160    // Crockford base32 alphabet (no I, L, O, U).
161    const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
162    let mut out = String::with_capacity(len);
163    let mut state = unique_u64();
164    let mut bits_left = 64;
165    while out.len() < len {
166        if bits_left < 5 {
167            // Re-seed with a fresh tier2 value rather than recycling
168            // the residual bits — keeps each character independent.
169            state = unique_u64();
170            bits_left = 64;
171        }
172        out.push(ALPHABET[(state & 0x1F) as usize] as char);
173        state >>= 5;
174        bits_left -= 5;
175    }
176    out
177}
178
179/// Stafford's variant 13 — a strong 64-bit avalanche mixer. Same
180/// family as splitmix64's finalizer but with constants tuned for
181/// stronger statistical properties on small input deltas (which is
182/// exactly our case — successive counter values differ only in the
183/// low bits).
184#[inline]
185fn stafford_mix13(mut z: u64) -> u64 {
186    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
187    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
188    z ^ (z >> 31)
189}
190
191/// Lazily initialise (once per process) and return the process salt.
192fn process_salt() -> u64 {
193    let current = PROCESS_SALT.load(Ordering::Relaxed);
194    if current != 0 {
195        return current;
196    }
197    let pid = process::id() as u64;
198    let nanos = SystemTime::now()
199        .duration_since(UNIX_EPOCH)
200        .map(|d| d.as_nanos() as u64)
201        .unwrap_or(0);
202    let candidate = stafford_mix13(
203        pid.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ nanos.wrapping_mul(0xC2B2_AE3D_27D4_EB4F),
204    )
205    .max(1);
206    // The first writer wins. Subsequent writers see the established
207    // salt and adopt it. This is racy only in the harmless sense that
208    // a brief moment may exist where two threads compute slightly
209    // different candidates; whichever lands first is the salt.
210    match PROCESS_SALT.compare_exchange(0, candidate, Ordering::Relaxed, Ordering::Relaxed) {
211        Ok(_) => candidate,
212        Err(existing) => existing,
213    }
214}
215
216// ------------------------------------------------------------
217// Bounded-range API
218//
219// Each bounded function below pulls fresh `unique_u64` values and
220// reduces them with Lemire's "Nearly Divisionless" rejection sampling.
221// The result is uniformly distributed over the requested range — no
222// modulo bias.
223//
224// Note on guarantees: Tier 2 raw `unique_u64` output is guaranteed
225// distinct across calls within a single process. The bounded-range
226// reductions DO NOT preserve that guarantee — multiple distinct u64
227// values necessarily map to the same bounded value when the range is
228// smaller than 2^64. Callers who need distinct bounded values must
229// either use the raw `unique_u64` stream themselves or de-duplicate.
230// ------------------------------------------------------------
231
232/// Produce a uniformly-distributed `u64` in `[0, n)`.
233///
234/// Internal helper for the bounded-range API. Implements Daniel
235/// Lemire's "Nearly Divisionless" rejection sampling. `n` MUST be
236/// greater than zero.
237#[inline]
238fn bounded_u64(n: u64) -> u64 {
239    debug_assert!(n != 0, "bounded_u64 requires n > 0");
240    let mut x = unique_u64();
241    let mut m: u128 = (x as u128).wrapping_mul(n as u128);
242    let mut l: u64 = m as u64;
243    if l < n {
244        let t: u64 = n.wrapping_neg() % n;
245        while l < t {
246            x = unique_u64();
247            m = (x as u128).wrapping_mul(n as u128);
248            l = m as u64;
249        }
250    }
251    (m >> 64) as u64
252}
253
254/// Generate a uniformly-distributed `u64` in the half-open range
255/// `[range.start, range.end)`.
256///
257/// # Panics
258///
259/// Panics if the range is empty (`range.start >= range.end`).
260///
261/// # Example
262///
263/// ```
264/// use mod_rand::tier2;
265///
266/// let n = tier2::range_u64(10..20);
267/// assert!((10..20).contains(&n));
268/// ```
269pub fn range_u64(range: Range<u64>) -> u64 {
270    let Range { start, end } = range;
271    assert!(start < end, "range_u64: empty range {start}..{end}");
272    let span = end - start;
273    start + bounded_u64(span)
274}
275
276/// Generate a uniformly-distributed `u64` in the closed range
277/// `[range.start(), range.end()]`.
278///
279/// The full-width inclusive range `0..=u64::MAX` is supported and
280/// is equivalent to a single `unique_u64()` draw.
281///
282/// # Panics
283///
284/// Panics if the range is empty (`*range.start() > *range.end()`).
285///
286/// # Example
287///
288/// ```
289/// use mod_rand::tier2;
290///
291/// let d = tier2::range_inclusive_u64(1..=6);
292/// assert!((1..=6).contains(&d));
293/// ```
294pub fn range_inclusive_u64(range: RangeInclusive<u64>) -> u64 {
295    let (start, end) = range.into_inner();
296    assert!(
297        start <= end,
298        "range_inclusive_u64: empty range {start}..={end}"
299    );
300    if start == 0 && end == u64::MAX {
301        return unique_u64();
302    }
303    let span = end - start + 1;
304    start + bounded_u64(span)
305}
306
307/// Generate a uniformly-distributed `u32` in the half-open range
308/// `[range.start, range.end)`.
309///
310/// # Panics
311///
312/// Panics if the range is empty.
313///
314/// # Example
315///
316/// ```
317/// use mod_rand::tier2;
318///
319/// let pct = tier2::range_u32(0..100);
320/// assert!(pct < 100);
321/// ```
322pub fn range_u32(range: Range<u32>) -> u32 {
323    let Range { start, end } = range;
324    assert!(start < end, "range_u32: empty range {start}..{end}");
325    let span = (end - start) as u64;
326    (start as u64 + bounded_u64(span)) as u32
327}
328
329/// Generate a uniformly-distributed `u32` in the closed range
330/// `[range.start(), range.end()]`.
331///
332/// The full-width inclusive range `0..=u32::MAX` is supported.
333///
334/// # Panics
335///
336/// Panics if the range is empty.
337pub fn range_inclusive_u32(range: RangeInclusive<u32>) -> u32 {
338    let (start, end) = range.into_inner();
339    assert!(
340        start <= end,
341        "range_inclusive_u32: empty range {start}..={end}"
342    );
343    let span = (end as u64) - (start as u64) + 1;
344    (start as u64 + bounded_u64(span)) as u32
345}
346
347/// Generate a uniformly-distributed `i64` in the half-open range
348/// `[range.start, range.end)`.
349///
350/// Negative bounds and mixed-sign ranges are supported.
351///
352/// # Panics
353///
354/// Panics if the range is empty.
355///
356/// # Example
357///
358/// ```
359/// use mod_rand::tier2;
360///
361/// let n = tier2::range_i64(-50..50);
362/// assert!((-50..50).contains(&n));
363/// ```
364pub fn range_i64(range: Range<i64>) -> i64 {
365    let Range { start, end } = range;
366    assert!(start < end, "range_i64: empty range {start}..{end}");
367    let span = (end as i128 - start as i128) as u64;
368    let offset = bounded_u64(span);
369    ((start as i128) + (offset as i128)) as i64
370}
371
372/// Generate a uniformly-distributed `i64` in the closed range
373/// `[range.start(), range.end()]`.
374///
375/// The full-width inclusive range `i64::MIN..=i64::MAX` is supported
376/// and is equivalent to reinterpreting a raw `unique_u64()` draw as
377/// `i64`.
378///
379/// # Panics
380///
381/// Panics if the range is empty.
382pub fn range_inclusive_i64(range: RangeInclusive<i64>) -> i64 {
383    let (start, end) = range.into_inner();
384    assert!(
385        start <= end,
386        "range_inclusive_i64: empty range {start}..={end}"
387    );
388    if start == i64::MIN && end == i64::MAX {
389        return unique_u64() as i64;
390    }
391    let span = ((end as i128) - (start as i128) + 1) as u64;
392    let offset = bounded_u64(span);
393    ((start as i128) + (offset as i128)) as i64
394}
395
396/// Generate a uniformly-distributed `i32` in the half-open range
397/// `[range.start, range.end)`.
398///
399/// # Panics
400///
401/// Panics if the range is empty.
402pub fn range_i32(range: Range<i32>) -> i32 {
403    let Range { start, end } = range;
404    assert!(start < end, "range_i32: empty range {start}..{end}");
405    let span = (end as i64 - start as i64) as u64;
406    let offset = bounded_u64(span);
407    ((start as i64) + (offset as i64)) as i32
408}
409
410/// Generate a uniformly-distributed `i32` in the closed range
411/// `[range.start(), range.end()]`.
412///
413/// The full-width inclusive range `i32::MIN..=i32::MAX` is supported.
414///
415/// # Panics
416///
417/// Panics if the range is empty.
418pub fn range_inclusive_i32(range: RangeInclusive<i32>) -> i32 {
419    let (start, end) = range.into_inner();
420    assert!(
421        start <= end,
422        "range_inclusive_i32: empty range {start}..={end}"
423    );
424    let span = ((end as i64) - (start as i64) + 1) as u64;
425    let offset = bounded_u64(span);
426    ((start as i64) + (offset as i64)) as i32
427}
428
429// ------------------------------------------------------------
430// Bounded-range API — additional integer widths
431// ------------------------------------------------------------
432
433/// Produce a uniformly-distributed `u128` in `[0, n)`.
434///
435/// Internal helper for the 128-bit bounded-range functions. Generalises
436/// Lemire's "Nearly Divisionless" rejection sampling to 128-bit width
437/// via a 256-bit intermediate product. Two `unique_u64` draws per call
438/// in the common case. `n` MUST be greater than zero.
439#[inline]
440fn bounded_u128(n: u128) -> u128 {
441    debug_assert!(n != 0, "bounded_u128 requires n > 0");
442    let mut x = draw_u128();
443    let (mut hi, mut lo) = mul_128_full(x, n);
444    if lo < n {
445        let t: u128 = n.wrapping_neg() % n;
446        while lo < t {
447            x = draw_u128();
448            let (h, l) = mul_128_full(x, n);
449            hi = h;
450            lo = l;
451        }
452    }
453    let _ = x;
454    hi
455}
456
457#[inline]
458fn draw_u128() -> u128 {
459    ((unique_u64() as u128) << 64) | (unique_u64() as u128)
460}
461
462#[inline]
463fn mul_128_full(a: u128, b: u128) -> (u128, u128) {
464    let a_lo: u64 = a as u64;
465    let a_hi: u64 = (a >> 64) as u64;
466    let b_lo: u64 = b as u64;
467    let b_hi: u64 = (b >> 64) as u64;
468    let p00: u128 = (a_lo as u128) * (b_lo as u128);
469    let p01: u128 = (a_lo as u128) * (b_hi as u128);
470    let p10: u128 = (a_hi as u128) * (b_lo as u128);
471    let p11: u128 = (a_hi as u128) * (b_hi as u128);
472    let mask_lo: u128 = 0xFFFF_FFFF_FFFF_FFFF;
473    let mid: u128 = (p00 >> 64) + (p01 & mask_lo) + (p10 & mask_lo);
474    let low: u128 = (p00 & mask_lo) | (mid << 64);
475    let high: u128 = p11 + (p01 >> 64) + (p10 >> 64) + (mid >> 64);
476    (high, low)
477}
478
479/// Generate a uniformly-distributed `u8` in the half-open range
480/// `[range.start, range.end)`.
481///
482/// # Panics
483///
484/// Panics if the range is empty.
485///
486/// # Example
487///
488/// ```
489/// use mod_rand::tier2;
490/// let n = tier2::range_u8(0..100);
491/// assert!(n < 100);
492/// ```
493pub fn range_u8(range: Range<u8>) -> u8 {
494    let Range { start, end } = range;
495    assert!(start < end, "range_u8: empty range {start}..{end}");
496    let span = (end - start) as u64;
497    (start as u64 + bounded_u64(span)) as u8
498}
499
500/// Generate a uniformly-distributed `u8` in the closed range
501/// `[range.start(), range.end()]`. The full-width `0..=u8::MAX` is
502/// supported.
503///
504/// # Panics
505///
506/// Panics if the range is empty.
507pub fn range_inclusive_u8(range: RangeInclusive<u8>) -> u8 {
508    let (start, end) = range.into_inner();
509    assert!(
510        start <= end,
511        "range_inclusive_u8: empty range {start}..={end}"
512    );
513    let span = (end as u64) - (start as u64) + 1;
514    (start as u64 + bounded_u64(span)) as u8
515}
516
517/// Generate a uniformly-distributed `u16` in the half-open range
518/// `[range.start, range.end)`.
519///
520/// # Panics
521///
522/// Panics if the range is empty.
523///
524/// # Example
525///
526/// ```
527/// use mod_rand::tier2;
528/// let n = tier2::range_u16(0..1000);
529/// assert!(n < 1000);
530/// ```
531pub fn range_u16(range: Range<u16>) -> u16 {
532    let Range { start, end } = range;
533    assert!(start < end, "range_u16: empty range {start}..{end}");
534    let span = (end - start) as u64;
535    (start as u64 + bounded_u64(span)) as u16
536}
537
538/// Generate a uniformly-distributed `u16` in the closed range
539/// `[range.start(), range.end()]`. The full-width `0..=u16::MAX` is
540/// supported.
541///
542/// # Panics
543///
544/// Panics if the range is empty.
545pub fn range_inclusive_u16(range: RangeInclusive<u16>) -> u16 {
546    let (start, end) = range.into_inner();
547    assert!(
548        start <= end,
549        "range_inclusive_u16: empty range {start}..={end}"
550    );
551    let span = (end as u64) - (start as u64) + 1;
552    (start as u64 + bounded_u64(span)) as u16
553}
554
555/// Generate a uniformly-distributed `u128` in the half-open range
556/// `[range.start, range.end)`.
557///
558/// # Panics
559///
560/// Panics if the range is empty.
561///
562/// # Example
563///
564/// ```
565/// use mod_rand::tier2;
566/// let n = tier2::range_u128(0..(u128::MAX / 2));
567/// assert!(n < u128::MAX / 2);
568/// ```
569pub fn range_u128(range: Range<u128>) -> u128 {
570    let Range { start, end } = range;
571    assert!(start < end, "range_u128: empty range {start}..{end}");
572    let span = end - start;
573    start + bounded_u128(span)
574}
575
576/// Generate a uniformly-distributed `u128` in the closed range
577/// `[range.start(), range.end()]`. The full-width `0..=u128::MAX` is
578/// supported.
579///
580/// # Panics
581///
582/// Panics if the range is empty.
583pub fn range_inclusive_u128(range: RangeInclusive<u128>) -> u128 {
584    let (start, end) = range.into_inner();
585    assert!(
586        start <= end,
587        "range_inclusive_u128: empty range {start}..={end}"
588    );
589    if start == 0 && end == u128::MAX {
590        return draw_u128();
591    }
592    let span = end - start + 1;
593    start + bounded_u128(span)
594}
595
596/// Generate a uniformly-distributed `usize` in the half-open range
597/// `[range.start, range.end)`.
598///
599/// # Panics
600///
601/// Panics if the range is empty.
602pub fn range_usize(range: Range<usize>) -> usize {
603    let Range { start, end } = range;
604    assert!(start < end, "range_usize: empty range {start}..{end}");
605    let span = (end - start) as u64;
606    (start as u64 + bounded_u64(span)) as usize
607}
608
609/// Generate a uniformly-distributed `usize` in the closed range
610/// `[range.start(), range.end()]`. The full-width `0..=usize::MAX` is
611/// supported.
612///
613/// # Panics
614///
615/// Panics if the range is empty.
616pub fn range_inclusive_usize(range: RangeInclusive<usize>) -> usize {
617    let (start, end) = range.into_inner();
618    assert!(
619        start <= end,
620        "range_inclusive_usize: empty range {start}..={end}"
621    );
622    if start == 0 && end == usize::MAX {
623        #[cfg(target_pointer_width = "64")]
624        {
625            return unique_u64() as usize;
626        }
627        #[cfg(not(target_pointer_width = "64"))]
628        {
629            let span = (end as u64) - (start as u64) + 1;
630            return (start as u64 + bounded_u64(span)) as usize;
631        }
632    }
633    let span = (end - start) as u64 + 1;
634    (start as u64 + bounded_u64(span)) as usize
635}
636
637/// Generate a uniformly-distributed `i8` in the half-open range
638/// `[range.start, range.end)`.
639///
640/// # Panics
641///
642/// Panics if the range is empty.
643pub fn range_i8(range: Range<i8>) -> i8 {
644    let Range { start, end } = range;
645    assert!(start < end, "range_i8: empty range {start}..{end}");
646    let span = (end as i64 - start as i64) as u64;
647    let offset = bounded_u64(span);
648    ((start as i64) + (offset as i64)) as i8
649}
650
651/// Generate a uniformly-distributed `i8` in the closed range
652/// `[range.start(), range.end()]`. The full-width `i8::MIN..=i8::MAX`
653/// is supported.
654///
655/// # Panics
656///
657/// Panics if the range is empty.
658pub fn range_inclusive_i8(range: RangeInclusive<i8>) -> i8 {
659    let (start, end) = range.into_inner();
660    assert!(
661        start <= end,
662        "range_inclusive_i8: empty range {start}..={end}"
663    );
664    let span = ((end as i64) - (start as i64) + 1) as u64;
665    let offset = bounded_u64(span);
666    ((start as i64) + (offset as i64)) as i8
667}
668
669/// Generate a uniformly-distributed `i16` in the half-open range
670/// `[range.start, range.end)`.
671///
672/// # Panics
673///
674/// Panics if the range is empty.
675pub fn range_i16(range: Range<i16>) -> i16 {
676    let Range { start, end } = range;
677    assert!(start < end, "range_i16: empty range {start}..{end}");
678    let span = (end as i64 - start as i64) as u64;
679    let offset = bounded_u64(span);
680    ((start as i64) + (offset as i64)) as i16
681}
682
683/// Generate a uniformly-distributed `i16` in the closed range
684/// `[range.start(), range.end()]`. The full-width `i16::MIN..=i16::MAX`
685/// is supported.
686///
687/// # Panics
688///
689/// Panics if the range is empty.
690pub fn range_inclusive_i16(range: RangeInclusive<i16>) -> i16 {
691    let (start, end) = range.into_inner();
692    assert!(
693        start <= end,
694        "range_inclusive_i16: empty range {start}..={end}"
695    );
696    let span = ((end as i64) - (start as i64) + 1) as u64;
697    let offset = bounded_u64(span);
698    ((start as i64) + (offset as i64)) as i16
699}
700
701/// Generate a uniformly-distributed `i128` in the half-open range
702/// `[range.start, range.end)`.
703///
704/// # Panics
705///
706/// Panics if the range is empty.
707pub fn range_i128(range: Range<i128>) -> i128 {
708    let Range { start, end } = range;
709    assert!(start < end, "range_i128: empty range {start}..{end}");
710    let span = (end as u128).wrapping_sub(start as u128);
711    let offset = bounded_u128(span);
712    (start as u128).wrapping_add(offset) as i128
713}
714
715/// Generate a uniformly-distributed `i128` in the closed range
716/// `[range.start(), range.end()]`. The full-width
717/// `i128::MIN..=i128::MAX` is supported.
718///
719/// # Panics
720///
721/// Panics if the range is empty.
722pub fn range_inclusive_i128(range: RangeInclusive<i128>) -> i128 {
723    let (start, end) = range.into_inner();
724    assert!(
725        start <= end,
726        "range_inclusive_i128: empty range {start}..={end}"
727    );
728    if start == i128::MIN && end == i128::MAX {
729        return draw_u128() as i128;
730    }
731    let span = (end as u128).wrapping_sub(start as u128) + 1;
732    let offset = bounded_u128(span);
733    (start as u128).wrapping_add(offset) as i128
734}
735
736/// Generate a uniformly-distributed `isize` in the half-open range
737/// `[range.start, range.end)`.
738///
739/// # Panics
740///
741/// Panics if the range is empty.
742pub fn range_isize(range: Range<isize>) -> isize {
743    let Range { start, end } = range;
744    assert!(start < end, "range_isize: empty range {start}..{end}");
745    let span = (end as i128 - start as i128) as u64;
746    let offset = bounded_u64(span);
747    ((start as i128) + (offset as i128)) as isize
748}
749
750/// Generate a uniformly-distributed `isize` in the closed range
751/// `[range.start(), range.end()]`. The full-width
752/// `isize::MIN..=isize::MAX` is supported.
753///
754/// # Panics
755///
756/// Panics if the range is empty.
757pub fn range_inclusive_isize(range: RangeInclusive<isize>) -> isize {
758    let (start, end) = range.into_inner();
759    assert!(
760        start <= end,
761        "range_inclusive_isize: empty range {start}..={end}"
762    );
763    if start == isize::MIN && end == isize::MAX {
764        #[cfg(target_pointer_width = "64")]
765        {
766            return unique_u64() as isize;
767        }
768        #[cfg(not(target_pointer_width = "64"))]
769        {
770            let span = ((end as i128) - (start as i128) + 1) as u64;
771            let offset = bounded_u64(span);
772            return ((start as i128) + (offset as i128)) as isize;
773        }
774    }
775    let span = ((end as i128) - (start as i128) + 1) as u64;
776    let offset = bounded_u64(span);
777    ((start as i128) + (offset as i128)) as isize
778}
779
780// ------------------------------------------------------------
781// String generation
782//
783// Distinct from `unique_*` — these are sampled-uniformly strings
784// without the per-call distinctness guarantee. Use `unique_*` when you
785// need every call to return a different value (tempdir names,
786// correlation IDs); use `random_*` for password-style content where
787// uniformity matters more than collision-freedom.
788// ------------------------------------------------------------
789
790/// Generate a random string of exactly `len` characters drawn from
791/// `charset` (uniformly).
792///
793/// Selection is unbiased — `bounded_u64(charset.len() as u64)` is
794/// used internally, so the per-character distribution is uniform.
795///
796/// # Panics
797///
798/// Panics if `charset` is empty or contains a non-ASCII byte
799/// (`byte >= 128`). The ASCII guard preserves UTF-8 validity of the
800/// returned `String`.
801///
802/// # Example
803///
804/// ```
805/// use mod_rand::{tier2, charsets};
806/// let s = tier2::random_string(16, charsets::ALPHANUMERIC);
807/// assert_eq!(s.len(), 16);
808/// ```
809pub fn random_string(len: usize, charset: &[u8]) -> String {
810    assert!(
811        !charset.is_empty(),
812        "random_string: charset must be non-empty"
813    );
814    assert!(
815        charset.iter().all(|&b| b < 128),
816        "random_string: charset must be ASCII (every byte < 128)"
817    );
818    let n = charset.len() as u64;
819    let mut out = String::with_capacity(len);
820    for _ in 0..len {
821        let idx = bounded_u64(n) as usize;
822        out.push(charset[idx] as char);
823    }
824    out
825}
826
827/// Generate a random ASCII alphanumeric string (`A-Z`, `a-z`, `0-9`)
828/// of exactly `len` characters.
829///
830/// Equivalent to `random_string(len, charsets::ALPHANUMERIC)`.
831///
832/// # Example
833///
834/// ```
835/// use mod_rand::tier2;
836/// let s = tier2::random_alphanumeric(12);
837/// assert_eq!(s.len(), 12);
838/// ```
839#[inline]
840pub fn random_alphanumeric(len: usize) -> String {
841    random_string(len, crate::charsets::ALPHANUMERIC)
842}
843
844/// Generate a random ASCII alphabetic string (`A-Z`, `a-z`) of exactly
845/// `len` characters.
846///
847/// # Example
848///
849/// ```
850/// use mod_rand::tier2;
851/// let s = tier2::random_alpha(8);
852/// assert_eq!(s.len(), 8);
853/// ```
854#[inline]
855pub fn random_alpha(len: usize) -> String {
856    random_string(len, crate::charsets::ALPHA)
857}
858
859/// Generate a random ASCII numeric string (`0-9`) of exactly `len`
860/// characters. Leading zeros may appear.
861///
862/// # Example
863///
864/// ```
865/// use mod_rand::tier2;
866/// let s = tier2::random_numeric(6);
867/// assert_eq!(s.len(), 6);
868/// ```
869#[inline]
870pub fn random_numeric(len: usize) -> String {
871    random_string(len, crate::charsets::NUMERIC)
872}
873
874/// Generate a random lowercase hex string (`0-9`, `a-f`) of exactly
875/// `len` characters.
876///
877/// Distinct from [`unique_hex`]: this function makes no uniqueness
878/// guarantee but draws each character uniformly from the hex alphabet.
879///
880/// # Example
881///
882/// ```
883/// use mod_rand::tier2;
884/// let s = tier2::random_hex_string(32);
885/// assert_eq!(s.len(), 32);
886/// ```
887#[inline]
888pub fn random_hex_string(len: usize) -> String {
889    random_string(len, crate::charsets::HEX_LOWER)
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use std::collections::HashSet;
896
897    #[test]
898    fn two_calls_differ() {
899        assert_ne!(unique_u64(), unique_u64());
900    }
901
902    #[test]
903    fn name_meets_exact_length() {
904        for len in [1, 4, 8, 16, 32, 64, 128] {
905            let n = unique_name(len);
906            assert_eq!(n.len(), len, "length {len}");
907        }
908    }
909
910    #[test]
911    fn name_uses_crockford_alphabet() {
912        let n = unique_name(256);
913        for c in n.chars() {
914            assert!(
915                c.is_ascii_digit() || c.is_ascii_uppercase(),
916                "char {c:?} outside Crockford alphabet"
917            );
918            assert!(!matches!(c, 'I' | 'L' | 'O' | 'U'), "ambiguous char {c}");
919        }
920    }
921
922    #[test]
923    fn names_are_unique_across_calls() {
924        // 10 000 names of 16 chars (~80 random bits) — collisions are
925        // astronomically unlikely given the counter guarantee.
926        let mut set = HashSet::with_capacity(10_000);
927        for _ in 0..10_000 {
928            assert!(set.insert(unique_name(16)));
929        }
930    }
931
932    #[test]
933    fn unique_u64_collision_free_at_scale() {
934        // 1 000 000 calls — every value must be distinct (counter
935        // guarantee). This is the headline correctness property of
936        // tier2.
937        let n = 1_000_000;
938        let mut set = HashSet::with_capacity(n);
939        for _ in 0..n {
940            assert!(set.insert(unique_u64()));
941        }
942        assert_eq!(set.len(), n);
943    }
944
945    #[test]
946    fn unique_hex_exact_length_and_alphabet() {
947        for len in [1, 7, 8, 9, 16, 32, 64, 128] {
948            let s = unique_hex(len);
949            assert_eq!(s.len(), len, "length {len}");
950            assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
951        }
952    }
953
954    #[test]
955    fn unique_base32_exact_length() {
956        for len in [1, 5, 8, 13, 32, 64] {
957            let s = unique_base32(len);
958            assert_eq!(s.len(), len);
959        }
960    }
961
962    #[test]
963    fn alphabet_distribution_is_reasonable() {
964        // Chi-squared test on Crockford-base32 alphabet usage over a
965        // large sample. With 32 buckets and 100 000 draws, expected
966        // count per bucket is ~3125. The 0.999 critical value for
967        // chi-squared with 31 d.f. is ~61.1. We use a generous cap of
968        // 100 to keep this test stable on slow CI.
969        let sample: String = (0..100_000)
970            .map(|_| unique_base32(1).chars().next().unwrap())
971            .collect();
972        let mut counts = [0u32; 32];
973        const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
974        for c in sample.chars() {
975            let idx = ALPHABET
976                .iter()
977                .position(|&b| b as char == c)
978                .expect("char from Crockford alphabet");
979            counts[idx] += 1;
980        }
981        let n = sample.len() as f64;
982        let expected = n / 32.0;
983        let chi: f64 = counts
984            .iter()
985            .map(|&c| {
986                let diff = c as f64 - expected;
987                diff * diff / expected
988            })
989            .sum();
990        assert!(chi < 100.0, "chi-squared {chi} too high (alphabet skew)");
991    }
992
993    #[test]
994    fn hex_distribution_is_reasonable() {
995        // Same idea against the 16-char hex alphabet. 100 000 draws,
996        // expected 6250 per bucket, critical value (15 d.f.) ~37.7;
997        // cap at 60.
998        let sample: String = unique_hex(100_000);
999        let mut counts = [0u32; 16];
1000        for c in sample.chars() {
1001            let v = c.to_digit(16).unwrap() as usize;
1002            counts[v] += 1;
1003        }
1004        let n = sample.len() as f64;
1005        let expected = n / 16.0;
1006        let chi: f64 = counts
1007            .iter()
1008            .map(|&c| {
1009                let diff = c as f64 - expected;
1010                diff * diff / expected
1011            })
1012            .sum();
1013        assert!(chi < 60.0, "chi-squared {chi} too high (hex skew)");
1014    }
1015
1016    #[test]
1017    fn zero_length_yields_empty_string() {
1018        assert_eq!(unique_name(0), "");
1019        assert_eq!(unique_hex(0), "");
1020        assert_eq!(unique_base32(0), "");
1021    }
1022
1023    #[test]
1024    fn process_salt_is_stable_within_process() {
1025        let a = process_salt();
1026        let b = process_salt();
1027        assert_eq!(a, b);
1028        assert_ne!(a, 0);
1029    }
1030
1031    // ------------------------------------------------------------
1032    // Bounded-range tests
1033    // ------------------------------------------------------------
1034
1035    #[test]
1036    fn range_u64_bounds() {
1037        for _ in 0..10_000 {
1038            let n = range_u64(100..200);
1039            assert!((100..200).contains(&n));
1040        }
1041    }
1042
1043    #[test]
1044    fn range_u64_single_value_window() {
1045        // [start, start+1) — every draw lands on start.
1046        for _ in 0..1000 {
1047            assert_eq!(range_u64(7..8), 7);
1048        }
1049    }
1050
1051    #[test]
1052    fn range_inclusive_u64_die_roll() {
1053        // Verify all six faces appear over many rolls.
1054        let mut faces = [0u32; 6];
1055        for _ in 0..10_000 {
1056            let d = range_inclusive_u64(1..=6);
1057            assert!((1..=6).contains(&d));
1058            faces[(d - 1) as usize] += 1;
1059        }
1060        for (i, &c) in faces.iter().enumerate() {
1061            assert!(c > 0, "face {} never appeared in 10000 rolls", i + 1);
1062        }
1063    }
1064
1065    #[test]
1066    fn range_inclusive_u64_single_value() {
1067        for _ in 0..1000 {
1068            assert_eq!(range_inclusive_u64(42..=42), 42);
1069        }
1070    }
1071
1072    #[test]
1073    fn range_inclusive_u64_full_width() {
1074        // 0..=u64::MAX is the full u64 space; just ensure no panic
1075        // and at least one draw differs from another.
1076        let a = range_inclusive_u64(0..=u64::MAX);
1077        let b = range_inclusive_u64(0..=u64::MAX);
1078        assert_ne!(a, b);
1079    }
1080
1081    #[test]
1082    fn range_u32_bounds() {
1083        for _ in 0..10_000 {
1084            let n = range_u32(0..256);
1085            assert!(n < 256);
1086        }
1087    }
1088
1089    #[test]
1090    fn range_inclusive_u32_full_width() {
1091        // Just exercises the full-width path without crashing.
1092        for _ in 0..1000 {
1093            let _ = range_inclusive_u32(0..=u32::MAX);
1094        }
1095    }
1096
1097    #[test]
1098    fn range_i64_negative() {
1099        for _ in 0..10_000 {
1100            let n = range_i64(-100..-50);
1101            assert!((-100..-50).contains(&n));
1102        }
1103    }
1104
1105    #[test]
1106    fn range_i64_mixed_sign() {
1107        let mut saw_neg = false;
1108        let mut saw_pos = false;
1109        for _ in 0..10_000 {
1110            let n = range_i64(-100..100);
1111            assert!((-100..100).contains(&n));
1112            if n < 0 {
1113                saw_neg = true;
1114            }
1115            if n >= 0 {
1116                saw_pos = true;
1117            }
1118        }
1119        assert!(saw_neg && saw_pos);
1120    }
1121
1122    #[test]
1123    fn range_inclusive_i64_full_width() {
1124        // i64::MIN..=i64::MAX — just verify no panic.
1125        let _ = range_inclusive_i64(i64::MIN..=i64::MAX);
1126    }
1127
1128    #[test]
1129    fn range_i32_bounds() {
1130        for _ in 0..10_000 {
1131            let n = range_i32(-1000..1000);
1132            assert!((-1000..1000).contains(&n));
1133        }
1134    }
1135
1136    #[test]
1137    fn range_inclusive_i32_full_width() {
1138        for _ in 0..1000 {
1139            let _ = range_inclusive_i32(i32::MIN..=i32::MAX);
1140        }
1141    }
1142
1143    #[test]
1144    #[should_panic(expected = "empty range")]
1145    fn range_u64_panics_on_empty() {
1146        let _ = range_u64(10..10);
1147    }
1148
1149    #[test]
1150    #[should_panic(expected = "empty range")]
1151    #[allow(clippy::reversed_empty_ranges)]
1152    fn range_u64_panics_on_reverse() {
1153        let _ = range_u64(10..5);
1154    }
1155
1156    #[test]
1157    #[should_panic(expected = "empty range")]
1158    #[allow(clippy::reversed_empty_ranges)]
1159    fn range_inclusive_u64_panics_on_reverse() {
1160        let _ = range_inclusive_u64(10..=5);
1161    }
1162
1163    #[test]
1164    #[should_panic(expected = "empty range")]
1165    #[allow(clippy::reversed_empty_ranges)]
1166    fn range_i64_panics_on_reverse() {
1167        let _ = range_i64(5..-5);
1168    }
1169
1170    #[test]
1171    fn range_uniformity_chi_squared() {
1172        // 100 000 draws over a range of 100 buckets. Same statistical
1173        // threshold reasoning as in tier1. Note: this is a real
1174        // uniformity check on the Tier 2 reduction — if anyone
1175        // replaces rejection sampling with `unique_u64() % 100`, the
1176        // bias from the modulo operation would inflate chi-squared
1177        // beyond the threshold and fail this test.
1178        let mut counts = [0u32; 100];
1179        for _ in 0..100_000 {
1180            let v = range_u32(0..100);
1181            counts[v as usize] += 1;
1182        }
1183        let expected = 100_000.0 / 100.0;
1184        let chi: f64 = counts
1185            .iter()
1186            .map(|&c| {
1187                let diff = c as f64 - expected;
1188                diff * diff / expected
1189            })
1190            .sum();
1191        assert!(
1192            chi < 250.0,
1193            "chi-squared {chi} too high — bounded-range output is biased"
1194        );
1195    }
1196}