use std::error::Error;
use std::fmt;
use std::io;
const BACKEND: &str = "getrandom";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntropyErrorKind {
OsCode {
code: i32,
},
UnsupportedTarget,
BackendInternal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntropyError {
backend: &'static str,
kind: EntropyErrorKind,
}
impl EntropyError {
#[must_use]
pub fn backend(&self) -> &'static str {
self.backend
}
#[must_use]
pub fn kind(&self) -> EntropyErrorKind {
self.kind
}
#[must_use]
pub fn raw_os_error(&self) -> Option<i32> {
match self.kind {
EntropyErrorKind::OsCode { code } => Some(code),
EntropyErrorKind::UnsupportedTarget | EntropyErrorKind::BackendInternal => None,
}
}
#[must_use]
pub fn io_error_kind(&self) -> Option<io::ErrorKind> {
self.raw_os_error()
.map(|code| io::Error::from_raw_os_error(code).kind())
}
}
impl fmt::Display for EntropyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "could not read OS entropy from {}", self.backend)?;
match self.kind {
EntropyErrorKind::OsCode { code } => write!(
f,
": {} (OS error {code})",
io::Error::from_raw_os_error(code)
),
EntropyErrorKind::UnsupportedTarget => {
f.write_str(": this target's backend has no entropy source")
}
EntropyErrorKind::BackendInternal => {
f.write_str(": the backend failed without a usable OS error code")
}
}
}
}
impl Error for EntropyError {}
pub fn fill_bytes(buf: &mut [u8]) -> Result<(), EntropyError> {
fill_from(buf, getrandom::fill)
}
pub fn bytes<const N: usize>() -> Result<[u8; N], EntropyError> {
let mut out = [0u8; N];
fill_bytes(&mut out)?;
Ok(out)
}
fn narrow_status<T: Copy + TryInto<i32>>(status: Option<T>) -> Option<i32> {
status.and_then(|value| value.try_into().ok())
}
fn from_backend(raw_os_error: Option<i32>, unsupported: bool) -> EntropyError {
let kind = if let Some(code) = raw_os_error {
EntropyErrorKind::OsCode { code }
} else if unsupported {
EntropyErrorKind::UnsupportedTarget
} else {
EntropyErrorKind::BackendInternal
};
EntropyError {
backend: BACKEND,
kind,
}
}
fn fill_from<F>(buf: &mut [u8], source: F) -> Result<(), EntropyError>
where
F: FnOnce(&mut [u8]) -> Result<(), getrandom::Error>,
{
if buf.is_empty() {
return Ok(());
}
source(buf).map_err(|failure| {
from_backend(
narrow_status(failure.raw_os_error()),
failure == getrandom::Error::UNSUPPORTED,
)
})
}
#[cfg(test)]
mod tests {
use super::*;
const EINTR: i32 = 4;
const ENODEV: i32 = 19;
const fn assert_propagates<T: Error + Send + Sync + 'static>() {}
#[test]
fn fills_the_whole_buffer() -> Result<(), EntropyError> {
let mut buf = [0xAAu8; 64];
fill_bytes(&mut buf)?;
assert!(
buf.iter().any(|&byte_value| byte_value != 0xAA),
"buffer looks unwritten"
);
Ok(())
}
#[test]
fn successive_draws_differ() -> Result<(), EntropyError> {
let first = bytes::<32>()?;
let second = bytes::<32>()?;
assert_ne!(first, second);
Ok(())
}
#[test]
fn empty_buffer_is_a_no_op() {
assert!(fill_bytes(&mut []).is_ok());
}
#[test]
fn an_empty_buffer_reaches_no_entropy_source() {
let mut called = false;
let result = fill_from(&mut [], |_buf| {
called = true;
Err(getrandom::Error::UNSUPPORTED)
});
assert!(
result.is_ok(),
"an empty draw must succeed without a source"
);
assert!(!called, "an empty draw must not reach the entropy source");
}
#[test]
fn an_injected_os_code_round_trips_through_the_typed_error() {
assert_propagates::<EntropyError>();
for code in [1, EINTR, 13, ENODEV, 5, i32::MAX] {
let error = from_backend(Some(code), false);
assert_eq!(
error.raw_os_error(),
Some(code),
"OS error {code} must survive the typed error unchanged"
);
assert_eq!(
error.kind(),
EntropyErrorKind::OsCode { code },
"OS error {code} must be reported as the OS-code kind"
);
assert_eq!(
error.backend(),
"getrandom",
"the backend name is the stable identifier callers match on"
);
assert!(
error.to_string().contains(&code.to_string()),
"the rendered message must name OS error {code}, got {error}"
);
assert!(
format!("{error:?}").contains(&code.to_string()),
"the debug rendering of OS error {code} must name the code"
);
}
}
#[test]
fn an_injected_interruption_is_distinguished_from_a_missing_device() {
let interrupted = from_backend(Some(EINTR), false);
let missing = from_backend(Some(ENODEV), false);
assert_eq!(
interrupted.io_error_kind(),
Some(io::ErrorKind::Interrupted),
"an interruption must classify as Interrupted so a caller retries it"
);
assert_ne!(
interrupted.kind(),
missing.kind(),
"two different OS codes are two different failures"
);
assert_ne!(
interrupted.raw_os_error(),
missing.raw_os_error(),
"a caller reading the code must tell EINTR from ENODEV"
);
}
#[test]
fn the_os_classification_is_present_exactly_where_a_code_is() {
for code in [EINTR, ENODEV, 1, 13] {
let error = from_backend(Some(code), false);
assert_eq!(
error.io_error_kind().is_some(),
error.raw_os_error().is_some(),
"OS error {code} must classify exactly when it carries a code"
);
}
}
#[test]
fn a_backend_with_no_source_is_not_an_os_code() {
let error = from_backend(None, true);
assert_eq!(
error.kind(),
EntropyErrorKind::UnsupportedTarget,
"a backend with no source on this target is its own kind"
);
assert_eq!(
error.raw_os_error(),
None,
"an unsupported target has no OS code to report"
);
assert_eq!(
error.io_error_kind(),
None,
"an unsupported target has no io classification either"
);
assert!(
error.to_string().contains("no entropy source"),
"the rendered message must name the missing source, got {error}"
);
}
#[test]
fn a_backend_failure_without_a_source_is_reported_as_internal() {
let error = from_backend(None, false);
assert_eq!(
error.kind(),
EntropyErrorKind::BackendInternal,
"a failure with no code and no missing source is the internal bucket"
);
assert_eq!(error.raw_os_error(), None, "and carries no code");
}
#[test]
fn a_status_wider_than_the_declared_code_is_not_truncated() {
assert_eq!(
narrow_status(Some(4_i64)),
Some(4),
"a representable status must narrow to its own value"
);
assert_eq!(
narrow_status(Some(i64::from(i32::MAX))),
Some(i32::MAX),
"the boundary status must narrow to its own value"
);
assert_eq!(
narrow_status(Some(4_294_967_296_i64)),
None,
"a status above the declared width must be dropped, not truncated"
);
assert_eq!(
narrow_status::<i64>(None),
None,
"a backend failure with no status must narrow to no code"
);
assert_eq!(
from_backend(narrow_status(Some(4_294_967_296_i64)), false).kind(),
EntropyErrorKind::BackendInternal,
"a dropped width leaves the internal bucket rather than a wrong code"
);
}
#[test]
fn a_refused_fill_returns_the_typed_error_and_leaves_the_buffer_alone() {
let mut buf = [0xAAu8; 32];
let result = fill_from(&mut buf, |_buf| Err(getrandom::Error::UNSUPPORTED));
assert_eq!(
result,
Err(EntropyError {
backend: BACKEND,
kind: EntropyErrorKind::UnsupportedTarget,
}),
"a refusing source must surface as the typed error, not a panic or a short fill"
);
assert_eq!(
buf, [0xAAu8; 32],
"this module must not write or clear the buffer on a refusal"
);
}
#[test]
fn a_refused_fill_is_observable_through_the_typed_error_it_returns() {
let mut buf = [0u8; 8];
let observed = fill_from(&mut buf, |_buf| Err(getrandom::Error::UNSUPPORTED)).err();
assert_eq!(
observed,
Some(from_backend(None, true)),
"the real backend failure must classify exactly as the seam says"
);
assert_eq!(
observed.and_then(|error| error.raw_os_error()),
None,
"an unsupported backend reports no OS code"
);
}
#[test]
fn a_successful_fill_through_the_seam_still_writes_the_whole_buffer() {
let mut buf = [0u8; 16];
let result = fill_from(&mut buf, |buf| {
buf.fill(0x5A);
Ok(())
});
assert!(result.is_ok(), "a succeeding source must report success");
assert_eq!(
buf, [0x5Au8; 16],
"a successful fill must leave the source's bytes in the buffer"
);
}
}