chore: Documentation and unit tests for rosenpass_util::io

This commit is contained in:
Karolin Varner
2024-10-23 17:21:38 +02:00
parent e4fdfcae08
commit 6aea3c0c1f

View File

@@ -1,8 +1,262 @@
//! Helpers for performing IO
//!
//! # IO Error handling helpers tutorial
//!
//! ```
//! use std::io::ErrorKind as EK;
//!
//! // It can be a bit hard to use IO errors in match statements
//!
//! fn io_placeholder() -> std::io::Result<()> {
//! Ok(())
//! }
//!
//! loop {
//! match io_placeholder() {
//! Ok(()) => break,
//! // All errors are unreachable; just here for demo purposes
//! Err(e) if e.kind() == EK::Interrupted => continue,
//! Err(e) if e.kind() == EK::WouldBlock => {
//! panic!("This particular function is not designed to be used in nonblocking code!");
//! }
//! Err(e) => Err(e)?,
//! }
//! }
//!
//! // For this reason this module contains various helper functions to make
//! // matching on error kinds a bit less repetitive. [IoResultKindHintExt::io_err_kind_hint]
//! // provides the basic functionality for use mostly with std::io::Result
//!
//! use rosenpass_util::io::IoResultKindHintExt;
//!
//! loop {
//! match io_placeholder().io_err_kind_hint() {
//! Ok(()) => break,
//! // All errors are unreachable; just here for demo purposes
//! Err((_, EK::Interrupted)) => continue,
//! Err((_, EK::WouldBlock)) => {
//! // Unreachable, just here for explanation purposes
//! panic!("This particular function is not designed to be used in nonblocking code!");
//! }
//! Err((e, _)) => Err(e)?,
//! }
//! }
//!
//! // The trait can be customized; firstly, you can use IoErrorKind
//! // for error types that can be fully represented as std::io::ErrorKind
//!
//! use rosenpass_util::io::IoErrorKind;
//!
//! #[derive(thiserror::Error, Debug, PartialEq, Eq)]
//! enum MyErrno {
//! #[error("Got interrupted")]
//! Interrupted,
//! #[error("In nonblocking mode")]
//! WouldBlock,
//! }
//!
//! impl IoErrorKind for MyErrno {
//! fn io_error_kind(&self) -> std::io::ErrorKind {
//! use MyErrno as ME;
//! match self {
//! ME::Interrupted => EK::Interrupted,
//! ME::WouldBlock => EK::WouldBlock,
//! }
//! }
//! }
//!
//! assert_eq!(
//! EK::Interrupted,
//! std::io::Error::new(EK::Interrupted, "artificially interrupted").io_error_kind()
//! );
//! assert_eq!(EK::Interrupted, MyErrno::Interrupted.io_error_kind());
//! assert_eq!(EK::WouldBlock, MyErrno::WouldBlock.io_error_kind());
//!
//! // And when an error can not fully be represented as an std::io::ErrorKind,
//! // you can still use [TryIoErrorKind]
//!
//! use rosenpass_util::io::TryIoErrorKind;
//!
//! #[derive(thiserror::Error, Debug, PartialEq, Eq)]
//! enum MyErrnoOrBlue {
//! #[error("Got interrupted")]
//! Interrupted,
//! #[error("In nonblocking mode")]
//! WouldBlock,
//! #[error("I am feeling blue")]
//! FeelingBlue,
//! }
//!
//! impl TryIoErrorKind for MyErrnoOrBlue {
//! fn try_io_error_kind(&self) -> Option<std::io::ErrorKind> {
//! use MyErrnoOrBlue as ME;
//! match self {
//! ME::Interrupted => Some(EK::Interrupted),
//! ME::WouldBlock => Some(EK::WouldBlock),
//! ME::FeelingBlue => None,
//! }
//! }
//! }
//!
//! assert_eq!(
//! Some(EK::Interrupted),
//! MyErrnoOrBlue::Interrupted.try_io_error_kind()
//! );
//! assert_eq!(
//! Some(EK::WouldBlock),
//! MyErrnoOrBlue::WouldBlock.try_io_error_kind()
//! );
//! assert_eq!(None, MyErrnoOrBlue::FeelingBlue.try_io_error_kind());
//!
//! // TryIoErrorKind is automatically implemented for all types that implement
//! // IoErrorKind
//!
//! assert_eq!(
//! Some(EK::Interrupted),
//! std::io::Error::new(EK::Interrupted, "artificially interrupted").try_io_error_kind()
//! );
//! assert_eq!(
//! Some(EK::Interrupted),
//! MyErrno::Interrupted.try_io_error_kind()
//! );
//! assert_eq!(
//! Some(EK::WouldBlock),
//! MyErrno::WouldBlock.try_io_error_kind()
//! );
//!
//! // By implementing IoErrorKind, we can automatically make use of IoResultKindHintExt<T>
//! // with our custom error type
//!
//! //use rosenpass_util::io::IoResultKindHintExt;
//!
//! assert_eq!(
//! Ok::<_, MyErrno>(42).io_err_kind_hint(),
//! Ok(42));
//! assert!(matches!(
//! Err::<(), _>(std::io::Error::new(EK::Interrupted, "artificially interrupted")).io_err_kind_hint(),
//! Err((err, EK::Interrupted)) if format!("{err:?}") == "Custom { kind: Interrupted, error: \"artificially interrupted\" }"));
//! assert_eq!(
//! Err::<(), _>(MyErrno::Interrupted).io_err_kind_hint(),
//! Err((MyErrno::Interrupted, EK::Interrupted)));
//!
//! // Correspondingly, TryIoResultKindHintExt can be used for Results with Errors
//! // that implement TryIoErrorKind
//!
//! use crate::rosenpass_util::io::TryIoResultKindHintExt;
//!
//! assert_eq!(
//! Ok::<_, MyErrnoOrBlue>(42).try_io_err_kind_hint(),
//! Ok(42));
//! assert_eq!(
//! Err::<(), _>(MyErrnoOrBlue::Interrupted).try_io_err_kind_hint(),
//! Err((MyErrnoOrBlue::Interrupted, Some(EK::Interrupted))));
//! assert_eq!(
//! Err::<(), _>(MyErrnoOrBlue::FeelingBlue).try_io_err_kind_hint(),
//! Err((MyErrnoOrBlue::FeelingBlue, None)));
//!
//! // SubstituteForIoErrorKindExt serves as a helper to handle specific ErrorKinds
//! // using a method chaining style. It works on anything that implements TryIoErrorKind.
//!
//! use rosenpass_util::io::SubstituteForIoErrorKindExt;
//!
//! assert_eq!(Ok(42),
//! Err(MyErrnoOrBlue::Interrupted)
//! .substitute_for_ioerr_kind_with(EK::Interrupted, || 42));
//!
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock),
//! Err(MyErrnoOrBlue::WouldBlock)
//! .substitute_for_ioerr_kind_with(EK::Interrupted, || 42));
//!
//! // The other functions in SubstituteForIoErrorKindExt are mostly just wrappers,
//! // getting the same job done with minor convenience
//!
//! // Plain Ok() value instead of function
//! assert_eq!(Ok(42),
//! Err(MyErrnoOrBlue::Interrupted)
//! .substitute_for_ioerr_kind(EK::Interrupted, 42));
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock),
//! Err(MyErrnoOrBlue::WouldBlock)
//! .substitute_for_ioerr_kind(EK::Interrupted, 42));
//!
//! // For specific errors
//! assert_eq!(Ok(42),
//! Err(MyErrnoOrBlue::Interrupted)
//! .substitute_for_ioerr_interrupted_with(|| 42)
//! .substitute_for_ioerr_wouldblock_with(|| 23));
//! assert_eq!(Ok(23),
//! Err(MyErrnoOrBlue::WouldBlock)
//! .substitute_for_ioerr_interrupted_with(|| 42)
//! .substitute_for_ioerr_wouldblock_with(|| 23));
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue),
//! Err(MyErrnoOrBlue::FeelingBlue)
//! .substitute_for_ioerr_interrupted_with(|| 42)
//! .substitute_for_ioerr_wouldblock_with(|| 23));
//!
//! // And for specific errors without the function call
//! assert_eq!(Ok(42),
//! Err(MyErrnoOrBlue::Interrupted)
//! .substitute_for_ioerr_interrupted(42)
//! .substitute_for_ioerr_wouldblock(23));
//! assert_eq!(Ok(23),
//! Err(MyErrnoOrBlue::WouldBlock)
//! .substitute_for_ioerr_interrupted(42)
//! .substitute_for_ioerr_wouldblock(23));
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue),
//! Err(MyErrnoOrBlue::FeelingBlue)
//! .substitute_for_ioerr_interrupted(42)
//! .substitute_for_ioerr_wouldblock(23));
//!
//! // handle_interrupted automates the process of handling ErrorKind::Interrupted
//! // in cases where the action should simply be rerun; it can handle any error type
//! // that implements TryIoErrorKind. It lets other errors and Ok(_) pass through.
//!
//! use rosenpass_util::io::handle_interrupted;
//!
//! let mut ctr = 0u32;
//! let mut simulate_io = || -> Result<u32, MyErrnoOrBlue> {
//! let r = match ctr % 6 {
//! 1 => Ok(42),
//! 3 => Err(MyErrnoOrBlue::FeelingBlue),
//! 5 => Err(MyErrnoOrBlue::WouldBlock),
//! _ => Err(MyErrnoOrBlue::Interrupted),
//! };
//! ctr += 1;
//! r
//! };
//!
//! assert_eq!(Ok(Some(42)), handle_interrupted(&mut simulate_io));
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue), handle_interrupted(&mut simulate_io));
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock), handle_interrupted(&mut simulate_io));
//! // never returns None
//!
//! // nonblocking_handle_io_errors performs the same job, except that
//! // WouldBlock is substituted with Ok(None)
//!
//! use rosenpass_util::io::nonblocking_handle_io_errors;
//!
//! assert_eq!(Ok(Some(42)), nonblocking_handle_io_errors(&mut simulate_io));
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue), nonblocking_handle_io_errors(&mut simulate_io));
//! assert_eq!(Ok(None), nonblocking_handle_io_errors(&mut simulate_io));
//!
//! Ok::<_, anyhow::Error>(())
//! ```
use std::{borrow::Borrow, io};
use anyhow::ensure;
use zerocopy::AsBytes;
/// Generic trait for accessing [std::io::Error::kind]
///
/// # Examples
///
/// See [tutorial in the module](self).
pub trait IoErrorKind {
/// Conversion to [std::io::Error::kind]
///
/// # Examples
///
/// See [tutorial in the module](self).
fn io_error_kind(&self) -> io::ErrorKind;
}
@@ -12,7 +266,17 @@ impl<T: Borrow<io::Error>> IoErrorKind for T {
}
}
/// Generic trait for accessing [std::io::Error::kind] where it may not be present
///
/// # Examples
///
/// See [tutorial in the module](self).
pub trait TryIoErrorKind {
/// Conversion to [std::io::Error::kind] where it may not be present
///
/// # Examples
///
/// See [tutorial in the module](self).
fn try_io_error_kind(&self) -> Option<io::ErrorKind>;
}
@@ -22,8 +286,19 @@ impl<T: IoErrorKind> TryIoErrorKind for T {
}
}
/// Helper for accessing [std::io::Error::kind] in Results
///
/// # Examples
///
/// See [tutorial in the module](self).
pub trait IoResultKindHintExt<T>: Sized {
// Error trait including the ErrorKind hint
type Error;
/// Helper for accessing [std::io::Error::kind] in Results
///
/// # Examples
///
/// See [tutorial in the module](self).
fn io_err_kind_hint(self) -> Result<T, (Self::Error, io::ErrorKind)>;
}
@@ -37,8 +312,19 @@ impl<T, E: IoErrorKind> IoResultKindHintExt<T> for Result<T, E> {
}
}
/// Helper for accessing [std::io::Error::kind] in Results where it may not be present
///
/// # Examples
///
/// See [tutorial in the module](self).
pub trait TryIoResultKindHintExt<T>: Sized {
// Error trait including the ErrorKind hint
type Error;
/// Helper for accessing [std::io::Error::kind] in Results where it may not be present
///
/// # Examples
///
/// See [tutorial in the module](self).
fn try_io_err_kind_hint(self) -> Result<T, (Self::Error, Option<io::ErrorKind>)>;
}
@@ -52,17 +338,41 @@ impl<T, E: TryIoErrorKind> TryIoResultKindHintExt<T> for Result<T, E> {
}
}
/// Helper for working with IO results using a method chaining style
///
/// # Examples
///
/// See [tutorial in the module](self).
pub trait SubstituteForIoErrorKindExt<T>: Sized {
/// Error type produced by methods in this trait
type Error;
/// Substitute errors with a certain [std::io::ErrorKind] by a value produced by a function
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_kind_with<F: FnOnce() -> T>(
self,
kind: io::ErrorKind,
f: F,
) -> Result<T, Self::Error>;
/// Substitute errors with a certain [std::io::ErrorKind] by a value
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_kind(self, kind: io::ErrorKind, v: T) -> Result<T, Self::Error> {
self.substitute_for_ioerr_kind_with(kind, || v)
}
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::Interrupted] by a value
/// produced by a function
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_interrupted_with<F: FnOnce() -> T>(
self,
f: F,
@@ -70,10 +380,21 @@ pub trait SubstituteForIoErrorKindExt<T>: Sized {
self.substitute_for_ioerr_kind_with(io::ErrorKind::Interrupted, f)
}
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::Interrupted] by a value
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_interrupted(self, v: T) -> Result<T, Self::Error> {
self.substitute_for_ioerr_interrupted_with(|| v)
}
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::WouldBlock] by a value
/// produced by a function
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_wouldblock_with<F: FnOnce() -> T>(
self,
f: F,
@@ -81,6 +402,11 @@ pub trait SubstituteForIoErrorKindExt<T>: Sized {
self.substitute_for_ioerr_kind_with(io::ErrorKind::WouldBlock, f)
}
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::WouldBlock] by a value
///
/// # Examples
///
/// See [tutorial in the module](self).
fn substitute_for_ioerr_wouldblock(self, v: T) -> Result<T, Self::Error> {
self.substitute_for_ioerr_wouldblock_with(|| v)
}
@@ -107,6 +433,10 @@ impl<T, E: TryIoErrorKind> SubstituteForIoErrorKindExt<T> for Result<T, E> {
/// - If there is no error (i.e. on `Ok(r)`), the function will return `Ok(Some(r))`
/// - `Interrupted` is handled internally, by retrying the IO operation
/// - Other errors are returned as is
///
/// # Examples
///
/// See [tutorial in the module](self).
pub fn handle_interrupted<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
where
E: TryIoErrorKind,
@@ -128,6 +458,10 @@ where
/// - `Interrupted` is handled internally, by retrying the IO operation
/// - `WouldBlock` is handled by returning `Ok(None)`,
/// - Other errors are returned as is
///
/// # Examples
///
/// See [tutorial in the module](self).
pub fn nonblocking_handle_io_errors<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
where
E: TryIoErrorKind,
@@ -144,6 +478,7 @@ where
}
}
/// [std:io::Read] extension trait for call with [nonblocking_handle_io_errors] applied
pub trait ReadNonblockingWithBoringErrorsHandledExt {
/// Convenience wrapper using [nonblocking_handle_io_errors] with [std::io::Read]
fn read_nonblocking_with_boring_errors_handled(
@@ -161,7 +496,27 @@ impl<T: io::Read> ReadNonblockingWithBoringErrorsHandledExt for T {
}
}
/// Extension trait for [std::io::Read] providing the ability to read
/// a buffer exactly
pub trait ReadExt {
/// Version of [std::io::Read::read_exact] that throws if there
/// is extra data in the stream to be read
///
/// # Examples
///
/// ```
/// use rosenpass_util::io::ReadExt;
///
/// let mut buf = [0u8; 4];
///
/// // Over or underlong buffer yields error
/// assert!(b"12345".as_slice().read_exact_til_end(&mut buf).is_err());
/// assert!(b"123".as_slice().read_exact_til_end(&mut buf).is_err());
///
/// // Buffer of precisely the right length leads to successful read
/// assert!(b"1234".as_slice().read_exact_til_end(&mut buf).is_ok());
/// assert_eq!(b"1234", &buf);
/// ```
fn read_exact_til_end(&mut self, buf: &mut [u8]) -> anyhow::Result<()>;
}