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}