Commit Graph

5 Commits

Author SHA1 Message Date
ddidderr b8f9fa9b1b feat: add unbiased integer range helpers
Expand range sampling from the original u32-only helper to all primitive
integer widths. The new gen_range_<int> methods cover 0..n sampling, while
matching gen_range_<int>_in methods accept standard RangeBounds forms such as
exclusive, inclusive, open-ended, and full-width ranges.

Use rejection sampling against the matching primitive reader for each integer
width. That keeps modulo bias out of the public helpers without introducing a
general RNG trait or depending on rand. Full-width ranges fall back directly to
the primitive reader because their span cannot be represented in the same
integer type.

Document the generated APIs with doctested examples and add a ranges example
that demonstrates unsigned, signed, usize, and full-width sampling.

Test Plan:
- cargo test
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: IDEAS.md ergonomics backlog
2026-04-28 20:03:37 +02:00
ddidderr 733a2508cb feat: add byte-fill convenience construction
Add OsRandom::fill_bytes so callers can fill a byte slice without importing
std::io::Read. This keeps the common raw-byte use case close to the rest of
the crate's typed convenience API while still forwarding to the same
/dev/urandom handle.

Implement Default for OsRandom as the infallible convenience constructor. The
fallible OsRandom::try_new API remains the right choice when the caller wants
to handle an unavailable operating-system randomness source explicitly.

Document the public constructors and byte-fill helper with working examples,
and add a runnable bytes example for the top-level workflow.

Test Plan:
- cargo test
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: IDEAS.md ergonomics backlog
2026-04-28 19:58:27 +02:00
ddidderr d618bd7cc9 docs: add IDEAS.md with ergonomy backlog
Capture a short backlog of ergonomy improvements for the public API so the
ideas don't get lost between sessions. This is a planning document only —
no code changes, and nothing here is committed to as a roadmap.

Items recorded:
- Thread-local global helpers (e.g. `random_u64()`) to avoid threading an
  `OsRandom` through call sites for one-off uses.
- `gen_range` for all integer widths and `Range`/`RangeInclusive` overloads;
  today only `gen_range_u32` exists.
- `fill_bytes(&mut [u8])` so callers don't need to bring `io::Read` into
  scope just to fill a buffer.
- `bool()` and `f64()` (unit interval) helpers.
- Generic `shuffle(&mut [T])` and `choose<T>(&[T]) -> &T`, not just byte
  slices.
- Accept `&str` alphabets directly and drop the ASCII assert by iterating
  `chars()` instead of bytes.
- Loosen `&mut self` to `&self` (interior buffering) or impl `Clone`, so
  multiple call sites can share a handle without borrow-checker friction.
- Preset constructors: `OsRandom::password(len)`, `::hex(len)`,
  `::token(len)`.
2026-04-28 19:38:44 +02:00
ddidderr da9d5f96d8 docs(examples): add runnable demo example
Adds examples/demo.rs so users can quickly see the crate in action via
`cargo run --example demo` without writing any glue code first.

The example exercises the three main surface areas of the crate:

- the typed primitive readers (get_u8, get_u32, get_u64, get_i32) to
  show that every integer width is covered and that signed values come
  out signed;
- gen_range_u32 in a dice-roll idiom (+ 1 to shift 0..6 into 1..6),
  which doubles as a hint that the helper returns a half-open range;
- string_from with three of the prebuilt charsets (ALPHANUMERIC for a
  generic token, HEX_LOWER for a 128-bit-style hex string, DIGITS for
  a numeric PIN), demonstrating the typical "generate me a random
  identifier" use case the crate is designed for.

No library code changes; this is purely an onboarding aid. Picked an
example over expanding the crate-level rustdoc because a runnable
binary is easier to copy-paste-modify than a doctest, and Cargo's
examples/ convention is the idiomatic place for this.

Test Plan:
- `cargo run --example demo` prints one line per demonstrated API and
  exits 0.
