Compare commits

..

11 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 631dc1938c docs: update ergonomics backlog
Refresh IDEAS.md after the ergonomic API pass so the backlog no longer lists
implemented items as open work. Split the remaining ideas into deferred and
still-open groups to record which omissions were deliberate crate-scope choices.

This keeps the project notes aligned with the public API and captures the
reasoning for not adding globals, shuffle, or password presets yet.

Test Plan:
- Not run; documentation-only change.

Refs: IDEAS.md ergonomics backlog
2026-04-28 20:10:06 +02:00
ddidderr 808e0482c3 feat: support cloning random handles
Add OsRandom::try_clone for fallible duplication of the underlying
/dev/urandom file handle, and implement Clone as the infallible convenience
form. This gives independent call sites their own OsRandom values without
adding interior mutability or shared buffering to the core type.

The fallible method is documented as the preferred API when callers need to
handle a failed file-handle duplication. Clone mirrors Default by panicking only
for the convenience path.

Document both cloning forms with doctested examples and add a runnable clone
example that uses independent handles for IDs and string helpers.

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

Refs: IDEAS.md ergonomics backlog
2026-04-28 20:07:47 +02:00
ddidderr 53fc84a270 feat: add common string presets
Add token, hex, and pin helpers for the common ASCII string formats already
represented by the crate's built-in charsets. These methods keep frequent use
cases concise while continuing to route through string_from, so callers still
get the same uniform per-character sampling behavior.

Avoid adding a password preset in this change. Password generation implies
policy choices around symbols, ambiguous characters, and service-specific
constraints, while these presets are direct names for existing alphabets.

Document every preset with doctested examples and add a runnable presets
example. Update the demo to show both presets and custom string generation.

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

Refs: IDEAS.md ergonomics backlog
2026-04-28 20:06:39 +02:00
ddidderr 0365ab86d1 feat: add generic slice choice helper
Add OsRandom::choose for callers that want a uniformly selected reference from
any non-empty slice. This covers the common generic selection use case without
adding collection algorithms such as shuffle.

Keep OsRandom::pick as the byte-oriented helper by delegating it through
choose. This preserves the existing public API while sharing the usize range
sampler and avoiding the old u32 length limit.

Document choose, pick, and string_from with working examples, and add a choices
example that demonstrates generic slice selection alongside byte alphabets.

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

