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 for the current platform.
65    ///
66    /// On Linux, this is typically 2^20-1 (1,048,576).
67    /// On other platforms, this is `i32::MAX`.
68    #[cfg(target_os = "linux")]
69    pub const MAX: i32 = 1_048_576;
70
71    /// Maximum valid FD value for the current platform.
72    ///
73    /// On Linux, this is typically 2^20-1 (1,048,576).
74    /// On other platforms, this is `i32::MAX`.
75    #[cfg(not(target_os = "linux"))]
76    pub const MAX: i32 = i32::MAX;
77
78    /// Standard input file descriptor
79    pub const STDIN: Self = Self(0);
80
81    /// Standard output file descriptor
82    pub const STDOUT: Self = Self(1);
83
84    /// Standard error file descriptor
85    pub const STDERR: Self = Self(2);
86
87    /// Creates a new FD from a value.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if value is negative or exceeds maximum.
92    pub const fn new(value: i32) -> Result<Self, FdError> {
93        if value < 0 {
94            return Err(FdError::InvalidValue(value));
95        }
96        #[allow(clippy::absurd_extreme_comparisons)]
97        if value > Self::MAX {
98            return Err(FdError::TooLarge(value));
99        }
100        Ok(Self(value))
101    }
102
103    /// Creates a new FD without validation.
104    ///
105    /// This constructor bypasses validation and should only be used when
106    /// the caller can guarantee the value is a valid file descriptor.
107    /// For validated construction, use [`Fd::new`].
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// use bare_types::sys::Fd;
113    ///
114    /// // Standard file descriptors can be created directly
115    /// let stdin = Fd::new_unchecked(0);
116    /// assert!(stdin.is_stdin());
117    /// ```
118    #[must_use]
119    #[inline]
120    pub const fn new_unchecked(value: i32) -> Self {
121        Self(value)
122    }
123
124    /// Returns the FD value.
125    #[must_use]
126    #[inline]
127    pub const fn as_i32(&self) -> i32 {
128        self.0
129    }
130
131    /// Returns `true` if this is a standard file descriptor (stdin, stdout, or stderr).
132    #[must_use]
133    #[inline]
134    pub const fn is_standard(&self) -> bool {
135        self.0 >= 0 && self.0 <= 2
136    }
137
138    /// Returns `true` if this is stdin.
139    #[must_use]
140    #[inline]
141    pub const fn is_stdin(&self) -> bool {
142        self.0 == 0
143    }
144
145    /// Returns `true` if this is stdout.
146    #[must_use]
147    #[inline]
148    pub const fn is_stdout(&self) -> bool {
149        self.0 == 1
150    }
151
152    /// Returns `true` if this is stderr.
153    #[must_use]
154    #[inline]
155    pub const fn is_stderr(&self) -> bool {
156        self.0 == 2
157    }
158}
159
160impl TryFrom<i32> for Fd {
161    type Error = FdError;
162
163    fn try_from(value: i32) -> Result<Self, Self::Error> {
164        Self::new(value)
165    }
166}
167
168impl TryFrom<u32> for Fd {
169    type Error = FdError;
170
171    fn try_from(value: u32) -> Result<Self, Self::Error> {
172        if value > i32::MAX as u32 {
173            return Err(FdError::TooLarge(i32::MAX));
174        }
175        #[allow(clippy::cast_possible_wrap)]
176        Self::new(value as i32)
177    }
178}
179
180impl FromStr for Fd {
181    type Err = FdError;
182
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        let value = s.parse::<i32>().map_err(|_| FdError::InvalidValue(-1))?;
185        Self::new(value)
186    }
187}
188
189impl fmt::Display for Fd {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        write!(f, "{}", self.0)
192    }
193}
194
195#[cfg(feature = "arbitrary")]
196impl<'a> arbitrary::Arbitrary<'a> for Fd {
197    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
198        let value = i32::arbitrary(u)?;
199        if (0..=Self::MAX).contains(&value) {
200            Ok(Self(value))
201        } else {
202            Ok(Self(0))
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_new_valid() {
213        let fd = Fd::new(0).unwrap();
214        assert_eq!(fd.as_i32(), 0);
215        let fd = Fd::new(100).unwrap();
216        assert_eq!(fd.as_i32(), 100);
217    }
218
219    #[test]
220    fn test_new_negative() {
221        assert!(matches!(Fd::new(-1), Err(FdError::InvalidValue(-1))));
222    }
223
224    #[test]
225    #[cfg(target_os = "linux")]
226    fn test_new_too_large() {
227        assert!(matches!(Fd::new(1_048_577), Err(FdError::TooLarge(_))));
228    }
229
230    #[test]
231    #[cfg(not(target_os = "linux"))]
232    #[ignore = "On non-Linux platforms, MAX equals i32::MAX so no value can exceed it"]
233    fn test_new_too_large() {
234        assert!(matches!(Fd::new(i32::MAX), Err(FdError::TooLarge(_))));
235    }
236
237    #[test]
238    fn test_try_from_i32() {
239        let fd: Fd = 100i32.try_into().unwrap();
240        assert_eq!(fd.as_i32(), 100);
241    }
242
243    #[test]
244    fn test_try_from_u32() {
245        let fd: Fd = 100u32.try_into().unwrap();
246        assert_eq!(fd.as_i32(), 100);
247    }
248
249    #[test]
250    fn test_try_from_u32_too_large() {
251        assert!(matches!(
252            TryInto::<Fd>::try_into(i32::MAX as u32 + 1),
253            Err(FdError::TooLarge(_))
254        ));
255    }
256
257    #[test]
258    fn test_from_str() {
259        let fd: Fd = "100".parse().unwrap();
260        assert_eq!(fd.as_i32(), 100);
261    }
262
263    #[test]
264    fn test_from_str_error() {
265        assert!(matches!(
266            ("-1").parse::<Fd>(),
267            Err(FdError::InvalidValue(_))
268        ));
269        assert!("abc".parse::<Fd>().is_err());
270    }
271
272    #[test]
273    fn test_display() {
274        let fd = Fd::new(100).unwrap();
275        assert_eq!(format!("{}", fd), "100");
276    }
277
278    #[test]
279    fn test_constants() {
280        assert_eq!(Fd::STDIN.as_i32(), 0);
281        assert_eq!(Fd::STDOUT.as_i32(), 1);
282        assert_eq!(Fd::STDERR.as_i32(), 2);
283    }
284
285    #[test]
286    fn test_is_standard() {
287        assert!(Fd::STDIN.is_standard());
288        assert!(Fd::STDOUT.is_standard());
289        assert!(Fd::STDERR.is_standard());
290        let fd = Fd::new(100).unwrap();
291        assert!(!fd.is_standard());
292    }
293
294    #[test]
295    fn test_is_stdin() {
296        assert!(Fd::STDIN.is_stdin());
297        assert!(!Fd::STDOUT.is_stdin());
298        assert!(!Fd::STDERR.is_stdin());
299    }
300
301    #[test]
302    fn test_is_stdout() {
303        assert!(!Fd::STDIN.is_stdout());
304        assert!(Fd::STDOUT.is_stdout());
305        assert!(!Fd::STDERR.is_stdout());
306    }
307
308    #[test]
309    fn test_is_stderr() {
310        assert!(!Fd::STDIN.is_stderr());
311        assert!(!Fd::STDOUT.is_stderr());
312        assert!(Fd::STDERR.is_stderr());
313    }
314
315    #[test]
316    fn test_clone() {
317        let fd = Fd::new(100).unwrap();
318        let fd2 = fd.clone();
319        assert_eq!(fd, fd2);
320    }
321
322    #[test]
323    fn test_equality() {
324        let f1 = Fd::new(100).unwrap();
325        let f2 = Fd::new(100).unwrap();
326        let f3 = Fd::new(200).unwrap();
327        assert_eq!(f1, f2);
328        assert_ne!(f1, f3);
329    }
330
331    #[test]
332    fn test_ordering() {
333        let f1 = Fd::new(100).unwrap();
334        let f2 = Fd::new(200).unwrap();
335        assert!(f1 < f2);
336        assert!(f2 > f1);
337    }
338
339    #[test]
340    fn test_new_unchecked() {
341        let fd = Fd::new_unchecked(100);
342        assert_eq!(fd.as_i32(), 100);
343    }
344}