use core::fmt;
use core::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum FdError {
InvalidValue(i32),
TooLarge(i32),
}
impl fmt::Display for FdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidValue(v) => write!(f, "invalid FD value: {v} (must be non-negative)"),
Self::TooLarge(v) => write!(f, "FD too large: {v}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for FdError {}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fd(i32);
impl Fd {
#[cfg(target_os = "linux")]
pub const MAX: i32 = 1_048_576;
#[cfg(not(target_os = "linux"))]
pub const MAX: i32 = i32::MAX;
pub const STDIN: Self = Self(0);
pub const STDOUT: Self = Self(1);
pub const STDERR: Self = Self(2);
pub const fn new(value: i32) -> Result<Self, FdError> {
if value < 0 {
return Err(FdError::InvalidValue(value));
}
#[allow(clippy::absurd_extreme_comparisons)]
if value > Self::MAX {
return Err(FdError::TooLarge(value));
}
Ok(Self(value))
}
#[must_use]
#[inline]
pub const fn new_unchecked(value: i32) -> Self {
Self(value)
}
#[must_use]
#[inline]
pub const fn as_i32(&self) -> i32 {
self.0
}
#[must_use]
#[inline]
pub const fn is_standard(&self) -> bool {
self.0 >= 0 && self.0 <= 2
}
#[must_use]
#[inline]
pub const fn is_stdin(&self) -> bool {
self.0 == 0
}
#[must_use]
#[inline]
pub const fn is_stdout(&self) -> bool {
self.0 == 1
}
#[must_use]
#[inline]
pub const fn is_stderr(&self) -> bool {
self.0 == 2
}
}
impl TryFrom<i32> for Fd {
type Error = FdError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<u32> for Fd {
type Error = FdError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value > i32::MAX as u32 {
return Err(FdError::TooLarge(i32::MAX));
}
#[allow(clippy::cast_possible_wrap)]
Self::new(value as i32)
}
}
impl FromStr for Fd {
type Err = FdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s.parse::<i32>().map_err(|_| FdError::InvalidValue(-1))?;
Self::new(value)
}
}
impl fmt::Display for Fd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Fd {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let value = i32::arbitrary(u)?;
if (0..=Self::MAX).contains(&value) {
Ok(Self(value))
} else {
Ok(Self(0))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid() {
let fd = Fd::new(0).unwrap();
assert_eq!(fd.as_i32(), 0);
let fd = Fd::new(100).unwrap();
assert_eq!(fd.as_i32(), 100);
}
#[test]
fn test_new_negative() {
assert!(matches!(Fd::new(-1), Err(FdError::InvalidValue(-1))));
}
#[test]
#[cfg(target_os = "linux")]
fn test_new_too_large() {
assert!(matches!(Fd::new(1_048_577), Err(FdError::TooLarge(_))));
}
#[test]
#[cfg(not(target_os = "linux"))]
#[ignore = "On non-Linux platforms, MAX equals i32::MAX so no value can exceed it"]
fn test_new_too_large() {
assert!(matches!(Fd::new(i32::MAX), Err(FdError::TooLarge(_))));
}
#[test]
fn test_try_from_i32() {
let fd: Fd = 100i32.try_into().unwrap();
assert_eq!(fd.as_i32(), 100);
}
#[test]
fn test_try_from_u32() {
let fd: Fd = 100u32.try_into().unwrap();
assert_eq!(fd.as_i32(), 100);
}
#[test]
fn test_try_from_u32_too_large() {
assert!(matches!(
TryInto::<Fd>::try_into(i32::MAX as u32 + 1),
Err(FdError::TooLarge(_))
));
}
#[test]
fn test_from_str() {
let fd: Fd = "100".parse().unwrap();
assert_eq!(fd.as_i32(), 100);
}
#[test]
fn test_from_str_error() {
assert!(matches!(
("-1").parse::<Fd>(),
Err(FdError::InvalidValue(_))
));
assert!("abc".parse::<Fd>().is_err());
}
#[test]
fn test_display() {
let fd = Fd::new(100).unwrap();
assert_eq!(format!("{}", fd), "100");
}
#[test]
fn test_constants() {
assert_eq!(Fd::STDIN.as_i32(), 0);
assert_eq!(Fd::STDOUT.as_i32(), 1);
assert_eq!(Fd::STDERR.as_i32(), 2);
}
#[test]
fn test_is_standard() {
assert!(Fd::STDIN.is_standard());
assert!(Fd::STDOUT.is_standard());
assert!(Fd::STDERR.is_standard());
let fd = Fd::new(100).unwrap();
assert!(!fd.is_standard());
}
#[test]
fn test_is_stdin() {
assert!(Fd::STDIN.is_stdin());
assert!(!Fd::STDOUT.is_stdin());
assert!(!Fd::STDERR.is_stdin());
}
#[test]
fn test_is_stdout() {
assert!(!Fd::STDIN.is_stdout());
assert!(Fd::STDOUT.is_stdout());
assert!(!Fd::STDERR.is_stdout());
}
#[test]
fn test_is_stderr() {
assert!(!Fd::STDIN.is_stderr());
assert!(!Fd::STDOUT.is_stderr());
assert!(Fd::STDERR.is_stderr());
}
#[test]
fn test_clone() {
let fd = Fd::new(100).unwrap();
let fd2 = fd.clone();
assert_eq!(fd, fd2);
}
#[test]
fn test_equality() {
let f1 = Fd::new(100).unwrap();
let f2 = Fd::new(100).unwrap();
let f3 = Fd::new(200).unwrap();
assert_eq!(f1, f2);
assert_ne!(f1, f3);
}
#[test]
fn test_ordering() {
let f1 = Fd::new(100).unwrap();
let f2 = Fd::new(200).unwrap();
assert!(f1 < f2);
assert!(f2 > f1);
}
#[test]
fn test_new_unchecked() {
let fd = Fd::new_unchecked(100);
assert_eq!(fd.as_i32(), 100);
}
}