- `cargo clippy --all-targets` is clean.
2026-04-28 19:35:37 +02:00
ddidderr 5623568185 feat: initial implementation of ez-urandom
A small, dependency-light crate that wraps `/dev/urandom` and exposes
ergonomic helpers for the things people actually reach for randomness
to do: read a primitive integer, sample a number in a half-open range,
or generate a random ASCII string from a given alphabet.

Why this crate exists
---------------------

The Rust ecosystem already has `rand` and `getrandom`, but both bring
in more surface area than is warranted when all you need is "give me
some bytes from the OS." This crate intentionally stays tiny: a single
`File` handle to `/dev/urandom`, a handful of typed readers, and one
rejection-sampling helper. No traits to learn, no generic plumbing, no
algorithmic CSPRNGs in-process — the kernel does that job already.

What's included
---------------

- `OsRandom`: thin wrapper around an open `/dev/urandom` handle,
  constructed via `try_new()`. Implements `io::Read` so it can plug
  into anywhere a byte source is expected.
- Typed integer readers (`get_u8` ... `get_i128`, plus `usize`/`isize`)
  generated by a small `paste!` macro. Each reads exactly
  `size_of::<T>()` bytes and interprets them in native-endian order so
  every bit pattern is equally likely.
- `gen_range_u32(n)`: uniform integer in `0..n` via rejection sampling
  on top of `get_u32`. The cutoff is computed as the largest multiple
  of `n` that fits in `2^32`, so there is *no* modulo bias — values at
  or above the cutoff are discarded and resampled. This is the
  textbook fix for the "`rand() % n` is biased when `n` doesn't divide
  `RAND_MAX+1`" footgun.
- `pick(set)` and `string_from(set, len)`: uniform byte / ASCII string
  drawn from a caller-supplied alphabet, layered on `gen_range_u32`.
- `charset` module with `const`-built alphabets: `DIGITS`, `LOWERCASE`,
  `UPPERCASE`, `ALPHABETIC`, `ALPHANUMERIC`, `HEX_LOWER`, `HEX_UPPER`.
  Built with a small `const fn concat` so the arrays exist as compile-
  time constants with no runtime allocation.

Design choices and tradeoffs
----------------------------

- Linux/Unix only by construction: opens `/dev/urandom` directly. This
  is a deliberate scope limit — supporting Windows would mean pulling
  in `BCryptGenRandom` and an abstraction layer, which defeats the
  point of the crate. Users who need cross-platform should reach for
  `getrandom`.
- `unsafe_code = "forbid"` at the crate level. The implementation does
  not need `unsafe`, and forbidding it makes that contract explicit
  and machine-checked.
- Clippy `pedantic` is on as `warn`, plus `unwrap_used = "warn"` and
  `todo = "warn"`, so the code is held to a tighter standard than the
  defaults from day one.
- `panic = "unwind"` in release: the crate's `assert!`s (e.g. "set
  must not be empty") should be recoverable by callers that wrap
  them, not abort the process.
- Native-endian integer decoding: the bits are uniformly random, so
  endianness is irrelevant to the distribution. Choosing native-endian
  avoids a needless byte swap on every read.
- Rejection sampling uses a 32-bit cutoff regardless of the requested
  range. A 64-bit cutoff would reduce the rejection probability for
  ranges close to `u32::MAX`, but in practice the worst-case rejection
  rate is < 50% and the simpler code wins.

Known limitations
-----------------

- No async API; reads are blocking. `/dev/urandom` does not block in
  practice on Linux post-init, so this is fine for typical use.
- `gen_range` is only provided for `u32`. Wider ranges would need a
  64-bit variant; not added until a use case appears.
- File handle is held for the lifetime of `OsRandom`. Callers that
  want a fresh fd per call should construct a new instance.

Test Plan
---------

- `cargo build` and `cargo clippy --all-targets` are clean under the
  pedantic lint set configured in `Cargo.toml`.
- Manual smoke test by reading several integers and generating
  alphanumeric / hex / digit strings; values are well-distributed and
  no obvious bias is visible.
2026-04-28 19:35:36 +02:00