Skip to main content

zsh/extensions/
fds.rs

1//! File descriptor utilities for zshrs.
2//!
3//! The top half (`AutoClosePipes`, `heightenize_fd`, `BorrowedFdFile`)
4//! is borrowed from fish-shell's `fds.rs` — these are zshrs-original
5//! safe-wrapper infrastructure with no direct C zsh counterpart.
6//!
7//! The bottom half (`movefd`, `redup`, `zclose`, etc.) is a direct
8//! port of the fd helpers C zsh keeps in `Src/utils.c` for redirection
9//! and pipe management. Each function below is annotated with its
10//! exact C origin.
11
12use std::fs::File;
13use std::io;
14use std::mem::ManuallyDrop;
15use std::ops::{Deref, DerefMut};
16use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
17
18/// The first "high fd", outside the user-specifiable range (>&5).
19/// Mirrors the `FDT_HIGHFD` constant Src/exec.c uses when calling
20/// `movefd()` to keep low fds free for `>&N` user redirection.
21pub const FIRST_HIGH_FD: RawFd = 10;
22
23/// A pair of connected pipe file descriptors.
24/// zshrs-original RAII wrapper (borrowed from fish-shell). C zsh
25/// uses bare `int fds[2]` from `pipe(2)` and `mpipe()`
26/// (Src/exec.c) which gets manual `close(2)` calls on every error
27/// path; the OwnedFd pair makes that automatic.
28pub struct AutoClosePipes {
29    pub read: OwnedFd,
30    pub write: OwnedFd,
31}
32
33/// Create a pair of connected pipes with CLOEXEC set.
34/// zshrs-original wrapper around `pipe(2)` (borrowed from fish-
35/// shell). C zsh's equivalent is `mpipe()` in Src/exec.c which
36/// calls `pipe(2)` then `movefd(fd, 1)` on each end. The Rust path
37/// also sets `FD_CLOEXEC` so descriptors don't leak into child
38/// processes.
39pub fn make_autoclose_pipes() -> io::Result<AutoClosePipes> {
40    let (read_fd, write_fd) =
41        nix::unistd::pipe().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
42
43    // Move fds to high range and set CLOEXEC
44    let read_fd = heightenize_fd(read_fd)?;
45    let write_fd = heightenize_fd(write_fd)?;
46
47    Ok(AutoClosePipes {
48        read: read_fd,
49        write: write_fd,
50    })
51}
52
53/// Move an fd to the high range (>= FIRST_HIGH_FD) and set CLOEXEC.
54/// Equivalent to `movefd()` from Src/utils.c plus an additional
55/// `FD_CLOEXEC` set step. The C source sets `FD_CLOEXEC` indirectly
56/// by registering the fd in `fdtable[]` with `FDT_INTERNAL`.
57fn heightenize_fd(fd: OwnedFd) -> io::Result<OwnedFd> {
58    let raw_fd = fd.as_raw_fd();
59
60    if raw_fd >= FIRST_HIGH_FD {
61        set_cloexec(raw_fd, true)?;
62        return Ok(fd);
63    }
64
65    // Dup to high range with CLOEXEC
66    let new_fd = nix::fcntl::fcntl(raw_fd, nix::fcntl::FcntlArg::F_DUPFD_CLOEXEC(FIRST_HIGH_FD))
67        .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
68
69    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
70}
71
72/// Set or clear CLOEXEC on a file descriptor.
73/// Port of the `fdtable_flocks` close-on-exec set/clear logic
74/// Src/utils.c uses on internal fds — `fcntl(F_GETFD)` + `F_SETFD`
75/// with the bit toggled. The C source doesn't expose this as a
76/// dedicated function; we factor it out for the RAII pipe path.
77pub fn set_cloexec(fd: RawFd, should_set: bool) -> io::Result<()> {
78    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
79    if flags < 0 {
80        return Err(io::Error::last_os_error());
81    }
82
83    let new_flags = if should_set {
84        flags | libc::FD_CLOEXEC
85    } else {
86        flags & !libc::FD_CLOEXEC
87    };
88
89    if flags != new_flags {
90        let result = unsafe { libc::fcntl(fd, libc::F_SETFD, new_flags) };
91        if result < 0 {
92            return Err(io::Error::last_os_error());
93        }
94    }
95
96    Ok(())
97}
98
99/// Make an fd nonblocking.
100/// Port of the `fcntl(fd, F_SETFL, O_NONBLOCK)` idiom Src/utils.c
101/// uses for completion's coroutine fds and for `read -t`.
102pub fn make_fd_nonblocking(fd: RawFd) -> io::Result<()> {
103    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
104    if flags < 0 {
105        return Err(io::Error::last_os_error());
106    }
107
108    if (flags & libc::O_NONBLOCK) == 0 {
109        let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
110        if result < 0 {
111            return Err(io::Error::last_os_error());
112        }
113    }
114
115    Ok(())
116}
117
118/// Make an fd blocking.
119/// Counterpart of `make_fd_nonblocking`. C zsh restores blocking
120/// mode after `read -t` (Src/utils.c) by clearing `O_NONBLOCK`.
121pub fn make_fd_blocking(fd: RawFd) -> io::Result<()> {
122    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
123    if flags < 0 {
124        return Err(io::Error::last_os_error());
125    }
126
127    if (flags & libc::O_NONBLOCK) != 0 {
128        let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
129        if result < 0 {
130            return Err(io::Error::last_os_error());
131        }
132    }
133
134    Ok(())
135}
136
137/// Close a file descriptor, retrying on EINTR.
138/// Port of `zclose(int fd)` from Src/utils.c — same `EINTR` retry loop;
139/// negative fds are treated as already-closed (no-op).
140pub fn close_fd(fd: RawFd) {
141    if fd < 0 {
142        return;
143    }
144    loop {
145        let result = unsafe { libc::close(fd) };
146        if result == 0 {
147            break;
148        }
149        let err = io::Error::last_os_error();
150        if err.raw_os_error() != Some(libc::EINTR) {
151            break;
152        }
153    }
154}
155
156/// Duplicate a file descriptor.
157/// Port of the `dup(2)` call sites in Src/exec.c that copy a saved
158/// fd back into place during `exec >` / `exec <` redirection
159/// teardown.
160pub fn dup_fd(fd: RawFd) -> io::Result<RawFd> {
161    let new_fd = unsafe { libc::dup(fd) };
162    if new_fd < 0 {
163        Err(io::Error::last_os_error())
164    } else {
165        Ok(new_fd)
166    }
167}
168
169/// Duplicate fd to a specific target fd.
170/// Port of the `dup2(2)` calls Src/exec.c uses to install the
171/// shell's pipeline ends on fd 0/1/2 before `execve(2)`.
172pub fn dup2_fd(src: RawFd, dst: RawFd) -> io::Result<()> {
173    if src == dst {
174        return Ok(());
175    }
176    let result = unsafe { libc::dup2(src, dst) };
177    if result < 0 {
178        Err(io::Error::last_os_error())
179    } else {
180        Ok(())
181    }
182}
183
184/// A `File` wrapper that doesn't close on drop (borrows the fd).
185/// zshrs-original (borrowed from fish-shell). C zsh shares fds
186/// across builtins with bare `int fd` since lifetime is implicit;
187/// Rust needs an explicit RAII handle that dups the fd without
188/// transferring ownership.
189pub struct BorrowedFdFile(ManuallyDrop<File>);
190
191impl Deref for BorrowedFdFile {
192    type Target = File;
193    fn deref(&self) -> &Self::Target {
194        &self.0
195    }
196}
197
198impl DerefMut for BorrowedFdFile {
199    fn deref_mut(&mut self) -> &mut Self::Target {
200        &mut self.0
201    }
202}
203
204impl FromRawFd for BorrowedFdFile {
205    unsafe fn from_raw_fd(fd: RawFd) -> Self {
206        Self(ManuallyDrop::new(unsafe { File::from_raw_fd(fd) }))
207    }
208}
209
210impl AsRawFd for BorrowedFdFile {
211    fn as_raw_fd(&self) -> RawFd {
212        self.0.as_raw_fd()
213    }
214}
215
216impl IntoRawFd for BorrowedFdFile {
217    fn into_raw_fd(self) -> RawFd {
218        ManuallyDrop::into_inner(self.0).into_raw_fd()
219    }
220}
221
222impl Clone for BorrowedFdFile {
223    fn clone(&self) -> Self {
224        unsafe { Self::from_raw_fd(self.as_raw_fd()) }
225    }
226}
227
228impl BorrowedFdFile {
229    pub fn stdin() -> Self {
230        unsafe { Self::from_raw_fd(libc::STDIN_FILENO) }
231    }
232
233    pub fn stdout() -> Self {
234        unsafe { Self::from_raw_fd(libc::STDOUT_FILENO) }
235    }
236
237    pub fn stderr() -> Self {
238        unsafe { Self::from_raw_fd(libc::STDERR_FILENO) }
239    }
240}
241
242impl io::Read for BorrowedFdFile {
243    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
244        self.deref_mut().read(buf)
245    }
246}
247
248impl io::Write for BorrowedFdFile {
249    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
250        self.deref_mut().write(buf)
251    }
252
253    fn flush(&mut self) -> io::Result<()> {
254        self.deref_mut().flush()
255    }
256}
257
258// ============================================================================
259// Port from zsh/Src/utils.c: File descriptor table and management
260// ============================================================================
261
262/// File descriptor type constants.
263/// Port of the `FDT_*` enum from `Src/zsh.h` — the C source uses
264/// these to tag every fd in `fdtable[]` so `closem()` (Src/utils.c)
265/// can decide which fds to leave open across `exec(2)` / `fork(2)`.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum FdType {
268    Unused = 0,
269    External = 1,
270    Internal = 2,
271    Module = 3,
272    Flock = 4,
273    FlockExec = 5,
274    Proc = 6,
275}
276
277/// Move a file descriptor to >= `FIRST_HIGH_FD` so the low fds 0-9
278/// stay free for user redirection.
279/// Port of `movefd(int fd)` from Src/utils.c (~lines 1980-2012). Uses
280/// `fcntl(F_DUPFD_CLOEXEC)` to get the new fd in the high range
281/// then closes the original.
282pub fn movefd(fd: RawFd) -> RawFd {
283    if !(0..FIRST_HIGH_FD).contains(&fd) {
284        return fd;
285    }
286
287    unsafe {
288        let new_fd = libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, FIRST_HIGH_FD);
289        if new_fd != -1 {
290            libc::close(fd);
291            return new_fd;
292        }
293    }
294    fd
295}
296
297/// Duplicate fd `x` to `y`. If `x == -1`, fd `y` is closed.
298/// Port of `redup(int x, int y)` from Src/utils.c (~lines 2019-2068) — the C
299/// source's "move fd x onto y, closing x" primitive that the
300/// pipeline-builder calls for every redirection.
301pub fn redup(x: RawFd, y: RawFd) -> RawFd {
302    if x < 0 {
303        unsafe { libc::close(y) };
304        return y;
305    }
306
307    if x == y {
308        return y;
309    }
310
311    let result = unsafe { libc::dup2(x, y) };
312    if result == -1 {
313        return -1;
314    }
315
316    unsafe { libc::close(x) };
317    y
318}
319
320/// Close a file descriptor.
321/// Port of `zclose(int fd)` from Src/utils.c (~lines 2126-2148) — the
322/// safe-close shim that no-ops on negative fds and clears the
323/// fdtable entry the C source maintains for `closem()` accounting.
324pub fn zclose(fd: RawFd) -> i32 {
325    if fd >= 0 {
326        unsafe { libc::close(fd) }
327    } else {
328        -1
329    }
330}
331
332/// Duplicate a file descriptor.
333/// Port of the bare `dup(2)` calls Src/utils.c sprinkles around
334/// the redirection-save/restore code. Negative input returns -1
335/// (matches the C source's no-op-on-bad-fd convention).
336pub fn zdup(fd: RawFd) -> RawFd {
337    if fd < 0 {
338        return -1;
339    }
340    unsafe { libc::dup(fd) }
341}
342
343/// Check if a file descriptor is open.
344/// zshrs-original convenience — equivalent to the
345/// `fcntl(fd, F_GETFD)` probe Src/utils.c uses inline before
346/// touching an fd that may have been closed by a parallel branch.
347pub fn fd_is_open(fd: RawFd) -> bool {
348    if fd < 0 {
349        return false;
350    }
351    unsafe { libc::fcntl(fd, libc::F_GETFD, 0) >= 0 }
352}
353
354// ============================================================================
355// Tests
356// ============================================================================
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_make_autoclose_pipes() {
364        let pipes = make_autoclose_pipes().expect("Failed to create pipes");
365
366        // Both fds should be in the high range
367        assert!(pipes.read.as_raw_fd() >= FIRST_HIGH_FD);
368        assert!(pipes.write.as_raw_fd() >= FIRST_HIGH_FD);
369
370        // Both should have CLOEXEC set
371        let read_flags = unsafe { libc::fcntl(pipes.read.as_raw_fd(), libc::F_GETFD, 0) };
372        let write_flags = unsafe { libc::fcntl(pipes.write.as_raw_fd(), libc::F_GETFD, 0) };
373
374        assert!(read_flags >= 0);
375        assert!(write_flags >= 0);
376        assert_ne!(read_flags & libc::FD_CLOEXEC, 0);
377        assert_ne!(write_flags & libc::FD_CLOEXEC, 0);
378    }
379
380    #[test]
381    fn test_set_cloexec() {
382        let file = std::fs::File::open("/dev/null").unwrap();
383        let fd = file.as_raw_fd();
384
385        // Set CLOEXEC
386        set_cloexec(fd, true).unwrap();
387        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
388        assert_ne!(flags & libc::FD_CLOEXEC, 0);
389
390        // Clear CLOEXEC
391        set_cloexec(fd, false).unwrap();
392        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
393        assert_eq!(flags & libc::FD_CLOEXEC, 0);
394    }
395
396    #[test]
397    fn test_nonblocking() {
398        let file = std::fs::File::open("/dev/null").unwrap();
399        let fd = file.as_raw_fd();
400
401        // Make nonblocking
402        make_fd_nonblocking(fd).unwrap();
403        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
404        assert_ne!(flags & libc::O_NONBLOCK, 0);
405
406        // Make blocking again
407        make_fd_blocking(fd).unwrap();
408        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
409        assert_eq!(flags & libc::O_NONBLOCK, 0);
410    }
411
412    #[test]
413    fn test_borrowed_fd_file_does_not_close() {
414        let file = std::fs::File::open("/dev/null").unwrap();
415        let fd = file.as_raw_fd();
416
417        // Create borrowed file and let it fall out of scope. Direct
418        // `drop()` would tickle clippy::drop_non_drop because
419        // BorrowedFdFile has no Drop impl by design — the whole
420        // point of this type is that scope-end is a no-op for the
421        // underlying fd. The inner block makes the lifetime
422        // boundary explicit.
423        {
424            let _borrowed = unsafe { BorrowedFdFile::from_raw_fd(fd) };
425        }
426
427        // fd should still be valid
428        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
429        assert!(
430            flags >= 0,
431            "fd should still be valid after dropping BorrowedFdFile"
432        );
433
434        // Now drop the original file
435        drop(file);
436
437        // fd should now be invalid
438        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
439        assert!(
440            flags < 0,
441            "fd should be invalid after dropping original File"
442        );
443    }
444}