Skip to main content

bare_types/sys/
fd.rs

1//! File descriptor type for system information.
2//!
3//! This module provides a type-safe abstraction for file descriptors,
4//! ensuring valid FD values.
5
6use core::fmt;
7use core::str::FromStr;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// Error type for FD parsing.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[non_exhaustive]
16pub enum FdError {
17    /// Invalid FD value (must be non-negative)
18    ///
19    /// File descriptors must be non-negative integers.
20    /// This variant contains the invalid value.
21    InvalidValue(i32),
22    /// FD too large (platform-dependent)
23    ///
24    /// The provided file descriptor exceeds the maximum valid value for the platform.
25    /// On Linux, the maximum is typically 2^20-1 (1,048,576).
26    /// On other platforms, it may be `i32::MAX`.
27    /// This variant contains the invalid value.
28    TooLarge(i32),
29}
30
31impl fmt::Display for FdError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::InvalidValue(v) => write!(f, "invalid FD value: {v} (must be non-negative)"),
35            Self::TooLarge(v) => write!(f, "FD too large: {v}"),
36        }
37    }
38}
39
40#[cfg(feature = "std")]
41impl std::error::Error for FdError {}
42
43/// File descriptor.
44///
45/// This type provides type-safe file descriptors. It uses the newtype pattern
46/// with `#[repr(transparent)]` for zero-cost abstraction.
47///
48/// # Standard File Descriptors
49///
50/// - `0`: Standard input (stdin)
51/// - `1`: Standard output (stdout)
52/// - `2`: Standard error (stderr)
53///
54/// # Platform Differences
55///
56/// - **Linux/Unix**: FDs are typically 32-bit signed integers
57/// - **Windows**: Handles are used instead of FDs (this type is for Unix-like systems)
58#[repr(transparent)]
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub struct Fd(i32);
62
63impl Fd {
64    /// Maximum valid FD value (platform-dependent)
65    #[cfg(target_os = "linux")]
66    pub const MAX: i32 = 1_048_576;
67
68    #[cfg(not(target_os = "linux"))]
69    pub const MAX: i32 = i32::MAX;
70
71    /// Standard input file descriptor
72    pub const STDIN: Self = Self(0);
73
74    /// Standard output file descriptor
75    pub const STDOUT: Self = Self(1);
76
77    /// Standard error file descriptor
78    pub const STDERR: Self = Self(2);
79
80    /// Creates a new FD from a value.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if value is negative or exceeds maximum.
85    pub const fn new(value: i32) -> Result<Self, FdError> {
86        if value < 0 {
87            return Err(FdError::InvalidValue(value));
88        }
89        if value > Self::MAX {
90            return Err(FdError::TooLarge(value));
91        }
92        Ok(Self(value))
93    }
94
95    /// Creates a new FD without validation.
96    ///
97    /// This constructor bypasses validation and should only be used when
98    /// the caller can guarantee the value is a valid file descriptor.
99    /// For validated construction, use [`Fd::new`].
100    ///
101    /// # Examples
102    ///
103    /// ```rust
104    /// use bare_types::sys::Fd;
105    ///
106    /// // Standard file descriptors can be created directly
107    /// let stdin = Fd::new_unchecked(0);
108    /// assert!(stdin.is_stdin());
109    /// ```
110    #[must_use]
111    #[inline]
112    pub const fn new_unchecked(value: i32) -> Self {
113        Self(value)
114    }
115
116    /// Returns the FD value.
117    #[must_use]
118    #[inline]
119    pub const fn as_i32(&self) -> i32 {
120        self.0
121    }
122
123    /// Returns `true` if this is a standard file descriptor (stdin, stdout, or stderr).
124    #[must_use]
125    #[inline]
126    pub const fn is_standard(&self) -> bool {
127        self.0 >= 0 && self.0 <= 2
128    }
129
130    /// Returns `true` if this is stdin.
131    #[must_use]
132    #[inline]
133    pub const fn is_stdin(&self) -> bool {
134        self.0 == 0
135    }
136
137    /// Returns `true` if this is stdout.
138    #[must_use]
139    #[inline]
140    pub const fn is_stdout(&self) -> bool {
141        self.0 == 1
142    }
143
144    /// Returns `true` if this is stderr.
145    #[must_use]
146    #[inline]
147    pub const fn is_stderr(&self) -> bool {
148        self.0 == 2
149    }
150}
151
152impl TryFrom<i32> for Fd {
153    type Error = FdError;
154
155    fn try_from(value: i32) -> Result<Self, Self::Error> {
156        Self::new(value)
157    }
158}
159
160impl TryFrom<u32> for Fd {
161    type Error = FdError;
162
163    fn try_from(value: u32) -> Result<Self, Self::Error> {
164        if value > i32::MAX as u32 {
165            return Err(FdError::TooLarge(i32::MAX));
166        }
167        #[allow(clippy::cast_possible_wrap)]
168        Self::new(value as i32)
169    }
170}
171
172impl FromStr for Fd {
173    type Err = FdError;
174
175    fn from_str(s: &str) -> Result<Self, Self::Err> {
176        let value = s.parse::<i32>().map_err(|_| FdError::InvalidValue(-1))?;
177        Self::new(value)
178    }
179}
180
181impl fmt::Display for Fd {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{}", self.0)
184    }
185}
186
187#[cfg(feature = "arbitrary")]
188impl<'a> arbitrary::Arbitrary<'a> for Fd {
189    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
190        let value = i32::arbitrary(u)?;
191        if (0..=Self::MAX).contains(&value) {
192            Ok(Self(value))
193        } else {
194            Ok(Self(0))
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use core::convert::TryInto;
203
204    #[test]
205    fn test_new_valid() {
206        let fd = Fd::new(0).unwrap();
207        assert_eq!(fd.as_i32(), 0);
208        let fd = Fd::new(100).unwrap();
209        assert_eq!(fd.as_i32(), 100);
210    }
211
212    #[test]
213    fn test_new_negative() {
214        assert!(matches!(Fd::new(-1), Err(FdError::InvalidValue(-1))));
215    }
216
217    #[test]
218    fn test_new_too_large() {
219        assert!(matches!(Fd::new(i32::MAX), Err(FdError::TooLarge(_))));
220    }
221
222    #[test]
223    fn test_try_from_i32() {
224        let fd: Fd = 100i32.try_into().unwrap();
225        assert_eq!(fd.as_i32(), 100);
226    }
227
228    #[test]
229    fn test_try_from_u32() {
230        let fd: Fd = 100u32.try_into().unwrap();
231        assert_eq!(fd.as_i32(), 100);
232    }
233
234    #[test]
235    fn test_try_from_u32_too_large() {
236        assert!(matches!(
237            TryInto::<Fd>::try_into(i32::MAX as u32 + 1),
238            Err(FdError::TooLarge(_))
239        ));
240    }
241
242    #[test]
243    fn test_from_str() {
244        let fd: Fd = "100".parse().unwrap();
245        assert_eq!(fd.as_i32(), 100);
246    }
247
248    #[test]
249    fn test_from_str_error() {
250        assert!(matches!(
251            ("-1").parse::<Fd>(),
252            Err(FdError::InvalidValue(_))
253        ));
254        assert!("abc".parse::<Fd>().is_err());
255    }
256
257    #[test]
258    fn test_display() {
259        let fd = Fd::new(100).unwrap();
260        assert_eq!(format!("{}", fd), "100");
261    }
262
263    #[test]
264    fn test_constants() {
265        assert_eq!(Fd::STDIN.as_i32(), 0);
266        assert_eq!(Fd::STDOUT.as_i32(), 1);
267        assert_eq!(Fd::STDERR.as_i32(), 2);
268    }
269
270    #[test]
271    fn test_is_standard() {
272        assert!(Fd::STDIN.is_standard());
273        assert!(Fd::STDOUT.is_standard());
274        assert!(Fd::STDERR.is_standard());
275        let fd = Fd::new(100).unwrap();
276        assert!(!fd.is_standard());
277    }
278
279    #[test]
280    fn test_is_stdin() {
281        assert!(Fd::STDIN.is_stdin());
282        assert!(!Fd::STDOUT.is_stdin());
283        assert!(!Fd::STDERR.is_stdin());
284    }
285
286    #[test]
287    fn test_is_stdout() {
288        assert!(!Fd::STDIN.is_stdout());
289        assert!(Fd::STDOUT.is_stdout());
290        assert!(!Fd::STDERR.is_stdout());
291    }
292
293    #[test]
294    fn test_is_stderr() {
295        assert!(!Fd::STDIN.is_stderr());
296        assert!(!Fd::STDOUT.is_stderr());
297        assert!(Fd::STDERR.is_stderr());
298    }
299
300    #[test]
301    fn test_clone() {
302        let fd = Fd::new(100).unwrap();
303        let fd2 = fd.clone();
304        assert_eq!(fd, fd2);
305    }
306
307    #[test]
308    fn test_equality() {
309        let f1 = Fd::new(100).unwrap();
310        let f2 = Fd::new(100).unwrap();
311        let f3 = Fd::new(200).unwrap();
312        assert_eq!(f1, f2);
313        assert_ne!(f1, f3);
314    }
315
316    #[test]
317    fn test_ordering() {
318        let f1 = Fd::new(100).unwrap();
319        let f2 = Fd::new(200).unwrap();
320        assert!(f1 < f2);
321        assert!(f2 > f1);
322    }
323
324    #[test]
325    fn test_new_unchecked() {
326        let fd = Fd::new_unchecked(100);
327        assert_eq!(fd.as_i32(), 100);
328    }
329}