Skip to main content

zsh/
fds.rs

1//! File descriptor utilities for zshrs
2//!
3//! Based on fish-shell's fds.rs, providing safe fd management.
4
5use std::fs::File;
6use std::io;
7use std::mem::ManuallyDrop;
8use std::ops::{Deref, DerefMut};
9use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
10
11/// The first "high fd", outside the user-specifiable range (>&5).
12pub const FIRST_HIGH_FD: RawFd = 10;
13
14/// A pair of connected pipe file descriptors.
15pub struct AutoClosePipes {
16    pub read: OwnedFd,
17    pub write: OwnedFd,
18}
19
20/// Create a pair of connected pipes with CLOEXEC set.
21/// Returns None on failure.
22pub fn make_autoclose_pipes() -> io::Result<AutoClosePipes> {
23    let (read_fd, write_fd) =
24        nix::unistd::pipe().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
25
26    // Move fds to high range and set CLOEXEC
27    let read_fd = heightenize_fd(read_fd)?;
28    let write_fd = heightenize_fd(write_fd)?;
29
30    Ok(AutoClosePipes {
31        read: read_fd,
32        write: write_fd,
33    })
34}
35
36/// Move an fd to the high range (>= FIRST_HIGH_FD) and set CLOEXEC.
37fn heightenize_fd(fd: OwnedFd) -> io::Result<OwnedFd> {
38    let raw_fd = fd.as_raw_fd();
39
40    if raw_fd >= FIRST_HIGH_FD {
41        set_cloexec(raw_fd, true)?;
42        return Ok(fd);
43    }
44
45    // Dup to high range with CLOEXEC
46    let new_fd = nix::fcntl::fcntl(raw_fd, nix::fcntl::FcntlArg::F_DUPFD_CLOEXEC(FIRST_HIGH_FD))
47        .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
48
49    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
50}
51
52/// Set or clear CLOEXEC on a file descriptor.
53pub fn set_cloexec(fd: RawFd, should_set: bool) -> io::Result<()> {
54    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
55    if flags < 0 {
56        return Err(io::Error::last_os_error());
57    }
58
59    let new_flags = if should_set {
60        flags | libc::FD_CLOEXEC
61    } else {
62        flags & !libc::FD_CLOEXEC
63    };
64
65    if flags != new_flags {
66        let result = unsafe { libc::fcntl(fd, libc::F_SETFD, new_flags) };
67        if result < 0 {
68            return Err(io::Error::last_os_error());
69        }
70    }
71
72    Ok(())
73}
74
75/// Make an fd nonblocking.
76pub fn make_fd_nonblocking(fd: RawFd) -> io::Result<()> {
77    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
78    if flags < 0 {
79        return Err(io::Error::last_os_error());
80    }
81
82    if (flags & libc::O_NONBLOCK) == 0 {
83        let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
84        if result < 0 {
85            return Err(io::Error::last_os_error());
86        }
87    }
88
89    Ok(())
90}
91
92/// Make an fd blocking.
93pub fn make_fd_blocking(fd: RawFd) -> io::Result<()> {
94    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
95    if flags < 0 {
96        return Err(io::Error::last_os_error());
97    }
98
99    if (flags & libc::O_NONBLOCK) != 0 {
100        let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
101        if result < 0 {
102            return Err(io::Error::last_os_error());
103        }
104    }
105
106    Ok(())
107}
108
109/// Close a file descriptor, retrying on EINTR.
110pub fn close_fd(fd: RawFd) {
111    if fd < 0 {
112        return;
113    }
114    loop {
115        let result = unsafe { libc::close(fd) };
116        if result == 0 {
117            break;
118        }
119        let err = io::Error::last_os_error();
120        if err.raw_os_error() != Some(libc::EINTR) {
121            break;
122        }
123    }
124}
125
126/// Duplicate a file descriptor.
127pub fn dup_fd(fd: RawFd) -> io::Result<RawFd> {
128    let new_fd = unsafe { libc::dup(fd) };
129    if new_fd < 0 {
130        Err(io::Error::last_os_error())
131    } else {
132        Ok(new_fd)
133    }
134}
135
136/// Duplicate fd to a specific target fd.
137pub fn dup2_fd(src: RawFd, dst: RawFd) -> io::Result<()> {
138    if src == dst {
139        return Ok(());
140    }
141    let result = unsafe { libc::dup2(src, dst) };
142    if result < 0 {
143        Err(io::Error::last_os_error())
144    } else {
145        Ok(())
146    }
147}
148
149/// A File wrapper that doesn't close on drop (borrows the fd).
150pub struct BorrowedFdFile(ManuallyDrop<File>);
151
152impl Deref for BorrowedFdFile {
153    type Target = File;
154    fn deref(&self) -> &Self::Target {
155        &self.0
156    }
157}
158
159impl DerefMut for BorrowedFdFile {
160    fn deref_mut(&mut self) -> &mut Self::Target {
161        &mut self.0
162    }
163}
164
165impl FromRawFd for BorrowedFdFile {
166    unsafe fn from_raw_fd(fd: RawFd) -> Self {
167        Self(ManuallyDrop::new(unsafe { File::from_raw_fd(fd) }))
168    }
169}
170
171impl AsRawFd for BorrowedFdFile {
172    fn as_raw_fd(&self) -> RawFd {
173        self.0.as_raw_fd()
174    }
175}
176
177impl IntoRawFd for BorrowedFdFile {
178    fn into_raw_fd(self) -> RawFd {
179        ManuallyDrop::into_inner(self.0).into_raw_fd()
180    }
181}
182
183impl Clone for BorrowedFdFile {
184    fn clone(&self) -> Self {
185        unsafe { Self::from_raw_fd(self.as_raw_fd()) }
186    }
187}
188
189impl BorrowedFdFile {
190    pub fn stdin() -> Self {
191        unsafe { Self::from_raw_fd(libc::STDIN_FILENO) }
192    }
193
194    pub fn stdout() -> Self {
195        unsafe { Self::from_raw_fd(libc::STDOUT_FILENO) }
196    }
197
198    pub fn stderr() -> Self {
199        unsafe { Self::from_raw_fd(libc::STDERR_FILENO) }
200    }
201}
202
203impl io::Read for BorrowedFdFile {
204    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
205        self.deref_mut().read(buf)
206    }
207}
208
209impl io::Write for BorrowedFdFile {
210    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
211        self.deref_mut().write(buf)
212    }
213
214    fn flush(&mut self) -> io::Result<()> {
215        self.deref_mut().flush()
216    }
217}
218
219// ============================================================================
220// Port from zsh/Src/utils.c: File descriptor table and management
221// ============================================================================
222
223/// File descriptor type constants (from zsh.h FDT_*)
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum FdType {
226    Unused = 0,
227    External = 1,
228    Internal = 2,
229    Module = 3,
230    Flock = 4,
231    FlockExec = 5,
232    Proc = 6,
233}
234
235/// Move file descriptor to >= 10 to keep low fds free for user redirection
236/// Port from zsh/Src/utils.c movefd() lines 1980-2012
237pub fn movefd(fd: RawFd) -> RawFd {
238    if fd < 0 || fd >= FIRST_HIGH_FD {
239        return fd;
240    }
241
242    unsafe {
243        let new_fd = libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, FIRST_HIGH_FD);
244        if new_fd != -1 {
245            libc::close(fd);
246            return new_fd;
247        }
248    }
249    fd
250}
251
252/// Duplicate fd x to y. If x == -1, fd y is closed.
253/// Port from zsh/Src/utils.c redup() lines 2019-2068
254pub fn redup(x: RawFd, y: RawFd) -> RawFd {
255    if x < 0 {
256        unsafe { libc::close(y) };
257        return y;
258    }
259
260    if x == y {
261        return y;
262    }
263
264    let result = unsafe { libc::dup2(x, y) };
265    if result == -1 {
266        return -1;
267    }
268
269    unsafe { libc::close(x) };
270    y
271}
272
273/// Close a file descriptor
274/// Port from zsh/Src/utils.c zclose() lines 2126-2148
275pub fn zclose(fd: RawFd) -> i32 {
276    if fd >= 0 {
277        unsafe { libc::close(fd) }
278    } else {
279        -1
280    }
281}
282
283/// Duplicate file descriptor
284pub fn zdup(fd: RawFd) -> RawFd {
285    if fd < 0 {
286        return -1;
287    }
288    unsafe { libc::dup(fd) }
289}
290
291/// Check if file descriptor is open
292pub fn fd_is_open(fd: RawFd) -> bool {
293    if fd < 0 {
294        return false;
295    }
296    unsafe { libc::fcntl(fd, libc::F_GETFD, 0) >= 0 }
297}
298
299// ============================================================================
300// Tests
301// ============================================================================
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_make_autoclose_pipes() {
309        let pipes = make_autoclose_pipes().expect("Failed to create pipes");
310
311        // Both fds should be in the high range
312        assert!(pipes.read.as_raw_fd() >= FIRST_HIGH_FD);
313        assert!(pipes.write.as_raw_fd() >= FIRST_HIGH_FD);
314
315        // Both should have CLOEXEC set
316        let read_flags = unsafe { libc::fcntl(pipes.read.as_raw_fd(), libc::F_GETFD, 0) };
317        let write_flags = unsafe { libc::fcntl(pipes.write.as_raw_fd(), libc::F_GETFD, 0) };
318
319        assert!(read_flags >= 0);
320        assert!(write_flags >= 0);
321        assert_ne!(read_flags & libc::FD_CLOEXEC, 0);
322        assert_ne!(write_flags & libc::FD_CLOEXEC, 0);
323    }
324
325    #[test]
326    fn test_set_cloexec() {
327        let file = std::fs::File::open("/dev/null").unwrap();
328        let fd = file.as_raw_fd();
329
330        // Set CLOEXEC
331        set_cloexec(fd, true).unwrap();
332        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
333        assert_ne!(flags & libc::FD_CLOEXEC, 0);
334
335        // Clear CLOEXEC
336        set_cloexec(fd, false).unwrap();
337        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
338        assert_eq!(flags & libc::FD_CLOEXEC, 0);
339    }
340
341    #[test]
342    fn test_nonblocking() {
343        let file = std::fs::File::open("/dev/null").unwrap();
344        let fd = file.as_raw_fd();
345
346        // Make nonblocking
347        make_fd_nonblocking(fd).unwrap();
348        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
349        assert_ne!(flags & libc::O_NONBLOCK, 0);
350
351        // Make blocking again
352        make_fd_blocking(fd).unwrap();
353        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
354        assert_eq!(flags & libc::O_NONBLOCK, 0);
355    }
356
357    #[test]
358    fn test_borrowed_fd_file_does_not_close() {
359        let file = std::fs::File::open("/dev/null").unwrap();
360        let fd = file.as_raw_fd();
361
362        // Create borrowed file and drop it
363        let borrowed = unsafe { BorrowedFdFile::from_raw_fd(fd) };
364        drop(borrowed);
365
366        // fd should still be valid
367        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
368        assert!(
369            flags >= 0,
370            "fd should still be valid after dropping BorrowedFdFile"
371        );
372
373        // Now drop the original file
374        drop(file);
375
376        // fd should now be invalid
377        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
378        assert!(
379            flags < 0,
380            "fd should be invalid after dropping original File"
381        );
382    }
383}