Refs: IDEAS.md ergonomics backlog
2026-04-28 20:04:57 +02:00
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
11 changed files with 627 additions and 30 deletions
Generated
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "ez-urandom"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"paste",
]
+2 -1
View File
@@ -1,7 +1,8 @@
[package]
name = "ez-urandom"
version = "0.1.0"
version = "1.0.0"
edition = "2024"
license = "MIT-0"
[dependencies]
paste = "1"
+27
View File
@@ -0,0 +1,27 @@
# Ergonomy ideas
- Implemented:
- `Default` for infallible convenience construction.
- `fill_bytes(&mut [u8])` convenience so callers don't need to import
`io::Read`.
- `gen_range_<int>` for all primitive integer widths.
- `gen_range_<int>_in` for standard `RangeBounds` forms, including `Range`
and `RangeInclusive`.
- `choose<T>(&[T]) -> &T` for generic slice selection.
- `Clone` plus fallible `try_clone()` so multiple call sites can own separate
handles without adding interior mutability.
- Presets for direct alphabet names: `OsRandom::token(len)`, `::hex(len)`,
and `::pin(len)`.
- Deferred to keep the crate small:
- `thread_local` global helpers like `ez_urandom::random_u64()` without
constructing or threading an `OsRandom`.
- `shuffle(&mut [T])`; `choose` covers the smaller selection use case without
growing into collection algorithms.
- `OsRandom::password(len)`; password generation needs policy decisions about
symbols, ambiguous characters, and service-specific constraints.
- Still open for later consideration:
- `bool()` / `f64()` (unit interval) helpers.
- Accept a `&str` alphabet directly (not just `&[u8]`) and drop the ASCII
assert by iterating `chars()`.
+5
View File
@@ -0,0 +1,5 @@
Copyright 2026 ddidderr
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+14
View File
@@ -0,0 +1,14 @@
//! Run with: `cargo run --example bytes`
use ez_urandom::OsRandom;
fn main() -> std::io::Result<()> {
let mut rng = OsRandom::default();
let mut session_key = [0u8; 32];
rng.fill_bytes(&mut session_key)?;
println!("session key bytes: {session_key:02x?}");
Ok(())
}
+16
View File
@@ -0,0 +1,16 @@
//! Run with: `cargo run --example choices`
use ez_urandom::{OsRandom, charset};
fn main() -> std::io::Result<()> {
let mut rng = OsRandom::try_new()?;
let environments = ["dev", "staging", "prod"];
let environment = rng.choose(&environments)?;
let suffix = rng.pick(charset::HEX_LOWER)?;
println!("environment: {environment}");
println!("hex suffix : {}", char::from(suffix));
Ok(())
}
+15
View File
@@ -0,0 +1,15 @@
//! Run with: `cargo run --example clone`
use ez_urandom::OsRandom;
fn main() -> std::io::Result<()> {
let mut ids = OsRandom::try_new()?;
let mut tokens = ids.try_clone()?;
let mut fallback = tokens.clone();
println!("id : {}", ids.get_u64()?);
println!("token : {}", tokens.token(16)?);
println!("fallback: {}", fallback.hex(16)?);
Ok(())
}
+30
View File
@@ -0,0 +1,30 @@
//! Run with: `cargo run --example demo`
use ez_urandom::{OsRandom, charset};
fn main() -> std::io::Result<()> {
let mut rng = OsRandom::try_new()?;
println!("u8 : {}", rng.get_u8()?);
println!("u32 : {}", rng.get_u32()?);
println!("u64 : {}", rng.get_u64()?);
println!("i32 : {}", rng.get_i32()?);
println!("dice 1-6 : {}", rng.gen_range_u32(6)? + 1);
println!("offset : {}", rng.gen_range_i32_in(-10..=10)?);
println!("index : {}", rng.gen_range_usize_in(0..16)?);
let token = rng.token(24)?;
println!("token : {token}");
let hex = rng.hex(32)?;
println!("hex : {hex}");
let custom = rng.string_from(charset::ALPHANUMERIC, 12)?;
println!("custom : {custom}");
let pin = rng.pin(6)?;
println!("pin : {pin}");
Ok(())
}
+17
View File
@@ -0,0 +1,17 @@
//! Run with: `cargo run --example presets`
use ez_urandom::OsRandom;
fn main() -> std::io::Result<()> {
let mut rng = OsRandom::try_new()?;
let token = rng.token(24)?;
let hex = rng.hex(32)?;
let pin = rng.pin(6)?;
println!("token: {token}");
println!("hex : {hex}");
println!("pin : {pin}");
Ok(())
}
+21
View File
@@ -0,0 +1,21 @@
//! Run with: `cargo run --example ranges`
use ez_urandom::OsRandom;
fn main() -> std::io::Result<()> {
let mut rng = OsRandom::try_new()?;
let byte = rng.gen_range_u8(10)?;
let port = rng.gen_range_u16_in(49152..=65535)?;
let index = rng.gen_range_usize_in(0..8)?;
let offset = rng.gen_range_i32_in(-10..=10)?;
let full_width = rng.gen_range_i128_in(..)?;
println!("u8 below 10 : {byte}");
println!("ephemeral port : {port}");
println!("usize index : {index}");
println!("signed offset : {offset}");
println!("full-width i128 : {full_width}");
Ok(())
}
+479 -28
View File
@@ -1,14 +1,30 @@
// SPDX-License-Identifier: MIT-0
//! Easy access to operating-system randomness via `/dev/urandom`.
//!
//! The crate exposes [`OsRandom`], a small wrapper around an open handle to
//! `/dev/urandom` with helpers for reading primitive integers, sampling
//! uniformly without modulo bias, and generating random ASCII strings from a
//! caller-supplied alphabet. Common alphabets live in [`charset`].
//!
//! # Project layout
//!
//! - [`OsRandom`] owns an open `/dev/urandom` handle.
//! - Constructors open or duplicate that handle.
//! - Primitive readers turn raw bytes into integer values.
//! - Convenience byte readers fill caller-provided buffers.
//! - Sampling helpers build unbiased higher-level choices from those primitive
//! readers.
//! - Range helpers sample integer spans.
//! - Slice helpers choose caller-owned values by reference.
//! - [`charset`] contains reusable ASCII alphabets for string generation.
//! - String helpers compose those alphabets into common token formats.
use std::{
fs::File,
io::{self, Read},
mem::size_of,
ops::{Bound, RangeBounds},
};
use ::paste::paste;
@@ -94,6 +110,179 @@ macro_rules! os_random_get_integer_impls {
};
}
macro_rules! os_random_unsigned_range_impls {
($(($t:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => {
$(
/// Returns a uniformly distributed integer in `0..n`.
///
/// # Panics
/// Panics if `n == 0`.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
#[doc = concat!("let value = rng.", stringify!($method), "(10)?;")]
/// assert!(value < 10);
/// # Ok(())
/// # }
/// ```
pub fn $method(&mut self, n: $t) -> io::Result<$t> {
assert!(n > 0, "n must be greater than zero");
self.$below_method(n)
}
/// Returns a uniformly distributed integer from `range`.
///
/// The range may use any [`RangeBounds`] form, including `a..b`,
/// `a..=b`, `..b`, `..=b`, `a..`, and `..`.
///
/// # Panics
/// Panics if the range is empty or if an excluded bound overflows
/// the integer type.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
#[doc = concat!("let value = rng.", stringify!($range_method), "(4..=9)?;")]
/// assert!((4..=9).contains(&value));
/// # Ok(())
/// # }
/// ```
pub fn $range_method<R>(&mut self, range: R) -> io::Result<$t>
where
R: RangeBounds<$t>,
{
let start = match range.start_bound() {
Bound::Included(&start) => start,
Bound::Excluded(&start) => start
.checked_add(1)
.expect("excluded range start must not overflow"),
Bound::Unbounded => <$t>::MIN,
};
let end = match range.end_bound() {
Bound::Included(&end) => end,
Bound::Excluded(&end) => end
.checked_sub(1)
.expect("excluded range end must not overflow"),
Bound::Unbounded => <$t>::MAX,
};
assert!(start <= end, "range must not be empty");
let span = end.wrapping_sub(start).wrapping_add(1);
if span == 0 {
return self.$get_method();
}
Ok(start.wrapping_add(self.$below_method(span)?))
}
fn $below_method(&mut self, n: $t) -> io::Result<$t> {
let threshold = (0 as $t).wrapping_sub(n) % n;
loop {
let value = self.$get_method()?;
if value >= threshold {
return Ok(value % n);
}
}
}
)*
};
}
macro_rules! os_random_signed_range_impls {
($(($t:ty, $u:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => {
$(
/// Returns a uniformly distributed integer in `0..n`.
///
/// # Panics
/// Panics if `n <= 0`.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
#[doc = concat!("let value = rng.", stringify!($method), "(10)?;")]
/// assert!((0..10).contains(&value));
/// # Ok(())
/// # }
/// ```
pub fn $method(&mut self, n: $t) -> io::Result<$t> {
assert!(n > 0, "n must be greater than zero");
self.$range_method(0..n)
}
/// Returns a uniformly distributed integer from `range`.
///
/// The range may use any [`RangeBounds`] form, including `a..b`,
/// `a..=b`, `..b`, `..=b`, `a..`, and `..`.
///
/// # Panics
/// Panics if the range is empty or if an excluded bound overflows
/// the integer type.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
#[doc = concat!("let value = rng.", stringify!($range_method), "(-4..=4)?;")]
/// assert!((-4..=4).contains(&value));
/// # Ok(())
/// # }
/// ```
pub fn $range_method<R>(&mut self, range: R) -> io::Result<$t>
where
R: RangeBounds<$t>,
{
const SIGN_BIT: $u = (<$u>::MAX >> 1) + 1;
let start = match range.start_bound() {
Bound::Included(&start) => start,
Bound::Excluded(&start) => start
.checked_add(1)
.expect("excluded range start must not overflow"),
Bound::Unbounded => <$t>::MIN,
};
let end = match range.end_bound() {
Bound::Included(&end) => end,
Bound::Excluded(&end) => end
.checked_sub(1)
.expect("excluded range end must not overflow"),
Bound::Unbounded => <$t>::MAX,
};
assert!(start <= end, "range must not be empty");
let start = start.cast_unsigned() ^ SIGN_BIT;
let end = end.cast_unsigned() ^ SIGN_BIT;
let span = end.wrapping_sub(start).wrapping_add(1);
if span == 0 {
return self.$get_method();
}
let value = start.wrapping_add(self.$below_method(span)?) ^ SIGN_BIT;
Ok(value.cast_signed())
}
)*
};
}
/// Constructor and primitive integer readers.
///
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
@@ -102,56 +291,237 @@ macro_rules! os_random_get_integer_impls {
impl OsRandom {
/// # Errors
/// Returns any I/O error produced while opening `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let value = rng.get_u64()?;
/// # let _ = value;
/// # Ok(())
/// # }
/// ```
pub fn try_new() -> Result<Self, io::Error> {
let devurandom = File::open(DEV_URANDOM)?;
Ok(Self { devurandom })
}
/// Fills `buf` with bytes from `/dev/urandom`.
///
/// This is equivalent to calling [`Read::read_exact`] on `OsRandom`, but it
/// keeps the common "fill this byte slice" use case available without
/// importing the [`Read`] trait.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let mut bytes = [0u8; 32];
/// rng.fill_bytes(&mut bytes)?;
/// # Ok(())
/// # }
/// ```
pub fn fill_bytes(&mut self, buf: &mut [u8]) -> io::Result<()> {
self.devurandom.read_exact(buf)
}
/// Duplicates this handle to `/dev/urandom`.
///
/// The cloned value has its own [`File`] handle, so separate call sites can
/// own independent `OsRandom` values while reading from the same
/// operating-system randomness source.
///
/// # Errors
/// Returns any I/O error produced while duplicating the underlying file
/// handle.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut first = ez_urandom::OsRandom::try_new()?;
/// let mut second = first.try_clone()?;
///
/// let a = first.get_u64()?;
/// let b = second.get_u64()?;
/// # let _ = (a, b);
/// # Ok(())
/// # }
/// ```
pub fn try_clone(&self) -> io::Result<Self> {
Ok(Self {
devurandom: self.devurandom.try_clone()?,
})
}
os_random_get_integer_impls!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
);
}
/// Opens `/dev/urandom` and panics if the operating-system source is
/// unavailable.
///
/// Prefer [`OsRandom::try_new`] when the caller should decide how to handle an
/// I/O failure.
///
/// # Examples
///
/// ```
/// let mut rng = ez_urandom::OsRandom::default();
/// let mut bytes = [0u8; 16];
/// rng.fill_bytes(&mut bytes).expect("/dev/urandom read failed");
/// ```
impl Default for OsRandom {
fn default() -> Self {
Self::try_new().expect("/dev/urandom should be available")
}
}
/// Duplicates the underlying `/dev/urandom` handle.
///
/// Prefer [`OsRandom::try_clone`] when the caller should decide how to handle an
/// I/O failure while duplicating the file handle.
///
/// # Examples
///
/// ```
/// let rng = ez_urandom::OsRandom::default();
/// let mut cloned = rng.clone();
/// let value = cloned.get_u32().expect("/dev/urandom read failed");
/// # let _ = value;
/// ```
impl Clone for OsRandom {
fn clone(&self) -> Self {
self.try_clone()
.expect("/dev/urandom handle should be cloneable")
}
}
/// Uniform sampling helpers.
///
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]
/// so the result is exactly uniform — no modulo bias.
/// Range methods use rejection sampling on top of the matching primitive
/// integer reader, so bounded results are exactly uniform — no modulo bias.
impl OsRandom {
/// Returns a uniformly distributed integer in `0..n`.
///
/// # Panics
/// Panics if `n == 0`.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
pub fn gen_range_u32(&mut self, n: u32) -> io::Result<u32> {
assert!(n > 0, "n must be greater than zero");
let n64 = u64::from(n);
// Largest multiple of n that fits in 2^32. Values at or above the
// cutoff would skew the modulo, so we discard and resample.
let cutoff = (1u64 << 32) - ((1u64 << 32) % n64);
loop {
let v = u64::from(self.get_u32()?);
if v < cutoff {
return Ok(u32::try_from(v % n64).expect("v % n is bounded by n which fits in u32"));
}
}
}
os_random_unsigned_range_impls!(
(u8, gen_range_u8, gen_range_u8_in, gen_below_u8, get_u8),
(u16, gen_range_u16, gen_range_u16_in, gen_below_u16, get_u16),
(u32, gen_range_u32, gen_range_u32_in, gen_below_u32, get_u32),
(u64, gen_range_u64, gen_range_u64_in, gen_below_u64, get_u64),
(
u128,
gen_range_u128,
gen_range_u128_in,
gen_below_u128,
get_u128
),
(
usize,
gen_range_usize,
gen_range_usize_in,
gen_below_usize,
get_usize
)
);
os_random_signed_range_impls!(
(i8, u8, gen_range_i8, gen_range_i8_in, gen_below_u8, get_i8),
(
i16,
u16,
gen_range_i16,
gen_range_i16_in,
gen_below_u16,
get_i16
),
(
i32,
u32,
gen_range_i32,
gen_range_i32_in,
gen_below_u32,
get_i32
),
(
i64,
u64,
gen_range_i64,
gen_range_i64_in,
gen_below_u64,
get_i64
),
(
i128,
u128,
gen_range_i128,
gen_range_i128_in,
gen_below_u128,
get_i128
),
(
isize,
usize,
gen_range_isize,
gen_range_isize_in,
gen_below_usize,
get_isize
)
);
/// Returns a uniformly chosen byte from `set`.
///
/// # Panics
/// Panics if `set` is empty or longer than `u32::MAX`.
/// Panics if `set` is empty.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let digit = rng.pick(ez_urandom::charset::DIGITS)?;
/// assert!(digit.is_ascii_digit());
/// # Ok(())
/// # }
/// ```
pub fn pick(&mut self, set: &[u8]) -> io::Result<u8> {
assert!(!set.is_empty(), "set must not be empty");
let n = u32::try_from(set.len()).expect("set must fit in u32");
let i = usize::try_from(self.gen_range_u32(n)?)
.expect("u32 fits in usize on supported platforms");
Ok(set[i])
Ok(*self.choose(set)?)
}
/// Returns a uniformly chosen item from `items`.
///
/// The returned reference points into the caller-provided slice; no item is
/// cloned or moved.
///
/// # Panics
/// Panics if `items` is empty.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let colors = ["red", "green", "blue"];
/// let color = rng.choose(&colors)?;
/// assert!(colors.contains(color));
/// # Ok(())
/// # }
/// ```
pub fn choose<'a, T>(&mut self, items: &'a [T]) -> io::Result<&'a T> {
assert!(!items.is_empty(), "items must not be empty");
let i = self.gen_range_usize(items.len())?;
Ok(&items[i])
}
/// Returns a `String` of `len` characters drawn uniformly from `set`.
@@ -162,6 +532,18 @@ impl OsRandom {
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let token = rng.string_from(ez_urandom::charset::ALPHANUMERIC, 24)?;
/// assert_eq!(token.len(), 24);
/// assert!(token.is_ascii());
/// # Ok(())
/// # }
/// ```
pub fn string_from(&mut self, set: &[u8], len: usize) -> io::Result<String> {
assert!(set.is_ascii(), "set must contain only ASCII bytes");
let mut buf = vec![0u8; len];
@@ -170,6 +552,75 @@ impl OsRandom {
}
Ok(String::from_utf8(buf).expect("ASCII bytes are valid UTF-8"))
}
/// Returns an alphanumeric ASCII token of `len` characters.
///
/// Characters are drawn uniformly from [`charset::ALPHANUMERIC`].
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let token = rng.token(32)?;
/// assert_eq!(token.len(), 32);
/// assert!(token.bytes().all(|byte| byte.is_ascii_alphanumeric()));
/// # Ok(())
/// # }
/// ```
pub fn token(&mut self, len: usize) -> io::Result<String> {
self.string_from(charset::ALPHANUMERIC, len)
}
/// Returns a lowercase hexadecimal string of `len` characters.
///
/// Characters are drawn uniformly from [`charset::HEX_LOWER`]. The `len`
/// argument is the number of hex characters, not the number of source
/// bytes.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let hex = rng.hex(16)?;
/// assert_eq!(hex.len(), 16);
/// assert!(hex.bytes().all(|byte| byte.is_ascii_hexdigit()));
/// # Ok(())
/// # }
/// ```
pub fn hex(&mut self, len: usize) -> io::Result<String> {
self.string_from(charset::HEX_LOWER, len)
}
/// Returns a decimal digit string of `len` characters.
///
/// This is useful for PINs, short human-entered codes, and other cases
/// where only ASCII digits are accepted.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let pin = rng.pin(6)?;
/// assert_eq!(pin.len(), 6);
/// assert!(pin.bytes().all(|byte| byte.is_ascii_digit()));
/// # Ok(())
/// # }
/// ```
pub fn pin(&mut self, len: usize) -> io::Result<String> {
self.string_from(charset::DIGITS, len)
}
}
/// Forwards reads directly to the underlying `/dev/urandom` handle so an