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
This commit is contained in:
2026-04-28 19:58:27 +02:00
parent d618bd7cc9
commit 733a2508cb
2 changed files with 77 additions and 0 deletions
+63
View File
@@ -4,6 +4,16 @@
//! `/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.
//! - [`charset`] contains reusable ASCII alphabets for string generation.
use std::{
fs::File,
@@ -102,17 +112,70 @@ 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)
}
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")
}
}
/// Uniform sampling helpers.
///
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]