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//! - 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}