#[cfg(test)]
#[path = "tests/std.rs"]
mod tests;
use base64::DecodeError;
use core::{
convert::TryFrom,
fmt::{Debug, Display},
hash::Hash,
str::FromStr,
};
use hex::FromHexError;
use rust_decimal::{
Decimal,
prelude::ToPrimitive as _,
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
env,
ffi::OsString,
path::{Component as PathComponent, Path, PathBuf},
};
use thiserror::Error as ThisError;
#[cfg(feature = "crypto")]
use crate::crypto::Hashed;
#[cfg(feature = "crypto")]
use ::{
core::future::Future,
digest::Digest as _,
std::{
fs::File,
io::{BufReader, Error as IoError, Read as _},
},
tokio::{
fs::File as AsyncFile,
io::{AsyncReadExt as _, BufReader as AsyncBufReader},
},
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, ThisError)]
#[non_exhaustive]
pub enum ByteSizedError {
#[error("The supplied data is longer than {0} bytes")]
DataTooLong(usize),
#[error("The supplied data is shorter than {0} bytes")]
DataTooShort(usize),
#[error("The supplied data is not in valid base64 format")]
InvalidBase64String,
#[error("The supplied data is not in valid hexadecimal format")]
InvalidHexString,
}
#[derive(Clone, Debug)]
pub struct LimitIterator<I> {
iter: I,
limit: Option<usize>,
count: usize,
}
impl<I: Iterator> Iterator for LimitIterator<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
#[expect(clippy::arithmetic_side_effects, reason = "Range is controlled")]
if let Some(limit) = self.limit {
if self.count >= limit {
return None;
}
self.count += 1;
}
self.iter.next()
}
}
pub trait AsStr {
#[must_use]
fn as_str(&self) -> &str;
}
impl AsStr for String {
fn as_str(&self) -> &str {
self.as_str()
}
}
impl AsStr for str {
fn as_str(&self) -> &str {
self
}
}
pub trait ByteSized<const SIZE: usize>:
Sized
+ Clone
+ for<'a> ForceFrom<&'a [u8]>
+ ForceFrom<Vec<u8>>
+ for<'a> ForceFrom<&'a Vec<u8>>
{
#[must_use]
fn as_bytes(&self) -> &[u8; SIZE];
#[must_use]
fn to_bytes(&self) -> [u8; SIZE];
#[must_use]
fn from_bytes(bytes: [u8; SIZE]) -> Self;
#[must_use]
fn to_base64(&self) -> String;
fn from_base64(encoded: &str) -> Result<Self, DecodeError>;
#[must_use]
fn to_hex(&self) -> String;
fn from_hex(encoded: &str) -> Result<Self, FromHexError>;
#[must_use]
fn to_vec(&self) -> Vec<u8>;
}
pub trait ByteSizedFull<const SIZE: usize>:
ByteSized<SIZE>
+ AsRef<[u8; SIZE]>
+ Debug
+ Default
+ Display
+ From<[u8; SIZE]>
+ for<'a> From<&'a [u8; SIZE]>
+ FromStr
+ Hash
+ PartialEq
+ Serialize
+ for<'de> Deserialize<'de>
+ for<'a> TryFrom<&'a [u8]>
+ for<'a> TryFrom<&'a str>
+ TryFrom<String>
+ for<'a> TryFrom<&'a String>
+ TryFrom<Box<str>>
+ for<'a> TryFrom<Cow<'a, str>>
+ TryFrom<Vec<u8>>
+ for<'a> TryFrom<&'a Vec<u8>>
{}
pub trait ByteSizedMut<const SIZE: usize>:
ByteSized<SIZE>
+ AsMut<[u8; SIZE]>
{
fn as_mut_bytes(&mut self) -> &mut [u8; SIZE];
#[must_use]
fn into_bytes(self) -> [u8; SIZE];
#[must_use]
fn into_vec(self) -> Vec<u8>;
}
#[cfg(feature = "crypto")]
pub trait FileExt {
fn hash<T: Hashed>(path: &Path) -> Result<T, IoError>;
}
#[cfg(feature = "crypto")]
impl FileExt for File {
fn hash<T: Hashed>(path: &Path) -> Result<T, IoError> {
let file = Self::open(path)?;
let mut reader = BufReader::new(file);
let mut hasher = T::Algorithm::new();
let mut buffer = [0; 0x2000]; loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
#[expect(clippy::indexing_slicing, reason = "Infallible")]
hasher.update(&buffer[..count]);
}
Ok(T::from_digest(hasher.finalize()))
}
}
#[cfg(feature = "crypto")]
pub trait AsyncFileExt {
fn hash<T: Hashed>(path: &Path) -> impl Future<Output = Result<T, IoError>> + Send;
}
#[cfg(feature = "crypto")]
impl AsyncFileExt for AsyncFile {
async fn hash<T: Hashed>(path: &Path) -> Result<T, IoError> {
let file = Self::open(path).await?;
let mut reader = AsyncBufReader::new(file);
let mut hasher = T::Algorithm::new();
let mut buffer = [0; 0x2000]; loop {
let count = reader.read(&mut buffer).await?;
if count == 0 {
break;
}
#[expect(clippy::indexing_slicing, reason = "Infallible")]
hasher.update(&buffer[..count]);
}
Ok(T::from_digest(hasher.finalize()))
}
}
pub trait FromIntWithScale<T>: Sized {
fn from_int_with_scale(value: T, scale: u8) -> Option<Self>;
}
macro_rules! impl_from_int_with_scale_for_float {
($t:ty, f32) => {
impl FromIntWithScale<$t> for f32 {
#[allow(clippy::allow_attributes, reason = "Multiple possibilities through the macro invocation")]
#[allow(clippy::cast_lossless, reason = "Being potentially lossy does not matter here")]
fn from_int_with_scale(value: $t, scale: u8) -> Option<Self> {
let factor = 10_u32.checked_pow(u32::from(scale))?;
#[allow(clippy::cast_precision_loss, reason = "Losing precision does not matter here")]
let scaled = value as f32 / factor as f32;
#[allow(trivial_numeric_casts, reason = "Trivial casts here are due to the macro permutations")]
#[allow(clippy::cast_sign_loss, reason = "Loss of sign does not matter here, as we are checking for overflow")]
#[allow(clippy::cast_possible_wrap, reason = "Possible wrapping does not matter here, as we are checking for underflow")]
#[allow(clippy::invalid_upcast_comparisons, reason = "Superfluous upcast comparisons here are due to the macro permutations")]
if scaled.is_infinite() || (value as u128) > 0x0100_0000_u128 || (value as i128) < -0x0100_0000_i128 {
None
} else {
Some(scaled)
}
}
}
};
($t:ty, f64) => {
impl FromIntWithScale<$t> for f64 {
#[allow(clippy::allow_attributes, reason = "Multiple possibilities through the macro invocation")]
#[allow(clippy::cast_lossless, reason = "Being potentially lossy does not matter here")]
fn from_int_with_scale(value: $t, scale: u8) -> Option<Self> {
let factor = 10_u64.checked_pow(u32::from(scale))?;
#[allow(clippy::cast_precision_loss, reason = "Losing precision does not matter here")]
let scaled = value as f64 / factor as f64;
#[allow(trivial_numeric_casts, reason = "Trivial casts here are due to the macro permutations")]
#[allow(clippy::cast_sign_loss, reason = "Loss of sign does not matter here, as we are checking for overflow")]
#[allow(clippy::cast_possible_wrap, reason = "Possible wrapping does not matter here, as we are checking for underflow")]
#[allow(clippy::invalid_upcast_comparisons, reason = "Superfluous upcast comparisons here are due to the macro permutations")]
if scaled.is_infinite() || (value as u128) > 0x0020_0000_0000_0000_u128 || (value as i128) < -0x0020_0000_0000_0000_i128 {
None
} else {
Some(scaled)
}
}
}
};
}
impl_from_int_with_scale_for_float!(i8, f32);
impl_from_int_with_scale_for_float!(i16, f32);
impl_from_int_with_scale_for_float!(i32, f32);
impl_from_int_with_scale_for_float!(i64, f32);
impl_from_int_with_scale_for_float!(i128, f32);
impl_from_int_with_scale_for_float!(i8, f64);
impl_from_int_with_scale_for_float!(i16, f64);
impl_from_int_with_scale_for_float!(i32, f64);
impl_from_int_with_scale_for_float!(i64, f64);
impl_from_int_with_scale_for_float!(i128, f64);
impl_from_int_with_scale_for_float!(u8, f32);
impl_from_int_with_scale_for_float!(u16, f32);
impl_from_int_with_scale_for_float!(u32, f32);
impl_from_int_with_scale_for_float!(u64, f32);
impl_from_int_with_scale_for_float!(u128, f32);
impl_from_int_with_scale_for_float!(u8, f64);
impl_from_int_with_scale_for_float!(u16, f64);
impl_from_int_with_scale_for_float!(u32, f64);
impl_from_int_with_scale_for_float!(u64, f64);
impl_from_int_with_scale_for_float!(u128, f64);
macro_rules! impl_from_int_with_scale_for_decimal {
(i128) => {
impl FromIntWithScale<i128> for Decimal {
fn from_int_with_scale(value: i128, scale: u8) -> Option<Self> {
if value > Decimal::MAX.to_i128().unwrap() || value < Decimal::MIN.to_i128().unwrap() {
None
} else {
Decimal::try_from_i128_with_scale(value, u32::from(scale)).ok()
}
}
}
};
(u128) => {
impl FromIntWithScale<u128> for Decimal {
#[allow(clippy::allow_attributes, reason = "Multiple possibilities through the macro invocation")]
#[allow(clippy::cast_lossless, reason = "Being potentially lossy does not matter here")]
fn from_int_with_scale(value: u128, scale: u8) -> Option<Self> {
#[allow(clippy::cast_possible_wrap, reason = "Possible wrapping does not matter here, as we are checking for underflow")]
if value > Decimal::MAX.to_u128().unwrap() || (value as i128) < Decimal::MIN.to_i128().unwrap() {
None
} else {
Decimal::try_from_i128_with_scale(value as i128, u32::from(scale)).ok()
}
}
}
};
($t:ty) => {
impl FromIntWithScale<$t> for Decimal {
#[allow(clippy::allow_attributes, reason = "Multiple possibilities through the macro invocation")]
#[allow(clippy::cast_lossless, reason = "Being potentially lossy does not matter here")]
fn from_int_with_scale(value: $t, scale: u8) -> Option<Self> {
Decimal::try_from_i128_with_scale(value as i128, u32::from(scale)).ok()
}
}
};
}
impl_from_int_with_scale_for_decimal!(i8);
impl_from_int_with_scale_for_decimal!(i16);
impl_from_int_with_scale_for_decimal!(i32);
impl_from_int_with_scale_for_decimal!(i64);
impl_from_int_with_scale_for_decimal!(i128);
impl_from_int_with_scale_for_decimal!(u8);
impl_from_int_with_scale_for_decimal!(u16);
impl_from_int_with_scale_for_decimal!(u32);
impl_from_int_with_scale_for_decimal!(u64);
impl_from_int_with_scale_for_decimal!(u128);
pub trait ToIntWithScale<T>: Sized {
fn to_int_with_scale(&self, scale: u8) -> Option<T>;
}
macro_rules! impl_to_int_with_scale_for_float {
($t:ty, $f:ty) => {
impl ToIntWithScale<$t> for $f {
#[allow(clippy::allow_attributes, reason = "Multiple possibilities through the macro invocation")]
#[allow(clippy::cast_lossless, reason = "Being potentially lossy does not matter here")]
#[allow(clippy::cast_precision_loss, reason = "Losing precision does not matter here")]
fn to_int_with_scale(&self, scale: u8) -> Option<$t> {
let factor = 10_u64.checked_pow(u32::from(scale))?;
let scaled = (self * factor as $f).round();
if scaled.is_infinite() || scaled > <$t>::MAX as $f || scaled < <$t>::MIN as $f {
None
} else {
#[allow(clippy::cast_possible_truncation, reason = "Possible truncation does not matter here")]
#[allow(clippy::cast_sign_loss, reason = "Loss of sign will not occur here, as we are casting to a float")]
Some(scaled as $t)
}
}
}
};
}
impl_to_int_with_scale_for_float!(i8, f32);
impl_to_int_with_scale_for_float!(i16, f32);
impl_to_int_with_scale_for_float!(i32, f32);
impl_to_int_with_scale_for_float!(i64, f32);
impl_to_int_with_scale_for_float!(i128, f32);
impl_to_int_with_scale_for_float!(i8, f64);
impl_to_int_with_scale_for_float!(i16, f64);
impl_to_int_with_scale_for_float!(i32, f64);
impl_to_int_with_scale_for_float!(i64, f64);
impl_to_int_with_scale_for_float!(i128, f64);
impl_to_int_with_scale_for_float!(u8, f32);
impl_to_int_with_scale_for_float!(u16, f32);
impl_to_int_with_scale_for_float!(u32, f32);
impl_to_int_with_scale_for_float!(u64, f32);
impl_to_int_with_scale_for_float!(u128, f32);
impl_to_int_with_scale_for_float!(u8, f64);
impl_to_int_with_scale_for_float!(u16, f64);
impl_to_int_with_scale_for_float!(u32, f64);
impl_to_int_with_scale_for_float!(u64, f64);
impl_to_int_with_scale_for_float!(u128, f64);
macro_rules! impl_to_int_with_scale_for_decimal {
(i128) => {
impl ToIntWithScale<i128> for Decimal {
fn to_int_with_scale(&self, scale: u8) -> Option<i128> {
let factor = 10_u64.checked_pow(u32::from(scale))?;
(self.checked_mul(Decimal::from(factor))?.round()).to_i128()
}
}
};
(u128) => {
impl ToIntWithScale<u128> for Decimal {
fn to_int_with_scale(&self, scale: u8) -> Option<u128> {
let factor = 10_u64.checked_pow(u32::from(scale))?;
(self.checked_mul(Decimal::from(factor))?.round()).to_u128()
}
}
};
($t:ty) => {
impl ToIntWithScale<$t> for Decimal {
fn to_int_with_scale(&self, scale: u8) -> Option<$t> {
let factor = 10_u64.checked_pow(u32::from(scale))?;
let scaled = self.checked_mul(Decimal::from(factor))?.round();
if scaled > Decimal::from(<$t>::MAX) || scaled < Decimal::from(<$t>::MIN) {
None
} else {
scaled.to_i128().and_then(|value| value.try_into().ok())
}
}
}
};
}
impl_to_int_with_scale_for_decimal!(i8);
impl_to_int_with_scale_for_decimal!(i16);
impl_to_int_with_scale_for_decimal!(i32);
impl_to_int_with_scale_for_decimal!(i64);
impl_to_int_with_scale_for_decimal!(i128);
impl_to_int_with_scale_for_decimal!(u8);
impl_to_int_with_scale_for_decimal!(u16);
impl_to_int_with_scale_for_decimal!(u32);
impl_to_int_with_scale_for_decimal!(u64);
impl_to_int_with_scale_for_decimal!(u128);
pub trait ForceFrom<T> {
fn force_from(value: T) -> Self;
}
pub trait IteratorExt: Iterator {
fn limit(self, limit: Option<usize>) -> LimitIterator<Self> where Self: Sized {
LimitIterator { iter: self, limit, count: 0 }
}
}
impl<I: Iterator> IteratorExt for I {}
pub trait PathExt {
fn append<P: AsRef<Path>>(&self, suffix: P) -> PathBuf;
fn is_subjective(&self) -> bool;
fn normalize(&self) -> PathBuf;
fn restrict<P: AsRef<Path>>(&self, base: P) -> PathBuf;
fn strip_parentdirs(&self, remove_all: bool) -> PathBuf;
fn strip_root(&self) -> PathBuf;
}
impl PathExt for Path {
fn append<P: AsRef<Self>>(&self, suffix: P) -> PathBuf {
PathBuf::from([
self.as_os_str().to_os_string(),
OsString::from(suffix.as_ref()),
].into_iter().collect::<OsString>())
}
fn is_subjective(&self) -> bool {
self.is_relative() && {
let mut components = self.components();
matches!(components.next(), Some(PathComponent::CurDir | PathComponent::ParentDir))
}
}
fn normalize(&self) -> PathBuf {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
if self.as_os_str().is_empty() {
return cwd;
}
let mut segments: Vec<OsString> = vec![];
let mut had_prefix = false;
let mut had_root = false;
for (i, component) in self.components().enumerate() {
match component {
PathComponent::Prefix(prefix) => {
segments.push(prefix.as_os_str().to_os_string());
had_prefix = true;
},
PathComponent::RootDir => {
if had_prefix || i == 0 {
segments.push(component.as_os_str().to_os_string());
had_root = true;
}
},
PathComponent::CurDir |
PathComponent::ParentDir => {
if i == 0 {
segments.append(
cwd.components()
.map(|c| c.as_os_str().to_os_string())
.collect::<Vec<OsString>>()
.as_mut()
);
}
if component == PathComponent::ParentDir {
if segments.len() > usize::from(had_prefix).saturating_add(usize::from(had_root)) {
drop(segments.pop());
}
}
},
PathComponent::Normal(_) => {
if i == 0 {
segments.push(cwd.as_os_str().to_os_string());
}
segments.push(component.as_os_str().to_os_string());
},
}
}
segments.iter().collect()
}
fn restrict<P: AsRef<Self>>(&self, base: P) -> PathBuf {
let basepath = base.as_ref().normalize();
if self.as_os_str().is_empty() {
return basepath;
}
let path = if self.is_absolute() {
self.to_path_buf()
} else {
basepath.join(self)
}.normalize();
match path.strip_prefix(&basepath) {
Ok(_) => path,
Err(_) => basepath,
}
}
fn strip_parentdirs(&self, remove_all: bool) -> PathBuf {
if self.as_os_str().is_empty() || (!remove_all && self.is_absolute()) {
return self.to_owned();
}
let mut at_start = true;
let mut segments: Vec<OsString> = vec![];
for component in self.components() {
match component {
PathComponent::Prefix(_) |
PathComponent::RootDir |
PathComponent::CurDir |
PathComponent::Normal(_) => {
segments.push(component.as_os_str().to_os_string());
at_start = false;
},
PathComponent::ParentDir => {
if !remove_all && !at_start {
segments.push(component.as_os_str().to_os_string());
}
},
}
}
segments.iter().collect()
}
fn strip_root(&self) -> PathBuf {
if self.as_os_str().is_empty() || self.is_relative() {
return self.to_owned();
}
let mut segments: Vec<OsString> = vec![];
for component in self.components() {
match component {
PathComponent::Prefix(_) |
PathComponent::RootDir => {},
PathComponent::CurDir |
PathComponent::ParentDir |
PathComponent::Normal(_) => {
segments.push(component.as_os_str().to_os_string());
},
}
}
if cfg!(windows) {
segments.iter()
.collect::<PathBuf>()
.to_str()
.map_or_else(PathBuf::new, |s| PathBuf::from(s.trim_start_matches('\\')))
} else {
segments.iter().collect()
}
}
}