mod-rand 1.0.0

Tiered randomness for Rust: fast PRNG, process-unique seeds, and OS-backed cryptographic random — plus bounded ranges, strings, tokens, shuffle, sample, and weighted choice. Zero dependencies, MSRV 1.75.
Documentation
<h1 align="center">
  <img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
  <br>
  <code>mod-rand</code>
  <br>
  <sup>
    <sub>TIERED RANDOMNESS FOR RUST</sub>
  </sup>
</h1>

<p align="center">
    <a href="https://crates.io/crates/mod-rand"><img alt="crates.io" src="https://img.shields.io/crates/v/mod-rand.svg"></a>
    <a href="https://crates.io/crates/mod-rand"><img alt="downloads" src="https://img.shields.io/crates/d/mod-rand.svg"></a>
    <a href="https://docs.rs/mod-rand"><img alt="docs.rs" src="https://docs.rs/mod-rand/badge.svg"></a>
    <img alt="MSRV" src="https://img.shields.io/badge/MSRV-1.75%2B-blue.svg?style=flat-square" title="Rust Version">
    <a href="https://github.com/jamesgober/mod-rand/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/jamesgober/mod-rand/actions/workflows/ci.yml/badge.svg"></a>
</p>

<p align="center">
    Numbers, bytes, strings, tokens, and collection ops (shuffle, sample,<br>
    weighted choice) at three explicit threat-model tiers — one zero-dependency library.
</p>

---

## What it is

`mod-rand` is a tiered **randomness** library for Rust. Three concrete
random sources, each chosen to be excellent for one threat model;
every operation you'd reach for — bounded integer ranges, byte
buffers, hex tokens, random or process-unique strings, shuffles,
samples, and weighted choice — exposed at the appropriate tier.

Zero runtime dependencies. MSRV `1.75`. API locked under SemVer
for the entire `1.x` line.

```rust
use mod_rand::tier1::Xoshiro256;
use mod_rand::{tier2, tier3};

// Tier 1 — fast deterministic PRNG (xoshiro256**). Reproducible from a seed.
let mut rng = Xoshiro256::seed_from_u64(42);
let n: u64       = rng.next_u64();
let pct: u32     = rng.gen_range_u32(0..100);
let token: String = rng.gen_alphanumeric(16);

// Tier 2 — process-unique values. Tempdir names, request IDs.
let id: String = tier2::unique_name(12);

// Tier 3 — OS-backed cryptographic random. Tokens, keys, session IDs.
let secret: String = tier3::random_hex(32)?;
# Ok::<(), std::io::Error>(())
```

## What it covers

Every category of randomness operation that's in scope, at every tier
where it makes sense.

| Operation                              | Tier 1 (PRNG)      | Tier 2 (process-unique) | Tier 3 (crypto) |
|----------------------------------------|--------------------|--------------------------|-----------------|
| Raw `u32` / `u64` draws                | `next_u32` / `next_u64` | `unique_u64`            | `random_u32` / `random_u64` |
| Fill a `&mut [u8]`                     | `fill_bytes`       || `fill_bytes`     |
| Bounded integer ranges (every width)   | `gen_range_*`      | `range_*`                | `random_range_*` |
| 128-bit integer ranges                 | `gen_range_u128` / `i128` | `range_u128` / `i128` | `random_range_u128` / `i128` |
| Uniform `f64` / `f32`                  | `gen_f64` / `gen_f32` / `next_f64` |||
| Bernoulli trial                        | `gen_bool(p)`      |||
| Hex strings                            | `gen_hex(len)`     | `unique_hex(len)` / `random_hex_string(len)` | `random_hex(bytes)` / `random_hex_string(len)` |
| Alphanumeric strings                   | `gen_alphanumeric` | `unique_name` / `random_alphanumeric` | `random_alphanumeric` |
| Arbitrary-charset strings              | `gen_string`       | `random_string`          | `random_string` |
| Base32 / base58 / base64 / URL-safe    | via `charsets::*`  | via `charsets::*`        | via `charsets::*` |
| Shuffle a `&mut [T]`                   | `shuffle`          || `shuffle`        |
| Sample `k` without replacement         | `sample`           |||
| Weighted choice                        | `weighted_choice` / `weighted_index` |||
| Stream splitting (`2^128` / `2^192`)   | `jump` / `long_jump` |||

