3 Commits

Author SHA1 Message Date
ddidderr 08038e685b [release] ez-urandom v1.0.0 2026-04-28 20:27:39 +02:00
ddidderr 852c2551fd docs: declare MIT-0 licensing
Add the MIT No Attribution license text and mark the crate with the
matching SPDX identifier. This makes the repository license explicit in
both the source header and Cargo package metadata, so downstream tools
can recognize the crate as MIT-0 without guessing from prose.

The license file uses the SPDX MIT-0 text with this project's copyright
holder. The Rust source keeps the machine-readable SPDX-License-Identifier
header at the top of the crate root.

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

Trailer:
- SPDX-License-Identifier: MIT-0
- SPDX Specification: https://spdx.github.io/spdx-spec/v3.0.1/
- SPDX MIT-0: https://spdx.org/licenses/MIT-0.html
2026-04-28 20:26:47 +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