1use core::fmt;
7use core::str::FromStr;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[non_exhaustive]
16pub enum FdError {
17 InvalidValue(i32),
22 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#[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 #[cfg(target_os = "linux")]
69 pub const MAX: i32 = 1_048_576;
70
71 #[cfg(not(target_os = "linux"))]
76 pub const MAX: i32 = i32::MAX;
77
78 pub const STDIN: Self = Self(0);
80
81 pub const STDOUT: Self = Self(1);
83
84 pub const STDERR: Self = Self(2);
86
87 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 #[must_use]
119 #[inline]
120 pub const fn new_unchecked(value: i32) -> Self {
121 Self(value)
122 }
123
124 #[must_use]
126 #[inline]
127 pub const fn as_i32(&self) -> i32 {
128 self.0
129 }
130
131 #[must_use]
133 #[inline]
134 pub const fn is_standard(&self) -> bool {
135 self.0 >= 0 && self.0 <= 2
136 }
137
138 #[must_use]
140 #[inline]
141 pub const fn is_stdin(&self) -> bool {
142 self.0 == 0
143 }
144
145 #[must_use]
147 #[inline]
148 pub const fn is_stdout(&self) -> bool {
149 self.0 == 1
150 }
151
152 #[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}