Unit tests & api doc

Merge pull request #458 from rosenpass/dev/karo/docs_and_unit_tests
This commit is contained in:
Karolin Varner
2024-10-24 17:25:56 +02:00
committed by GitHub
5 changed files with 1066 additions and 144 deletions

View File

@@ -2,6 +2,17 @@
#[macro_export]
/// A simple for loop to repeat a $body a number of times
///
/// # Examples
///
/// ```
/// use rosenpass_util::repeat;
/// let mut sum = 0;
/// repeat!(10, {
/// sum += 1;
/// });
/// assert_eq!(sum, 10);
/// ```
macro_rules! repeat {
($times:expr, $body:expr) => {
for _ in 0..($times) {
@@ -12,6 +23,23 @@ macro_rules! repeat {
#[macro_export]
/// Return unless the condition $cond is true, with return value $val, if given.
///
/// # Examples
///
/// ```
/// use rosenpass_util::return_unless;
/// fn test_fn() -> i32 {
/// return_unless!(true, 1);
/// 0
/// }
/// assert_eq!(test_fn(), 0);
/// fn test_fn2() -> i32 {
/// return_unless!(false, 1);
/// 0
/// }
/// assert_eq!(test_fn2(), 1);
/// ```
macro_rules! return_unless {
($cond:expr) => {
if !($cond) {
@@ -27,6 +55,23 @@ macro_rules! return_unless {
#[macro_export]
/// Return if the condition $cond is true, with return value $val, if given.
///
/// # Examples
///
/// ```
/// use rosenpass_util::return_if;
/// fn test_fn() -> i32 {
/// return_if!(true, 1);
/// 0
/// }
/// assert_eq!(test_fn(), 1);
/// fn test_fn2() -> i32 {
/// return_if!(false, 1);
/// 0
/// }
/// assert_eq!(test_fn2(), 0);
/// ```
macro_rules! return_if {
($cond:expr) => {
if $cond {
@@ -42,6 +87,27 @@ macro_rules! return_if {
#[macro_export]
/// Break unless the condition is true, from the loop with label $val, if given.
///
/// # Examples
///
/// ```
/// use rosenpass_util::break_if;
/// let mut sum = 0;
/// for i in 0..10 {
/// break_if!(i == 5);
/// sum += 1;
/// }
/// assert_eq!(sum, 5);
/// let mut sum = 0;
/// 'one: for _ in 0..10 {
/// for j in 0..20 {
/// break_if!(j == 5, 'one);
/// sum += 1;
/// }
/// }
/// assert_eq!(sum, 5);
/// ```
macro_rules! break_if {
($cond:expr) => {
if $cond {
@@ -57,6 +123,25 @@ macro_rules! break_if {
#[macro_export]
/// Continue if the condition is true, in the loop with label $val, if given.
///
/// # Examples
///
/// ```
/// use rosenpass_util::continue_if;
/// let mut sum = 0;
/// for i in 0..10 {
/// continue_if!(i == 5);
/// sum += 1;
/// }
/// assert_eq!(sum, 9);
/// let mut sum = 0;
/// 'one: for i in 0..10 {
/// continue_if!(i == 5, 'one);
/// sum += 1;
/// }
/// assert_eq!(sum, 9);
/// ```
macro_rules! continue_if {
($cond:expr) => {
if $cond {
@@ -69,81 +154,3 @@ macro_rules! continue_if {
}
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_repeat() {
let mut sum = 0;
repeat!(10, {
sum += 1;
});
assert_eq!(sum, 10);
}
#[test]
fn test_return_unless() {
fn test_fn() -> i32 {
return_unless!(true, 1);
0
}
assert_eq!(test_fn(), 0);
fn test_fn2() -> i32 {
return_unless!(false, 1);
0
}
assert_eq!(test_fn2(), 1);
}
#[test]
fn test_return_if() {
fn test_fn() -> i32 {
return_if!(true, 1);
0
}
assert_eq!(test_fn(), 1);
fn test_fn2() -> i32 {
return_if!(false, 1);
0
}
assert_eq!(test_fn2(), 0);
}
#[test]
fn test_break_if() {
let mut sum = 0;
for i in 0..10 {
break_if!(i == 5);
sum += 1;
}
assert_eq!(sum, 5);
let mut sum = 0;
'one: for _ in 0..10 {
for j in 0..20 {
break_if!(j == 5, 'one);
sum += 1;
}
}
assert_eq!(sum, 5);
}
#[test]
fn test_continue_if() {
let mut sum = 0;
for i in 0..10 {
continue_if!(i == 5);
sum += 1;
}
assert_eq!(sum, 9);
let mut sum = 0;
'one: for i in 0..10 {
continue_if!(i == 5, 'one);
sum += 1;
}
assert_eq!(sum, 9);
}
}

View File

@@ -1,3 +1,5 @@
//! Utilities for working with file descriptors
use anyhow::bail;
use rustix::{
fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
@@ -8,10 +10,37 @@ use crate::{mem::Forgetting, result::OkExt};
/// Prepare a file descriptor for use in Rust code.
///
/// Checks if the file descriptor is valid and duplicates it to a new file descriptor.
/// The old file descriptor is masked to avoid potential use after free (on file descriptor)
/// in case the given file descriptor is still used somewhere
///
/// # Panic and safety
///
/// Will panic if the given file descriptor is negative of or larger than
/// the file descriptor numbers permitted by the operating system.
///
/// # Examples
///
/// ```
/// use std::io::Write;
/// use std::os::fd::{IntoRawFd, AsRawFd};
/// use tempfile::tempdir;
/// use rosenpass_util::fd::{claim_fd, FdIo};
///
/// // Open a file and turn it into a raw file descriptor
/// let orig = tempfile::tempfile()?.into_raw_fd();
///
/// // Reclaim that file and ready it for reading
/// let mut claimed = FdIo(claim_fd(orig)?);
///
/// // A different file descriptor is used
/// assert!(orig.as_raw_fd() != claimed.0.as_raw_fd());
///
/// // Write some data
/// claimed.write_all(b"Hello, World!")?;
///
/// Ok::<(), std::io::Error>(())
/// ```
pub fn claim_fd(fd: RawFd) -> rustix::io::Result<OwnedFd> {
let new = clone_fd_cloexec(unsafe { BorrowedFd::borrow_raw(fd) })?;
mask_fd(fd)?;
@@ -22,7 +51,32 @@ pub fn claim_fd(fd: RawFd) -> rustix::io::Result<OwnedFd> {
///
/// Checks if the file descriptor is valid.
///
/// Unlike [claim_fd], this will reuse the same file descriptor identifier instead of masking it.
/// Unlike [claim_fd], this will try to reuse the same file descriptor identifier instead of masking it.
///
/// # Panic and safety
///
/// Will panic if the given file descriptor is negative of or larger than
/// the file descriptor numbers permitted by the operating system.
///
/// # Examples
///
/// ```
/// use std::io::Write;
/// use std::os::fd::IntoRawFd;
/// use tempfile::tempdir;
/// use rosenpass_util::fd::{claim_fd_inplace, FdIo};
///
/// // Open a file and turn it into a raw file descriptor
/// let fd = tempfile::tempfile()?.into_raw_fd();
///
/// // Reclaim that file and ready it for reading
/// let mut fd = FdIo(claim_fd_inplace(fd)?);
///
/// // Write some data
/// fd.write_all(b"Hello, World!")?;
///
/// Ok::<(), std::io::Error>(())
/// ```
pub fn claim_fd_inplace(fd: RawFd) -> rustix::io::Result<OwnedFd> {
let mut new = unsafe { OwnedFd::from_raw_fd(fd) };
let tmp = clone_fd_cloexec(&new)?;
@@ -30,6 +84,13 @@ pub fn claim_fd_inplace(fd: RawFd) -> rustix::io::Result<OwnedFd> {
Ok(new)
}
/// Will close the given file descriptor and overwrite
/// it with a masking file descriptor (see [open_nullfd]) to prevent accidental reuse.
///
/// # Panic and safety
///
/// Will panic if the given file descriptor is negative of or larger than
/// the file descriptor numbers permitted by the operating system.
pub fn mask_fd(fd: RawFd) -> rustix::io::Result<()> {
// Safety: because the OwnedFd resulting from OwnedFd::from_raw_fd is wrapped in a Forgetting,
// it never gets dropped, meaning that fd is never closed and thus outlives the OwnedFd
@@ -37,11 +98,17 @@ pub fn mask_fd(fd: RawFd) -> rustix::io::Result<()> {
clone_fd_to_cloexec(open_nullfd()?, &mut owned)
}
/// Duplicate a file descriptor, setting the close on exec flag
pub fn clone_fd_cloexec<Fd: AsFd>(fd: Fd) -> rustix::io::Result<OwnedFd> {
const MINFD: RawFd = 3; // Avoid stdin, stdout, and stderr
/// Avoid stdin, stdout, and stderr
const MINFD: RawFd = 3;
fcntl_dupfd_cloexec(fd, MINFD)
}
/// Duplicate a file descriptor, setting the close on exec flag.
///
/// This is slightly different from [clone_fd_cloexec], as this function supports specifying an
/// explicit destination file descriptor.
#[cfg(target_os = "linux")]
pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::Result<()> {
use rustix::io::{dup3, DupFlags};
@@ -56,7 +123,21 @@ pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::R
}
/// Open a "blocked" file descriptor. I.e. a file descriptor that is neither meant for reading nor
/// writing
/// writing.
///
/// # Safety
///
/// The behavior of the file descriptor when being written to or from is undefined.
///
/// # Examples
///
/// ```
/// use std::{fs::File, io::Write, os::fd::IntoRawFd};
/// use rustix::fd::FromRawFd;
/// use rosenpass_util::fd::open_nullfd;
///
/// let nullfd = open_nullfd().unwrap();
/// ```
pub fn open_nullfd() -> rustix::io::Result<OwnedFd> {
use rustix::fs::{open, Mode, OFlags};
// TODO: Add tests showing that this will throw errors on use
@@ -64,8 +145,24 @@ pub fn open_nullfd() -> rustix::io::Result<OwnedFd> {
}
/// Convert low level errors into std::io::Error
///
/// # Examples
///
/// ```
/// use std::io::ErrorKind as EK;
/// use rustix::io::Errno;
/// use rosenpass_util::fd::IntoStdioErr;
///
/// let e = Errno::INTR.into_stdio_err();
/// assert!(matches!(e.kind(), EK::Interrupted));
///
/// let r : rustix::io::Result<()> = Err(Errno::INTR);
/// assert!(matches!(r, Err(e) if e.kind() == EK::Interrupted));
/// ```
pub trait IntoStdioErr {
/// Target type produced (e.g. std::io:Error or std::io::Result depending on context
type Target;
/// Convert low level errors to
fn into_stdio_err(self) -> Self::Target;
}
@@ -86,6 +183,10 @@ impl<T> IntoStdioErr for rustix::io::Result<T> {
}
/// Read and write directly from a file descriptor
///
/// # Examples
///
/// See [claim_fd].
pub struct FdIo<Fd: AsFd>(pub Fd);
impl<Fd: AsFd> std::io::Read for FdIo<Fd> {
@@ -104,7 +205,17 @@ impl<Fd: AsFd> std::io::Write for FdIo<Fd> {
}
}
/// Helpers for accessing stat(2) information
pub trait StatExt {
/// Check if the file is a socket
///
/// # Examples
///
/// ```
/// use rosenpass_util::fd::StatExt;
/// assert!(rustix::fs::stat("/")?.is_socket() == false);
/// Ok::<(), rustix::io::Errno>(())
/// ````
fn is_socket(&self) -> bool;
}
@@ -116,8 +227,21 @@ impl StatExt for rustix::fs::Stat {
}
}
/// Helpers for accessing stat(2) information on an open file descriptor
pub trait TryStatExt {
/// Error type returned by operations
type Error;
/// Check if the file is a socket
///
/// # Examples
///
/// ```
/// use rosenpass_util::fd::TryStatExt;
/// let fd = rustix::fs::open("/", rustix::fs::OFlags::empty(), rustix::fs::Mode::empty())?;
/// assert!(matches!(fd.is_socket(), Ok(false)));
/// Ok::<(), rustix::io::Errno>(())
/// ````
fn is_socket(&self) -> Result<bool, Self::Error>;
}
@@ -132,13 +256,18 @@ where
}
}
/// Determine the type of socket a file descriptor represents
pub trait GetSocketType {
/// Error type returned by operations in this trait
type Error;
/// Look up the socket; see [rustix::net::sockopt::get_socket_type]
fn socket_type(&self) -> Result<rustix::net::SocketType, Self::Error>;
/// Checks if the socket is a datagram socket
fn is_datagram_socket(&self) -> Result<bool, Self::Error> {
use rustix::net::SocketType;
matches!(self.socket_type()?, SocketType::DGRAM).ok()
}
/// Checks if the socket is a stream socket
fn is_stream_socket(&self) -> Result<bool, Self::Error> {
Ok(self.socket_type()? == rustix::net::SocketType::STREAM)
}
@@ -155,13 +284,18 @@ where
}
}
/// Distinguish different socket address familys; e.g. IP and unix sockets
#[cfg(target_os = "linux")]
pub trait GetSocketDomain {
/// Error type returned by operations in this trait
type Error;
/// Retrieve the socket domain (address family)
fn socket_domain(&self) -> Result<rustix::net::AddressFamily, Self::Error>;
/// Alias for [socket_domain]
fn socket_address_family(&self) -> Result<rustix::net::AddressFamily, Self::Error> {
self.socket_domain()
}
/// Check if the underlying socket is a unix domain socket
fn is_unix_socket(&self) -> Result<bool, Self::Error> {
Ok(self.socket_domain()? == rustix::net::AddressFamily::UNIX)
}
@@ -179,10 +313,14 @@ where
}
}
/// Distinguish different types of unix sockets
#[cfg(target_os = "linux")]
pub trait GetUnixSocketType {
/// Error type returned by operations in this trait
type Error;
/// Check if the socket is a unix stream socket
fn is_unix_stream_socket(&self) -> Result<bool, Self::Error>;
/// Returns Ok(()) only if the underlying socket is a unix stream socket
fn demand_unix_stream_socket(&self) -> anyhow::Result<()>;
}
@@ -210,14 +348,18 @@ where
}
#[cfg(target_os = "linux")]
/// Distinguish between different network socket protocols (e.g. tcp, udp)
pub trait GetSocketProtocol {
/// Retrieve the socket protocol
fn socket_protocol(&self) -> Result<Option<rustix::net::Protocol>, rustix::io::Errno>;
/// Check if the socket is a udp socket
fn is_udp_socket(&self) -> Result<bool, rustix::io::Errno> {
self.socket_protocol()?
.map(|p| p == rustix::net::ipproto::UDP)
.unwrap_or(false)
.ok()
}
/// Return Ok(()) only if the socket is a udp socket
fn demand_udp_socket(&self) -> anyhow::Result<()> {
match self.socket_protocol() {
Ok(Some(rustix::net::ipproto::UDP)) => Ok(()),
@@ -243,58 +385,50 @@ where
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{read_to_string, File};
use std::io::{Read, Write};
use std::os::fd::IntoRawFd;
use tempfile::tempdir;
#[test]
fn test_claim_fd() {
let tmp_dir = tempdir().unwrap();
let path = tmp_dir.path().join("test");
let file = File::create(path.clone()).unwrap();
let fd: RawFd = file.into_raw_fd();
let owned_fd = claim_fd(fd).unwrap();
let mut file = unsafe { File::from_raw_fd(owned_fd.into_raw_fd()) };
file.write_all(b"Hello, World!").unwrap();
let message = read_to_string(path).unwrap();
assert_eq!(message, "Hello, World!");
}
#[test]
#[should_panic(expected = "fd != u32::MAX as RawFd")]
fn test_claim_fd_invalid_neg() {
let fd: RawFd = -1;
let _ = claim_fd(fd);
let _ = claim_fd(-1);
}
#[test]
#[should_panic(expected = "fd != u32::MAX as RawFd")]
fn test_claim_fd_invalid_max() {
let fd: RawFd = i64::MAX as RawFd;
let _ = claim_fd(fd);
let _ = claim_fd(i64::MAX as RawFd);
}
#[test]
fn test_open_nullfd_write() {
let nullfd = open_nullfd().unwrap();
let mut file = unsafe { File::from_raw_fd(nullfd.into_raw_fd()) };
let res = file.write_all(b"Hello, World!");
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
"Bad file descriptor (os error 9)"
);
#[should_panic]
fn test_claim_fd_inplace_invalid_neg() {
let _ = claim_fd_inplace(-1);
}
#[test]
fn test_open_nullfd_read() {
let nullfd = open_nullfd().unwrap();
let mut file = unsafe { File::from_raw_fd(nullfd.into_raw_fd()) };
let mut buffer = [0; 10];
let res = file.read_exact(&mut buffer);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "failed to fill whole buffer");
#[should_panic]
fn test_claim_fd_inplace_invalid_max() {
let _ = claim_fd_inplace(i64::MAX as RawFd);
}
#[test]
#[should_panic]
fn test_mask_fd_invalid_neg() {
let _ = mask_fd(-1);
}
#[test]
#[should_panic]
fn test_mask_fd_invalid_max() {
let _ = mask_fd(i64::MAX as RawFd);
}
#[test]
fn test_open_nullfd() -> anyhow::Result<()> {
let mut file = FdIo(open_nullfd()?);
let mut buf = [0; 10];
assert!(matches!(file.read(&mut buf), Ok(0) | Err(_)));
assert!(matches!(file.write(&buf), Err(_)));
Ok(())
}
}

View File

@@ -1,15 +1,45 @@
//! Helpers for working with files
use anyhow::ensure;
use std::fs::File;
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
use std::{fs::OpenOptions, path::Path};
/// Level of secrecy applied for a file
pub enum Visibility {
/// The file might contain a public key
Public,
/// The file might contain a secret key
Secret,
}
/// Open a file writable
/// Open a file writeably, truncating the file.
///
/// Sensible default permissions are chosen based on the value of `visibility`
///
/// # Examples
///
/// ```
/// use std::io::{Write, Read};
/// use tempfile::tempdir;
/// use rosenpass_util::file::{fopen_r, fopen_w, Visibility};
///
/// const CONTENTS : &[u8] = b"Hello World";
///
/// let dir = tempdir()?;
/// let path = dir.path().join("secret_key");
///
/// let mut f = fopen_w(&path, Visibility::Secret)?;
/// f.write_all(CONTENTS)?;
///
/// let mut f = fopen_r(&path)?;
/// let mut b = Vec::new();
/// f.read_to_end(&mut b)?;
/// assert_eq!(CONTENTS, &b);
///
/// Ok::<(), std::io::Error>(())
/// ```
pub fn fopen_w<P: AsRef<Path>>(path: P, visibility: Visibility) -> std::io::Result<File> {
let mut options = OpenOptions::new();
options.create(true).write(true).read(false).truncate(true);
@@ -19,7 +49,12 @@ pub fn fopen_w<P: AsRef<Path>>(path: P, visibility: Visibility) -> std::io::Resu
};
options.open(path)
}
/// Open a file readable
/// Open a file readably
///
/// # Examples
///
/// See [fopen_w].
pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
OpenOptions::new()
.read(true)
@@ -29,9 +64,47 @@ pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
.open(path)
}
/// Extension trait for [std::io::Read] adding [read_slice_to_end]
pub trait ReadSliceToEnd {
/// Error type returned by functions in this trait
type Error;
/// Read slice asserting that the length of the data to read is at most
/// as long as the buffer to read into
///
/// Note that this *may* append data read to [buf] even if the function fails,
/// so the caller should make no assumptions about the contents of the buffer
/// after calling read_slice_to_end if the result is an error.
///
/// # Examples
///
/// ```
/// use rosenpass_util::file::ReadSliceToEnd;
///
/// const DATA : &[u8] = b"Hello World";
///
/// // It is OK if file and buffer are equally long
/// let mut buf = vec![b' '; 11];
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf[..DATA.len()]);
/// assert!(res.is_ok()); // Read is overlong
/// assert_eq!(buf, DATA); // Finally, data was successfully read
///
/// // It is OK if the buffer is longer than the file
/// let mut buf = vec![b' '; 16];
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf);
/// assert!(matches!(res, Ok(11)));
/// assert_eq!(buf, b"Hello World "); // Data was still read to the buffer!
///
/// // It is not OK if the buffer is shorter than the file
/// let mut buf = vec![b' '; 5];
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf);
/// assert!(res.is_err());
///
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
/// assert_eq!(buf, b"Hello"); // Data was still read to the buffer!
///
/// Ok::<(), std::io::Error>(())
/// ```
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
}
@@ -53,9 +126,50 @@ impl<R: Read> ReadSliceToEnd for R {
}
}
/// Extension trait for [std::io::Read] adding [read_exact_to_end]
pub trait ReadExactToEnd {
/// Error type returned by functions in this trait
type Error;
/// Read slice asserting that the length of the data to be read
/// and the buffer are exactly the same length.
///
/// Note that this *may* append data read to [buf] even if the function fails,
/// so the caller should make no assumptions about the contents of the buffer
/// after calling read_exact_to_end if the result is an error.
///
/// # Examples
///
/// ```
/// use rosenpass_util::file::ReadExactToEnd;
///
/// const DATA : &[u8] = b"Hello World";
///
/// // It is OK if file and buffer are equally long
/// let mut buf = vec![b' '; 11];
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf[..DATA.len()]);
/// assert!(res.is_ok()); // Read is overlong
/// assert_eq!(buf, DATA); // Finally, data was successfully read
///
/// // It is not OK if the buffer is longer than the file
/// let mut buf = vec![b' '; 16];
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf);
/// assert!(res.is_err());
///
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
/// // The read implementation for &[u8] happens not to do this
/// assert_eq!(buf, b" "); // Data was still read to the buffer!
///
/// // It is not OK if the buffer is shorter than the file
/// let mut buf = vec![b' '; 5];
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf);
/// assert!(res.is_err());
///
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
/// assert_eq!(buf, b"Hello"); // Data was still read to the buffer!
///
/// Ok::<(), std::io::Error>(())
/// ```
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<(), Self::Error>;
}
@@ -70,51 +184,190 @@ impl<R: Read> ReadExactToEnd for R {
}
}
/// Load a value from a file
pub trait LoadValue {
/// Error type returned
type Error;
/// Load a value from a file
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use std::io::Write;
/// use tempfile::tempdir;
/// use rosenpass_util::file::{fopen_r, fopen_w, LoadValue, ReadExactToEnd, StoreValue, Visibility};
///
/// #[derive(Debug, PartialEq, Eq)]
/// struct MyInt(pub u32);
///
/// impl StoreValue for MyInt {
/// type Error = std::io::Error;
///
/// fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error> {
/// let mut f = fopen_w(path, Visibility::Public)?;
/// f.write_all(&self.0.to_le_bytes())
/// }
/// }
///
/// impl LoadValue for MyInt {
/// type Error = anyhow::Error;
///
/// fn load<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
/// where
/// Self: Sized,
/// {
/// let mut b = [0u8; 4];
/// fopen_r(path)?.read_exact_to_end(&mut b)?;
/// Ok(MyInt(u32::from_le_bytes(b)))
/// }
/// }
///
/// let dir = tempdir()?;
/// let path = dir.path().join("my_int");
///
/// let orig = MyInt(17);
/// orig.store(&path)?;
///
/// let copy = MyInt::load(&path)?;
/// assert_eq!(orig, copy);
///
/// Ok::<(), anyhow::Error>(())
/// ```
fn load<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
where
Self: Sized;
}
/// Load a value from a file encoded as base64
pub trait LoadValueB64 {
/// Error type returned
type Error;
/// Load a value from a file encoded as base64
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use tempfile::tempdir;
/// use rosenpass_util::b64::{b64_decode, b64_encode};
/// use rosenpass_util::file::{
/// fopen_r, fopen_w, LoadValueB64, ReadSliceToEnd, StoreValueB64, StoreValueB64Writer,
/// Visibility,
/// };
///
/// #[derive(Debug, PartialEq, Eq)]
/// struct MyInt(pub u32);
///
/// impl StoreValueB64Writer for MyInt {
/// type Error = anyhow::Error;
///
/// fn store_b64_writer<const F: usize, W: std::io::Write>(
/// &self,
/// mut writer: W,
/// ) -> Result<(), Self::Error> {
/// // Let me just point out while writing this example,
/// // that this API is currently, entirely shit in terms of
/// // how it deals with buffer lengths.
/// let mut buf = [0u8; F];
/// let b64 = b64_encode(&self.0.to_le_bytes(), &mut buf)?;
/// writer.write_all(b64.as_bytes())?;
/// Ok(())
/// }
/// }
///
/// impl StoreValueB64 for MyInt {
/// type Error = anyhow::Error;
///
/// fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
/// where
/// Self: Sized,
/// {
/// // The buffer length (first generic arg) is kind of an upper bound
/// self.store_b64_writer::<F, _>(fopen_w(path, Visibility::Public)?)
/// }
/// }
///
/// impl LoadValueB64 for MyInt {
/// type Error = anyhow::Error;
///
/// fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
/// where
/// Self: Sized,
/// {
/// // The buffer length is kind of an upper bound
/// let mut b64_buf = [0u8; F];
/// let b64_len = fopen_r(path)?.read_slice_to_end(&mut b64_buf)?;
/// let b64_dat = &b64_buf[..b64_len];
///
/// let mut buf = [0u8; 4];
/// b64_decode(b64_dat, &mut buf)?;
/// Ok(MyInt(u32::from_le_bytes(buf)))
/// }
/// }
///
/// let dir = tempdir()?;
/// let path = dir.path().join("my_int");
///
/// let orig = MyInt(17);
/// orig.store_b64::<10, _>(&path)?;
///
/// let copy = MyInt::load_b64::<10, _>(&path)?;
/// assert_eq!(orig, copy);
///
/// Ok::<(), anyhow::Error>(())
/// ```
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
where
Self: Sized;
}
/// Store a value encoded as base64 in a file.
pub trait StoreValueB64 {
/// Error type returned
type Error;
/// Store a value encoded as base64 in a file.
///
/// # Examples
///
/// See [LoadValueB64::load_b64].
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
where
Self: Sized;
}
/// Store a value encoded as base64 to a writable stream
pub trait StoreValueB64Writer {
/// Error type returned
type Error;
/// Store a value encoded as base64 to a writable stream
///
/// # Examples
///
/// See [LoadValueB64::load_b64].
fn store_b64_writer<const F: usize, W: std::io::Write>(
&self,
writer: W,
) -> Result<(), Self::Error>;
}
/// Store a value in a file
pub trait StoreValue {
/// Error type returned
type Error;
/// Store a value in a file
///
/// # Examples
///
/// See [LoadValue::load].
fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
}
pub trait DisplayValueB64 {
type Error;
fn display_b64<'o>(&self, output: &'o mut [u8]) -> Result<&'o str, Self::Error>;
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,87 +1,260 @@
pub fn mutating<T, F>(mut v: T, f: F) -> T
//! Syntax sugar & helpers for a functional programming style and method chains
/// Mutate a value; mostly syntactic sugar
///
/// # Examples
///
/// ```
/// use std::borrow::Borrow;
/// use rosenpass_util::functional::{mutating, MutatingExt, sideeffect, SideffectExt, ApplyExt};
/// use rosenpass_util::mem::DiscardResultExt;
///
/// // Say you have a function that takes a mutable reference
/// fn replace<T: Copy + Eq>(slice: &mut [T], targ: T, by: T) {
/// for val in slice.iter_mut() {
/// if *val == targ {
/// *val = by;
/// }
/// }
/// }
///
/// // Or you have some action that you want to perform as a side effect
/// fn count<T: Copy + Eq>(accumulator: &mut usize, slice: &[T], targ: T) {
/// *accumulator += slice.iter()
/// .filter(|e| *e == &targ)
/// .count();
/// }
///
/// // Lets say, you also have a function that actually modifies the value
/// fn rot2<const N : usize>(slice: [u8; N]) -> [u8; N] {
/// let it = slice.iter()
/// .cycle()
/// .skip(2)
/// .take(N);
///
/// let mut ret = [0u8; N];
/// for (no, elm) in it.enumerate() {
/// ret[no] = *elm;
/// }
///
/// ret
/// }
///
/// // Then these function are kind of clunky to use in an expression;
/// // it can be done, but the resulting code is a bit verbose
/// let mut accu = 0;
/// assert_eq!(b"llo_WorldHe", &{
/// let mut buf = b"Hello World".to_owned();
/// count(&mut accu, &buf, b'l');
/// replace(&mut buf, b' ', b'_');
/// rot2(buf)
/// });
/// assert_eq!(accu, 3);
///
/// // Instead you could use mutating for a slightly prettier syntax,
/// // but this makes only sense if you want to apply a single action
/// assert_eq!(b"Hello_World",
/// &mutating(b"Hello World".to_owned(), |buf|
/// replace(buf, b' ', b'_')));
///
/// // The same is the case for sideeffect()
/// assert_eq!(b"Hello World",
/// &sideeffect(b"Hello World".to_owned(), |buf|
/// count(&mut accu, buf, b'l')));
/// assert_eq!(accu, 6);
///
/// // Calling rot2 on its own is straightforward of course
/// assert_eq!(b"llo WorldHe", &rot2(b"Hello World".to_owned()));
///
/// // These operations can be conveniently used in a method chain
/// // by using the extension traits.
/// //
/// // This is also quite handy if you just need to
/// // modify a value in a long method chain.
/// //
/// // Here apply() also comes in quite handy, because we can use it
/// // to modify the value itself (turning it into a reference).
/// assert_eq!(b"llo_WorldHe",
/// b"Hello World"
/// .to_owned()
/// .sideeffect(|buf| count(&mut accu, buf, b'l'))
/// .mutating(|buf| replace(buf, b' ', b'_'))
/// .apply(rot2)
/// .borrow() as &[u8]);
/// assert_eq!(accu, 9);
///
/// // There is also the mutating_mut variant, which can operate on any mutable reference;
/// // this is mainly useful in a method chain if you are dealing with a mutable reference.
/// //
/// // This example is quite artificial though.
/// assert_eq!(b"llo_WorldHe",
/// b"hello world"
/// .to_owned()
/// .mutating(|buf|
/// // Can not use sideeffect_ref at the start, because it drops the mut reference
/// // status
/// buf.sideeffect_mut(|buf| count(&mut accu, buf, b'l'))
/// .mutating_mut(|buf| replace(buf, b' ', b'_'))
/// .mutating_mut(|buf| replace(buf, b'h', b'H'))
/// .mutating_mut(|buf| replace(buf, b'w', b'W'))
/// // Using rot2 is more complex now
/// .mutating_mut(|buf| {
/// *buf = rot2(*buf);
/// })
/// // Can use sideeffect_ref at the end, because we no longer need
/// // the &mut reference
/// .sideeffect_ref(|buf| count(&mut accu, *buf, b'l'))
/// // And we can use apply to fix the return value if we really want to go
/// // crazy and avoid using a {} block
/// .apply(|_| ())
/// // [crate::mem::DiscardResult::discard_result] does the same job and it is more explicit.
/// .discard_result())
/// .borrow() as &[u8]);
/// assert_eq!(accu, 15);
/// ```
pub fn mutating<T, F>(mut v: T, mut f: F) -> T
where
F: Fn(&mut T),
F: FnMut(&mut T),
{
f(&mut v);
v
}
/// Mutating values on the fly in a method chain
pub trait MutatingExt {
/// Mutating values on the fly in a method chain (owning)
///
/// # Examples
///
/// See [mutating].
fn mutating<F>(self, f: F) -> Self
where
F: Fn(&mut Self);
F: FnMut(&mut Self);
/// Mutating values on the fly in a method chain (non-owning)
///
/// # Examples
///
/// See [mutating].
fn mutating_mut<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Self);
F: FnMut(&mut Self);
}
impl<T> MutatingExt for T {
fn mutating<F>(self, f: F) -> Self
where
F: Fn(&mut Self),
F: FnMut(&mut Self),
{
mutating(self, f)
}
fn mutating_mut<F>(&mut self, f: F) -> &mut Self
fn mutating_mut<F>(&mut self, mut f: F) -> &mut Self
where
F: Fn(&mut Self),
F: FnMut(&mut Self),
{
f(self);
self
}
}
pub fn sideeffect<T, F>(v: T, f: F) -> T
/// Apply a sideeffect using some value in an expression
///
/// # Examples
///
/// See [mutating].
pub fn sideeffect<T, F>(v: T, mut f: F) -> T
where
F: Fn(&T),
F: FnMut(&T),
{
f(&v);
v
}
/// Apply sideeffect on the fly in a method chain
pub trait SideffectExt {
/// Apply sideeffect on the fly in a method chain (owning)
///
/// # Examples
///
/// See [mutating].
fn sideeffect<F>(self, f: F) -> Self
where
F: Fn(&Self);
F: FnMut(&Self);
/// Apply sideeffect on the fly in a method chain (immutable ref)
///
/// # Examples
///
/// See [mutating].
fn sideeffect_ref<F>(&self, f: F) -> &Self
where
F: Fn(&Self);
F: FnMut(&Self);
/// Apply sideeffect on the fly in a method chain (mutable ref)
///
/// # Examples
///
/// See [mutating].
fn sideeffect_mut<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&Self);
F: FnMut(&Self);
}
impl<T> SideffectExt for T {
fn sideeffect<F>(self, f: F) -> Self
where
F: Fn(&Self),
F: FnMut(&Self),
{
sideeffect(self, f)
}
fn sideeffect_ref<F>(&self, f: F) -> &Self
fn sideeffect_ref<F>(&self, mut f: F) -> &Self
where
F: Fn(&Self),
F: FnMut(&Self),
{
f(self);
self
}
fn sideeffect_mut<F>(&mut self, f: F) -> &mut Self
fn sideeffect_mut<F>(&mut self, mut f: F) -> &mut Self
where
F: Fn(&Self),
F: FnMut(&Self),
{
f(self);
self
}
}
/// Just run the function
///
/// This is occasionally useful; in particular, you can
/// use it to control the meaning of the question mark operator.
///
/// # Examples
///
/// ```
/// use rosenpass_util::functional::run;
///
/// fn add_and_mul(a: Option<u32>, b: Option<u32>, c: anyhow::Result<u32>, d: anyhow::Result<u32>) -> u32 {
/// run(|| -> anyhow::Result<u32> {
/// let ab = run(|| Some(a? * b?)).unwrap_or(0);
/// Ok(ab + c? + d?)
/// }).unwrap()
/// }
///
/// assert_eq!(98, add_and_mul(Some(10), Some(9), Ok(3), Ok(5)));
/// assert_eq!(8, add_and_mul(None, Some(15), Ok(3), Ok(5)));
/// ```
pub fn run<R, F: FnOnce() -> R>(f: F) -> R {
f()
}
/// Apply a function to a value in a method chain
pub trait ApplyExt: Sized {
/// Apply a function to a value in a method chain
///
/// # Examples
///
/// See [mutating].
fn apply<R, F>(self, f: F) -> R
where
F: FnOnce(Self) -> R;

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<()>;
}