Tier 1 is `no_std`-compatible for everything that doesn't require
allocation. Tier 2 and Tier 3 require `std` (default feature).

## The three tiers

Pick by threat model, not by speed.

| Tier | Algorithm | Use case | Crypto-safe |
|------|-----------|----------|-------------|
| 1 | xoshiro256\*\* (splitmix64-seeded) | Simulation, fixtures, shuffling, non-security strings | No |
| 2 | PID + nanos + atomic counter + Stafford-mix-13 | Tempdir names, request IDs, log correlation IDs | No |
| 3 | OS syscall (`getrandom` / `BCryptGenRandom` / `getentropy`) | Tokens, keys, session IDs, password salts | Yes |

| Question                                           | Tier |
|----------------------------------------------------|------|
| Is the value reproducible from a seed?             | 1    |
| Must two calls inside one process always differ?   | 2    |
| Could an attacker benefit from predicting it?      | 3    |

If the answer to the third question is "yes" — even maybe — use Tier 3.

## Bounded ranges — every integer width

Every tier exposes `gen_range_*` / `range_*` / `random_range_*`
methods using Rust's range syntax. `..` is half-open, `..=` is
inclusive; the caller's choice is the contract. All bounded methods
use **Daniel Lemire's "Nearly Divisionless"** rejection sampling, so
output is uniformly distributed — no modulo bias.

Widths covered: `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, and the
signed counterparts.

```rust
use mod_rand::tier1::Xoshiro256;
use mod_rand::{tier2, tier3};

let mut rng = Xoshiro256::seed_from_u64(42);

let byte: u8   = rng.gen_range_inclusive_u8(0..=255);
let pct:  u32  = rng.gen_range_u32(0..100);
let big:  u128 = rng.gen_range_u128(0..(u128::MAX / 2));
let die:  u32  = rng.gen_range_inclusive_u32(1..=6);   // classic d6

let id     = tier2::range_inclusive_u32(1..=1_000);
let secret = tier3::random_range_inclusive_u64(0..=u64::MAX)?;
# Ok::<(), std::io::Error>(())
```

The 128-bit ranges generalise Lemire's algorithm to a 256-bit
intermediate via two `u64` draws; the full-width inclusive
`0..=u128::MAX` is special-cased to a raw 128-bit reinterpret.

## String generation

Custom charset, or a pick from the standard ones in
[`mod_rand::charsets`](https://docs.rs/mod-rand/latest/mod_rand/charsets/index.html).

```rust
use mod_rand::tier1::Xoshiro256;
use mod_rand::{charsets, tier2, tier3};

let mut rng = Xoshiro256::seed_from_u64(1);

// Tier 1 — deterministic from the seed.
let id    = rng.gen_alphanumeric(16);
let hex   = rng.gen_hex(32);
let pin   = rng.gen_numeric(6);

// Custom charset — pick from charsets:: or supply your own &[u8].
let url   = rng.gen_string(24, charsets::URL_SAFE);
let coin  = rng.gen_string(20, charsets::BASE58);

// Tier 2 — `unique_*` guarantees per-call distinctness;
// `random_*` guarantees a uniform distribution. Both ship.
let name  = tier2::unique_name(12);             // distinct across calls
let token = tier2::random_alphanumeric(16);     // uniform distribution

// Tier 3 — cryptographic.
let key   = tier3::random_alphanumeric(32)?;
# Ok::<(), std::io::Error>(())
```

Charset constants in `mod_rand::charsets`: `ALPHANUMERIC`, `ALPHA`,
`ALPHA_LOWER`, `ALPHA_UPPER`, `NUMERIC`, `HEX_LOWER`, `HEX_UPPER`,
`URL_SAFE` (RFC 4648 §5), `BASE58` (Bitcoin alphabet), `BASE64`
(RFC 4648 §4). Custom charsets are accepted as `&[u8]`; non-ASCII
charsets are rejected (panic on Tier 1 / Tier 2; `InvalidInput` on
Tier 3) so the returned `String` is always valid UTF-8.

## Collection operations

Fisher-Yates shuffle, sample-without-replacement, and weighted
choice. Available on Tier 1 (deterministic, reproducible) and
Tier 3 shuffle (cryptographic).

```rust
use mod_rand::tier1::Xoshiro256;
use mod_rand::tier3;

