bare-types 0.3.0

A zero-cost foundation for type-safe domain modeling in Rust. Implements the 'Parse, don't validate' philosophy to eliminate primitive obsession and ensure data integrity at the system boundary.
Documentation
//! File descriptor type for system information.
//!
//! This module provides a type-safe abstraction for file descriptors,
//! ensuring valid FD values.

use core::fmt;
use core::str::FromStr;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Error type for FD parsing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum FdError {
    /// Invalid FD value (must be non-negative)
    ///
    /// File descriptors must be non-negative integers.
    /// This variant contains the invalid value.
    InvalidValue(i32),
    /// FD too large (platform-dependent)
    ///
    /// The provided file descriptor exceeds the maximum valid value for the platform.
    /// On Linux, the maximum is typically 2^20-1 (1,048,576).
    /// On other platforms, it may be `i32::MAX`.
    /// This variant contains the invalid value.
    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 {}

/// File descriptor.
///
/// This type provides type-safe file descriptors. It uses the newtype pattern
/// with `#[repr(transparent)]` for zero-cost abstraction.
///
/// # Standard File Descriptors
///
/// - `0`: Standard input (stdin)
/// - `1`: Standard output (stdout)
/// - `2`: Standard error (stderr)
///
/// # Platform Differences
///
/// - **Linux/Unix**: FDs are typically 32-bit signed integers
/// - **Windows**: Handles are used instead of FDs (this type is for Unix-like systems)
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fd(i32);

impl Fd {
    /// Maximum valid FD value for the current platform.
    ///
    /// On Linux, this is typically 2^20-1 (1,048,576).
    /// On other platforms, this is `i32::MAX`.
    #[cfg(target_os = "linux")]
    pub const MAX: i32 = 1_048_576;

    /// Maximum valid FD value for the current platform.
    ///
    /// On Linux, this is typically 2^20-1 (1,048,576).
    /// On other platforms, this is `i32::MAX`.
    #[cfg(not(target_os = "linux"))]
    pub const MAX: i32 = i32::MAX;

    /// Standard input file descriptor
    pub const STDIN: Self = Self(0);

    /// Standard output file descriptor
    pub const STDOUT: Self = Self(1);

    /// Standard error file descriptor
    pub const STDERR: Self = Self(2);

    /// Creates a new FD from a value.
    ///
    /// # Errors
    ///
    /// Returns an error if value is negative or exceeds maximum.
    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))
    }

    /// Creates a new FD without validation.
    ///
    /// This constructor bypasses validation and should only be used when
    /// the caller can guarantee the value is a valid file descriptor.
    /// For validated construction, use [`Fd::new`].
    ///
    /// # Examples
    ///
    /// ```rust
    /// use bare_types::sys::Fd;
    ///
    /// // Standard file descriptors can be created directly
    /// let stdin = Fd::new_unchecked(0);
    /// assert!(stdin.is_stdin());
    /// ```
    #[must_use]
    #[inline]
    pub const fn new_unchecked(value: i32) -> Self {
        Self(value)
    }

    /// Returns the FD value.
    #[must_use]
    #[inline]
    pub const fn as_i32(&self) -> i32 {
        self.0
    }

    /// Returns `true` if this is a standard file descriptor (stdin, stdout, or stderr).
    #[must_use]
    #[inline]
    pub const fn is_standard(&self) -> bool {
        self.0 >= 0 && self.0 <= 2
    }

    /// Returns `true` if this is stdin.
    #[must_use]
    #[inline]
    pub const fn is_stdin(&self) -> bool {
        self.0 == 0
    }

    /// Returns `true` if this is stdout.
    #[must_use]
    #[inline]
    pub const fn is_stdout(&self) -> bool {
        self.0 == 1
    }

    /// Returns `true` if this is stderr.
    #[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);
    }
}