53fc84a2702f577e40dee13e73c09f45776c81fe
1 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |