mirror of
https://github.com/rosenpass/rosenpass.git
synced 2026-02-27 14:03:11 -08:00
Unit tests & api doc
Merge pull request #458 from rosenpass/dev/karo/docs_and_unit_tests
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
216
util/src/fd.rs
216
util/src/fd.rs
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
269
util/src/file.rs
269
util/src/file.rs
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
355
util/src/io.rs
355
util/src/io.rs
@@ -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<()>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user