let mut rng = Xoshiro256::seed_from_u64(1);

// Shuffle in place. Uniform over all n! permutations.
let mut hand: Vec<u8> = (0..52).collect();
rng.shuffle(&mut hand);

// Sample k references without replacement, in source-slice order.
let pool: Vec<&str> = vec!["alice", "bob", "carol", "dave", "erin"];
let chosen: Vec<&&str> = rng.sample(&pool, 2);

// Weighted choice over a probability vector.
let items   = ["common", "uncommon", "rare", "epic"];
let weights = [70.0, 20.0, 8.0, 2.0];
let drop    = rng.weighted_choice(&items, &weights).unwrap();

// Cryptographic shuffle — every permutation drawn from the OS CSPRNG.
let mut deck: Vec<u8> = (0..52).collect();
tier3::shuffle(&mut deck)?;
# Ok::<(), std::io::Error>(())
```

## Performance

Measured on a Ryzen 9 9950X3D, Windows 11 (`cargo bench`):

**Tier 1 — xoshiro256\*\* (target ~1 ns/u64)**

| op                                              | time      |
|-------------------------------------------------|-----------|
| `next_u64`                                      | ~0.6 ns   |
| `next_f64` / `gen_f64`                          | ~0.7 ns   |
| `gen_f32`                                       | ~0.7 ns   |
| `gen_bool(0.5)`                                 | ~0.8 ns   |
| `fill_bytes(32)`                                | ~2.0 ns   |
| `gen_range_u64(0..100)`                         | ~0.8 ns   |
| `gen_range_inclusive_u32(1..=6)` (d6)           | ~0.9 ns   |
| `gen_range_u8(0..100)`                          | ~0.8 ns   |
| `gen_range_u128(0..u128::MAX/2)`                | ~2.2 ns   |
| `gen_range_inclusive_u128(0..=u128::MAX)`       | ~1.1 ns   |
| `gen_range_u64(0..⅔·u64::MAX)` worst-case       | ~5.9 ns   |
| `gen_alphanumeric(16)`                          | ~58 ns    |
| `gen_alphanumeric(64)`                          | ~115 ns   |
| `gen_hex(32)`                                   | ~63 ns    |
| `shuffle(10)`                                   | ~10 ns    |
| `shuffle(100)`                                  | ~104 ns   |
| `shuffle(1000)`                                 | ~953 ns   |
| `shuffle(10000)`                                | ~9.5 µs   |
| `sample(k=10, n=1000)`                          | ~905 ns   |
| `weighted_index(10 weights)`                    | ~19 ns    |
| `weighted_index(100 weights)`                   | ~172 ns   |
| `weighted_index(1000 weights)`                  | ~1.1 µs   |

**Tier 2 — process-unique (dominated by `SystemTime::now()`)**

| op                                              | time      |
|-------------------------------------------------|-----------|
| `unique_u64`                                    | ~21 ns    |
| `unique_name(8)`                                | ~42 ns    |
| `unique_hex(16)`                                | ~47 ns    |
| `range_u64(0..100)`                             | ~23 ns    |
| `range_u128(0..u128::MAX/2)`                    | ~45 ns    |
| `random_alphanumeric(16)`                       | ~420 ns   |

**Tier 3 — OS CSPRNG (Windows / `BCryptGenRandom`)**

| op                                              | time      |
|-------------------------------------------------|-----------|
| `random_u64`                                    | ~35 ns    |
| `fill_bytes(32)`                                | ~53 ns    |
| `fill_bytes(1024)`                              | ~224 ns   |
| `random_hex(16)`                                | ~97 ns    |
| `random_range_u64(0..100)`                      | ~35 ns    |
| `random_range_u128(0..u128::MAX/2)`             | ~48 ns    |
| `random_alphanumeric(16)`                       | ~628 ns   |
| `random_alphanumeric(64)`                       | ~2.4 µs   |
| `shuffle(52)` (deck of cards)                   | ~1.9 µs   |
| `shuffle(1000)`                                 | ~38 µs    |

Tier 1 beats its 1 ns/u64 target. Linux / macOS Tier 3 numbers are
kernel-dependent; expect 100–500 ns per call on commodity hardware.

## Why this library exists

- **Zero dependencies.** No `rand`, no `getrandom` crate, no `libc`.
  Just `std`. Tier 1 works in `no_std`.
- **Explicit threat model.** You pick the tier; you know what
  guarantees you're getting.
- **Lower MSRV than the alternatives.** `1.75`. Many random crates
  in the ecosystem require `1.85+`.
- **Determinism contract on Tier 1.** Locked byte-for-byte under
  SemVer for the entire `1.x` line — see
  [`docs/STABILITY.md`]docs/STABILITY.md.
- **One library, three tiers, every operation.** Numbers, bytes,
  strings, tokens, shuffle, sample, weighted choice. You don't need
  `rand` + `rand_distr` + `fastrand` + a hand-rolled crypto random.

## How it compares

Honest, side-by-side. The full discussion (including non-strengths)
is in [`docs/COMPARISON.md`](docs/COMPARISON.md).

|                              | `mod-rand` | `rand` + `rand_distr` | `fastrand` | `nanorand` |
|------------------------------|------------|-----------------------|------------|------------|
| Fast PRNG                    | ✓ Tier 1   | ✓ many                || ✓ many     |
| OS cryptographic random      | ✓ Tier 3   | ✓ via `getrandom`     || ✓ ChaCha20 (userspace) |
| Process-unique stream        | ✓ Tier 2   | partial (build yourself) |||
| 128-bit integer ranges       |||||
| String generation in API     || via `Alphanumeric`    |||
| Shuffle / sample / weighted  ||| shuffle    | shuffle    |
| Named distributions          || ✓ (`rand_distr`)      |||
| Generic `Rng` trait          |||| partial    |
| Runtime crate dependencies   | **0**      | several (`getrandom`, etc.) | 0+ | 0+         |
| MSRV (at time of writing)    | **1.75**   | higher                | 1.61       | 1.65       |

Use `mod-rand` when its tier model fits and you want zero deps + a
locked API. Reach for `rand` if you need named distributions or
generic-trait ecosystem interop.

## Feature flags

```toml
[dependencies]
mod-rand = { version = "1.0", default-features = false }   # tier1 only, no_std
mod-rand = { version = "1.0", features = ["tier2"] }       # + tier2
mod-rand = "1.0"                                             # all three tiers (default)
```

| Feature  | Effect                                                         |
|----------|----------------------------------------------------------------|
| `std`    | Required by `tier2`, `tier3`, and string generation on `tier1` |
| `tier2`  | Enables `mod_rand::tier2`                                      |
| `tier3`  | Enables `mod_rand::tier3`                                      |

Default features: `["std", "tier2", "tier3"]`.

## Documentation

- [docs/API.md]docs/API.md — hand-written API reference, tier by tier.
- [docs/STABILITY.md]docs/STABILITY.md — SemVer policy, determinism
  contract, deprecation rules.
- [docs/COMPARISON.md]docs/COMPARISON.md — side-by-side with `rand`,
  `fastrand`, `nanorand`.
- [docs/API-FREEZE-AUDIT.md]docs/API-FREEZE-AUDIT.md — full manifest
  of every `1.0` public symbol.
- [CHANGELOG.md]CHANGELOG.md — release history.

## Minimum supported Rust version

`1.75` — pinned in `Cargo.toml`, verified by CI on every change. An
MSRV bump is a minor-version bump minimum.

## License

Apache-2.0. See [LICENSE](LICENSE).


<!-- COPYRIGHT
---------------------------------->
<div align="center">
  <br>
  <h2></h2>
  Copyright &copy; 2026 James Gober.
</div>