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