Compare commits
8 Commits
d618bd7cc9
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
08038e685b
|
|||
|
852c2551fd
|
|||
|
631dc1938c
|
|||
|
808e0482c3
|
|||
|
53fc84a270
|
|||
|
0365ab86d1
|
|||
|
b8f9fa9b1b
|
|||
|
733a2508cb
|
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ez-urandom"
|
name = "ez-urandom"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"paste",
|
"paste",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ez-urandom"
|
name = "ez-urandom"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "MIT-0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
paste = "1"
|
paste = "1"
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
# Ergonomy ideas
|
# Ergonomy ideas
|
||||||
|
|
||||||
- `Default` / `thread_local` global: `ez_urandom::random_u64()` without
|
- Implemented:
|
||||||
constructing or threading an `OsRandom`.
|
- `Default` for infallible convenience construction.
|
||||||
- `gen_range` for all int widths (currently only `u32`) plus a `Range` /
|
- `fill_bytes(&mut [u8])` convenience so callers don't need to import
|
||||||
`RangeInclusive` overload.
|
`io::Read`.
|
||||||
- `fill_bytes(&mut [u8])` convenience so callers don't need to import
|
- `gen_range_<int>` for all primitive integer widths.
|
||||||
`io::Read`.
|
- `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.
|
- `bool()` / `f64()` (unit interval) helpers.
|
||||||
- `shuffle(&mut [T])` and `choose<T>(&[T]) -> &T` — generic, not just bytes.
|
|
||||||
- Accept a `&str` alphabet directly (not just `&[u8]`) and drop the ASCII
|
- Accept a `&str` alphabet directly (not just `&[u8]`) and drop the ASCII
|
||||||
assert by iterating `chars()`.
|
assert by iterating `chars()`.
|
||||||
- Loosen `&mut self` to `&self` via interior buffering, or impl `Clone`, so
|
|
||||||
multiple call sites don't fight the borrow checker.
|
|
||||||
- Builder / presets: `OsRandom::password(len)`, `::hex(len)`, `::token(len)`.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
+8
-3
@@ -11,14 +11,19 @@ fn main() -> std::io::Result<()> {
|
|||||||
println!("i32 : {}", rng.get_i32()?);
|
println!("i32 : {}", rng.get_i32()?);
|
||||||
|
|
||||||
println!("dice 1-6 : {}", rng.gen_range_u32(6)? + 1);
|
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.string_from(charset::ALPHANUMERIC, 24)?;
|
let token = rng.token(24)?;
|
||||||
println!("token : {token}");
|
println!("token : {token}");
|
||||||
|
|
||||||
let hex = rng.string_from(charset::HEX_LOWER, 32)?;
|
let hex = rng.hex(32)?;
|
||||||
println!("hex : {hex}");
|
println!("hex : {hex}");
|
||||||
|
|
||||||
let pin = rng.string_from(charset::DIGITS, 6)?;
|
let custom = rng.string_from(charset::ALPHANUMERIC, 12)?;
|
||||||
|
println!("custom : {custom}");
|
||||||
|
|
||||||
|
let pin = rng.pin(6)?;
|
||||||
println!("pin : {pin}");
|
println!("pin : {pin}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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
@@ -1,14 +1,30 @@
|
|||||||
|
// SPDX-License-Identifier: MIT-0
|
||||||
|
|
||||||
//! Easy access to operating-system randomness via `/dev/urandom`.
|
//! Easy access to operating-system randomness via `/dev/urandom`.
|
||||||
//!
|
//!
|
||||||
//! The crate exposes [`OsRandom`], a small wrapper around an open handle to
|
//! The crate exposes [`OsRandom`], a small wrapper around an open handle to
|
||||||
//! `/dev/urandom` with helpers for reading primitive integers, sampling
|
//! `/dev/urandom` with helpers for reading primitive integers, sampling
|
||||||
//! uniformly without modulo bias, and generating random ASCII strings from a
|
//! uniformly without modulo bias, and generating random ASCII strings from a
|
||||||
//! caller-supplied alphabet. Common alphabets live in [`charset`].
|
//! 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::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
mem::size_of,
|
mem::size_of,
|
||||||
|
ops::{Bound, RangeBounds},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::paste::paste;
|
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.
|
/// Constructor and primitive integer readers.
|
||||||
///
|
///
|
||||||
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
|
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
|
||||||
@@ -102,56 +291,237 @@ macro_rules! os_random_get_integer_impls {
|
|||||||
impl OsRandom {
|
impl OsRandom {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns any I/O error produced while opening `/dev/urandom`.
|
/// 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> {
|
pub fn try_new() -> Result<Self, io::Error> {
|
||||||
let devurandom = File::open(DEV_URANDOM)?;
|
let devurandom = File::open(DEV_URANDOM)?;
|
||||||
|
|
||||||
Ok(Self { devurandom })
|
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!(
|
os_random_get_integer_impls!(
|
||||||
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
|
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.
|
/// Uniform sampling helpers.
|
||||||
///
|
///
|
||||||
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]
|
/// Range methods use rejection sampling on top of the matching primitive
|
||||||
/// so the result is exactly uniform — no modulo bias.
|
/// integer reader, so bounded results are exactly uniform — no modulo bias.
|
||||||
impl OsRandom {
|
impl OsRandom {
|
||||||
/// Returns a uniformly distributed integer in `0..n`.
|
os_random_unsigned_range_impls!(
|
||||||
///
|
(u8, gen_range_u8, gen_range_u8_in, gen_below_u8, get_u8),
|
||||||
/// # Panics
|
(u16, gen_range_u16, gen_range_u16_in, gen_below_u16, get_u16),
|
||||||
/// Panics if `n == 0`.
|
(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),
|
||||||
/// # Errors
|
(
|
||||||
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
u128,
|
||||||
pub fn gen_range_u32(&mut self, n: u32) -> io::Result<u32> {
|
gen_range_u128,
|
||||||
assert!(n > 0, "n must be greater than zero");
|
gen_range_u128_in,
|
||||||
let n64 = u64::from(n);
|
gen_below_u128,
|
||||||
// Largest multiple of n that fits in 2^32. Values at or above the
|
get_u128
|
||||||
// cutoff would skew the modulo, so we discard and resample.
|
),
|
||||||
let cutoff = (1u64 << 32) - ((1u64 << 32) % n64);
|
(
|
||||||
loop {
|
usize,
|
||||||
let v = u64::from(self.get_u32()?);
|
gen_range_usize,
|
||||||
if v < cutoff {
|
gen_range_usize_in,
|
||||||
return Ok(u32::try_from(v % n64).expect("v % n is bounded by n which fits in u32"));
|
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`.
|
/// Returns a uniformly chosen byte from `set`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if `set` is empty or longer than `u32::MAX`.
|
/// Panics if `set` is empty.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
/// 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> {
|
pub fn pick(&mut self, set: &[u8]) -> io::Result<u8> {
|
||||||
assert!(!set.is_empty(), "set must not be empty");
|
Ok(*self.choose(set)?)
|
||||||
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");
|
/// Returns a uniformly chosen item from `items`.
|
||||||
Ok(set[i])
|
///
|
||||||
|
/// 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`.
|
/// Returns a `String` of `len` characters drawn uniformly from `set`.
|
||||||
@@ -162,6 +532,18 @@ impl OsRandom {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
/// 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> {
|
pub fn string_from(&mut self, set: &[u8], len: usize) -> io::Result<String> {
|
||||||
assert!(set.is_ascii(), "set must contain only ASCII bytes");
|
assert!(set.is_ascii(), "set must contain only ASCII bytes");
|
||||||
let mut buf = vec![0u8; len];
|
let mut buf = vec![0u8; len];
|
||||||
@@ -170,6 +552,75 @@ impl OsRandom {
|
|||||||
}
|
}
|
||||||
Ok(String::from_utf8(buf).expect("ASCII bytes are valid UTF-8"))
|
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
|
/// Forwards reads directly to the underlying `/dev/urandom` handle so an
|
||||||
|
|||||||
Reference in New Issue
Block a user