initial commit

This commit is contained in:
2024-02-12 09:23:56 +01:00
commit b8ffa29969
5 changed files with 223 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+16
View File
@@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ez-urandom"
version = "0.1.0"
dependencies = [
"paste",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "ez-urandom"
version = "0.1.0"
edition = "2024"
[dependencies]
paste = "1"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
[profile.release]
debug = false
strip = true
lto = true
panic = "unwind"
codegen-units = 1
+3
View File
@@ -0,0 +1,3 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
imports_layout = "HorizontalVertical"
+181
View File
@@ -0,0 +1,181 @@
//! 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`].
use std::{
fs::File,
io::{self, Read},
mem::size_of,
};
use ::paste::paste;
const DEV_URANDOM: &str = "/dev/urandom";
/// Common character sets for use with [`OsRandom::pick`] and
/// [`OsRandom::string_from`].
pub mod charset {
const fn concat<const N: usize>(parts: &[&[u8]]) -> [u8; N] {
let mut out = [0u8; N];
let mut i = 0;
let mut p = 0;
while p < parts.len() {
let part = parts[p];
let mut j = 0;
while j < part.len() {
out[i] = part[j];
i += 1;
j += 1;
}
p += 1;
}
out
}
/// ASCII decimal digits `0``9`.
pub const DIGITS: &[u8] = b"0123456789";
/// ASCII lowercase letters `a``z`.
pub const LOWERCASE: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const UPPERCASE_ARR: [u8; 26] = {
let mut out = [0u8; 26];
let mut i = 0;
while i < 26 {
out[i] = LOWERCASE[i].to_ascii_uppercase();
i += 1;
}
out
};
/// ASCII uppercase letters `A``Z`.
pub const UPPERCASE: &[u8] = &UPPERCASE_ARR;
const ALPHABETIC_ARR: [u8; 52] = concat(&[LOWERCASE, UPPERCASE]);
/// ASCII letters: [`LOWERCASE`] followed by [`UPPERCASE`].
pub const ALPHABETIC: &[u8] = &ALPHABETIC_ARR;
const ALPHANUMERIC_ARR: [u8; 62] = concat(&[LOWERCASE, UPPERCASE, DIGITS]);
/// ASCII letters and digits: [`LOWERCASE`], [`UPPERCASE`], then [`DIGITS`].
pub const ALPHANUMERIC: &[u8] = &ALPHANUMERIC_ARR;
const HEX_LOWER_ARR: [u8; 16] = concat(&[DIGITS, LOWERCASE.split_at(6).0]);
/// Lowercase hexadecimal digits `0``9`, `a``f`.
pub const HEX_LOWER: &[u8] = &HEX_LOWER_ARR;
const HEX_UPPER_ARR: [u8; 16] = concat(&[DIGITS, UPPERCASE.split_at(6).0]);
/// Uppercase hexadecimal digits `0``9`, `A``F`.
pub const HEX_UPPER: &[u8] = &HEX_UPPER_ARR;
}
/// Handle to the OS randomness source (`/dev/urandom`).
///
/// Construct with [`OsRandom::try_new`], then call the typed `get_*` readers,
/// the uniform-sampling helpers, or use the [`Read`] impl to fill an arbitrary
/// buffer.
pub struct OsRandom {
devurandom: File,
}
macro_rules! os_random_get_integer_impls {
($($t:ty),*) => {
$(
paste! {
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
pub fn [<get_ $t>](&mut self) -> std::io::Result<$t> {
let mut buf = [0u8; size_of::<$t>()];
self.devurandom.read_exact(&mut buf)?;
Ok($t::from_ne_bytes(buf))
}
}
)*
};
}
/// Constructor and primitive integer readers.
///
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
/// `/dev/urandom` and interprets them in native-endian order, so every bit
/// pattern is equally likely.
impl OsRandom {
/// # Errors
/// Returns any I/O error produced while opening `/dev/urandom`.
pub fn try_new() -> Result<Self, io::Error> {
let devurandom = File::open(DEV_URANDOM)?;
Ok(Self { devurandom })
}
os_random_get_integer_impls!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
);
}
/// Uniform sampling helpers.
///
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]
/// so the result is 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"));
}
}
}
/// Returns a uniformly chosen byte from `set`.
///
/// # Panics
/// Panics if `set` is empty or longer than `u32::MAX`.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
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])
}
/// Returns a `String` of `len` characters drawn uniformly from `set`.
///
/// # Panics
/// Panics if `set` is empty, longer than `u32::MAX`, or contains
/// non-ASCII bytes.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
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];
for byte in &mut buf {
*byte = self.pick(set)?;
}
Ok(String::from_utf8(buf).expect("ASCII bytes are valid UTF-8"))
}
}
/// Forwards reads directly to the underlying `/dev/urandom` handle so an
/// `OsRandom` can be used anywhere an [`io::Read`] source is expected.
impl Read for OsRandom {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.devurandom.read(buf)
}
}