rush_sh/state/
fd_table.rs

1//! File Descriptor Table Management
2//!
3//! This module provides the file descriptor table implementation for managing
4//! open file descriptors in the Rush shell. The FD table is a critical component
5//! for I/O redirection operations, allowing the shell to:
6//!
7//! - Open files and assign them to specific file descriptor numbers
8//! - Duplicate file descriptors (e.g., `N>&M`, `N<&M`)
9//! - Close file descriptors (e.g., `N>&-`, `N<&-`)
10//! - Save and restore file descriptors for subshells and command groups
11//!
12//! ## File Descriptor Operations
13//!
14//! The [`FileDescriptorTable`] supports the following operations:
15//!
16//! - **Opening**: Open a file with specific read/write/append/truncate modes
17//! - **Duplication**: Duplicate one FD to another (POSIX dup2 semantics)
18//! - **Closing**: Mark an FD as explicitly closed
19//! - **Save/Restore**: Save current FD state and restore it later (for subshells)
20//!
21//! ## Subshell Support
22//!
23//! The FD table provides save/restore functionality that is essential for proper
24//! subshell execution. When entering a subshell:
25//!
26//! 1. Call [`FileDescriptorTable::save_all_fds`] to save the current state
27//! 2. Execute subshell commands (which may modify FDs)
28//! 3. Call [`FileDescriptorTable::restore_all_fds`] to restore the original state
29//!
30//! This ensures that FD modifications in subshells don't affect the parent shell.
31//!
32//! ## Example
33//!
34//! ```rust
35//! use rush_sh::state::FileDescriptorTable;
36//! use std::fs;
37//!
38//! let mut fd_table = FileDescriptorTable::new();
39//!
40//! // Create a temporary file for the example
41//! let temp_file = "/tmp/rush_fd_example.txt";
42//! fs::write(temp_file, "test content").unwrap();
43//!
44//! // Open a file for reading on FD 3
45//! fd_table.open_fd(3, temp_file, true, false, false, false, false).unwrap();
46//!
47//! // Duplicate FD 3 to FD 4
48//! fd_table.duplicate_fd(3, 4).unwrap();
49//!
50//! // Close FD 3
51//! fd_table.close_fd(3).unwrap();
52//!
53//! // FD 4 still has access to the file
54//! assert!(fd_table.is_open(4));
55//!
56//! // Clean up
57//! let _ = fs::remove_file(temp_file);
58//! ```
59
60use std::collections::HashMap;
61use std::fs::{File, OpenOptions};
62use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
63use std::process::Stdio;
64
65/// Represents an open file descriptor
66#[derive(Debug)]
67pub enum FileDescriptor {
68    /// Standard file opened for reading, writing, or both
69    File(File),
70    /// Duplicate of another file descriptor
71    Duplicate(RawFd),
72    /// Closed file descriptor
73    Closed,
74}
75
76impl FileDescriptor {
77    pub fn try_clone(&self) -> Result<Self, String> {
78        match self {
79            FileDescriptor::File(f) => {
80                let new_file = f
81                    .try_clone()
82                    .map_err(|e| format!("Failed to clone file: {}", e))?;
83                Ok(FileDescriptor::File(new_file))
84            }
85            FileDescriptor::Duplicate(fd) => Ok(FileDescriptor::Duplicate(*fd)),
86            FileDescriptor::Closed => Ok(FileDescriptor::Closed),
87        }
88    }
89}
90
91/// File descriptor table for managing open file descriptors
92#[derive(Debug)]
93pub struct FileDescriptorTable {
94    /// Map of fd number to file descriptor
95    fds: HashMap<i32, FileDescriptor>,
96    /// Saved file descriptors for restoration after command execution
97    saved_fds: HashMap<i32, RawFd>,
98}
99
100impl FileDescriptorTable {
101    /// Create a new empty file descriptor table
102    pub fn new() -> Self {
103        Self {
104            fds: HashMap::new(),
105            saved_fds: HashMap::new(),
106        }
107    }
108
109    /// Open a file and assign it to a file descriptor number
110    ///
111    /// # Arguments
112    /// * `fd_num` - The file descriptor number (0-9)
113    /// * `path` - Path to the file to open
114    /// * `read` - Whether to open for reading
115    /// * `write` - Whether to open for writing
116    /// * `append` - Whether to open in append mode
117    /// * `truncate` - Whether to truncate the file
118    ///
119    /// # Returns
120    /// * `Ok(())` on success
121    /// * `Err(String)` with error message on failure
122    #[allow(clippy::too_many_arguments)]
123    pub fn open_fd(
124        &mut self,
125        fd_num: i32,
126        path: &str,
127        read: bool,
128        write: bool,
129        append: bool,
130        truncate: bool,
131        create_new: bool,
132    ) -> Result<(), String> {
133        let mut opts = OpenOptions::new();
134        if create_new {
135            opts.create_new(true); // Atomic check-and-create
136        } else if truncate {
137            opts.create(true).truncate(true);
138        }
139
140        // Validate fd number
141        if !(0..=1024).contains(&fd_num) {
142            return Err(format!("Invalid file descriptor number: {}", fd_num));
143        }
144
145        // Open the file with the specified options
146        let file = OpenOptions::new()
147            .read(read)
148            .write(write)
149            .append(append)
150            .truncate(truncate)
151            .create(write || append)
152            .open(path)
153            .map_err(|e| format!("Cannot open {}: {}", path, e))?;
154
155        // Store the file descriptor
156        self.fds.insert(fd_num, FileDescriptor::File(file));
157        Ok(())
158    }
159
160    /// Duplicate a file descriptor
161    ///
162    /// # Arguments
163    /// * `source_fd` - The source file descriptor to duplicate
164    /// * `target_fd` - The target file descriptor number
165    ///
166    /// # Returns
167    /// * `Ok(())` on success
168    /// * `Err(String)` with error message on failure
169    pub fn duplicate_fd(&mut self, source_fd: i32, target_fd: i32) -> Result<(), String> {
170        // Validate fd numbers
171        if !(0..=1024).contains(&source_fd) {
172            return Err(format!("Invalid source file descriptor: {}", source_fd));
173        }
174        if !(0..=1024).contains(&target_fd) {
175            return Err(format!("Invalid target file descriptor: {}", target_fd));
176        }
177
178        // POSIX: Duplicating to self is a no-op
179        if source_fd == target_fd {
180            return Ok(());
181        }
182
183        // Get the raw fd to duplicate
184        let raw_fd = match self.get_raw_fd(source_fd) {
185            Some(fd) => fd,
186            None => {
187                return Err(format!(
188                    "File descriptor {} is not open or is closed",
189                    source_fd
190                ));
191            }
192        };
193
194        // Store the duplication
195        self.fds
196            .insert(target_fd, FileDescriptor::Duplicate(raw_fd));
197        Ok(())
198    }
199
200    /// Close a file descriptor
201    ///
202    /// # Arguments
203    /// * `fd_num` - The file descriptor number to close
204    ///
205    /// # Returns
206    /// * `Ok(())` on success
207    /// * `Err(String)` with error message on failure
208    pub fn close_fd(&mut self, fd_num: i32) -> Result<(), String> {
209        // Validate fd number
210        if !(0..=1024).contains(&fd_num) {
211            return Err(format!("Invalid file descriptor number: {}", fd_num));
212        }
213
214        // Mark the fd as closed
215        self.fds.insert(fd_num, FileDescriptor::Closed);
216        Ok(())
217    }
218
219    /// Save the current state of a file descriptor for later restoration
220    ///
221    /// # Arguments
222    /// * `fd_num` - The file descriptor number to save
223    ///
224    /// # Returns
225    /// * `Ok(())` on success
226    /// * `Err(String)` with error message on failure
227    pub fn save_fd(&mut self, fd_num: i32) -> Result<(), String> {
228        // Validate fd number
229        if !(0..=1024).contains(&fd_num) {
230            return Err(format!("Invalid file descriptor number: {}", fd_num));
231        }
232
233        // Duplicate the fd using dup() syscall to save it
234        let saved_fd = unsafe {
235            let raw_fd = fd_num as RawFd;
236            libc::dup(raw_fd)
237        };
238
239        if saved_fd < 0 {
240            return Err(format!("Failed to save file descriptor {}", fd_num));
241        }
242
243        self.saved_fds.insert(fd_num, saved_fd);
244        Ok(())
245    }
246
247    /// Restore a previously saved file descriptor
248    ///
249    /// # Arguments
250    /// * `fd_num` - The file descriptor number to restore
251    ///
252    /// # Returns
253    /// * `Ok(())` on success
254    /// * `Err(String)` with error message on failure
255    pub fn restore_fd(&mut self, fd_num: i32) -> Result<(), String> {
256        // Validate fd number
257        if !(0..=1024).contains(&fd_num) {
258            return Err(format!("Invalid file descriptor number: {}", fd_num));
259        }
260
261        // Get the saved fd
262        if let Some(saved_fd) = self.saved_fds.remove(&fd_num) {
263            // Restore using dup2() syscall
264            unsafe {
265                let result = libc::dup2(saved_fd, fd_num as RawFd);
266                libc::close(saved_fd); // Close the saved fd
267
268                if result < 0 {
269                    return Err(format!("Failed to restore file descriptor {}", fd_num));
270                }
271            }
272
273            // Remove from our tracking
274            self.fds.remove(&fd_num);
275        }
276
277        Ok(())
278    }
279
280    /// Create a deep copy of the file descriptor table
281    /// This duplicates all open file descriptors so they are independent of the original table
282    pub fn deep_clone(&self) -> Result<Self, String> {
283        let mut new_fds = HashMap::new();
284        for (fd, descriptor) in &self.fds {
285            new_fds.insert(*fd, descriptor.try_clone()?);
286        }
287
288        Ok(Self {
289            fds: new_fds,
290            saved_fds: self.saved_fds.clone(),
291        })
292    }
293
294    /// Save all currently open file descriptors
295    ///
296    /// # Returns
297    /// * `Ok(())` on success
298    /// * `Err(String)` with error message on failure
299    pub fn save_all_fds(&mut self) -> Result<(), String> {
300        // Save all fds that we're tracking
301        let fd_nums: Vec<i32> = self.fds.keys().copied().collect();
302        for fd_num in fd_nums {
303            self.save_fd(fd_num)?;
304        }
305
306        // Also explicitly save standard FDs (0, 1, 2) if they aren't already tracked
307        // This ensures changes to standard streams (via CommandGroup etc.) can be restored
308        for fd in 0..=2 {
309            if !self.fds.contains_key(&fd) {
310                // Try to save, ignore error if fd is closed/invalid
311                let _ = self.save_fd(fd);
312            }
313        }
314        Ok(())
315    }
316
317    /// Restore all previously saved file descriptors
318    ///
319    /// # Returns
320    /// * `Ok(())` on success
321    /// * `Err(String)` with error message on failure
322    pub fn restore_all_fds(&mut self) -> Result<(), String> {
323        // Restore all saved fds
324        let fd_nums: Vec<i32> = self.saved_fds.keys().copied().collect();
325        for fd_num in fd_nums {
326            self.restore_fd(fd_num)?;
327        }
328        Ok(())
329    }
330
331    /// Get a file handle for a given file descriptor number
332    ///
333    /// # Arguments
334    /// * `fd_num` - The file descriptor number
335    ///
336    /// # Returns
337    /// * `Some(Stdio)` if the fd is open and can be converted to Stdio
338    /// * `None` if the fd is not open or is closed
339    #[allow(dead_code)]
340    pub fn get_stdio(&self, fd_num: i32) -> Option<Stdio> {
341        match self.fds.get(&fd_num) {
342            Some(FileDescriptor::File(file)) => {
343                // Try to duplicate the file descriptor for Stdio
344                let raw_fd = file.as_raw_fd();
345                let dup_fd = unsafe { libc::dup(raw_fd) };
346                if dup_fd >= 0 {
347                    let file = unsafe { File::from_raw_fd(dup_fd) };
348                    Some(Stdio::from(file))
349                } else {
350                    None
351                }
352            }
353            Some(FileDescriptor::Duplicate(raw_fd)) => {
354                // Duplicate the raw fd for Stdio
355                let dup_fd = unsafe { libc::dup(*raw_fd) };
356                if dup_fd >= 0 {
357                    let file = unsafe { File::from_raw_fd(dup_fd) };
358                    Some(Stdio::from(file))
359                } else {
360                    None
361                }
362            }
363            Some(FileDescriptor::Closed) | None => None,
364        }
365    }
366
367    /// Get the raw file descriptor number for a given fd
368    ///
369    /// # Arguments
370    /// * `fd_num` - The file descriptor number
371    ///
372    /// # Returns
373    /// * `Some(RawFd)` if the fd is open
374    /// * `None` if the fd is not open or is closed
375    pub fn get_raw_fd(&self, fd_num: i32) -> Option<RawFd> {
376        match self.fds.get(&fd_num) {
377            Some(FileDescriptor::File(file)) => Some(file.as_raw_fd()),
378            Some(FileDescriptor::Duplicate(raw_fd)) => Some(*raw_fd),
379            Some(FileDescriptor::Closed) => None,
380            None => {
381                // Standard file descriptors (0, 1, 2) are always open unless explicitly closed
382                if (0..=2).contains(&fd_num) {
383                    Some(fd_num as RawFd)
384                } else {
385                    None
386                }
387            }
388        }
389    }
390
391    /// Check if a file descriptor is open
392    ///
393    /// # Arguments
394    /// * `fd_num` - The file descriptor number
395    ///
396    /// # Returns
397    /// * `true` if the fd is open
398    /// * `false` if the fd is closed or not tracked
399    pub fn is_open(&self, fd_num: i32) -> bool {
400        matches!(
401            self.fds.get(&fd_num),
402            Some(FileDescriptor::File(_)) | Some(FileDescriptor::Duplicate(_))
403        )
404    }
405
406    /// Check if a file descriptor is closed
407    ///
408    /// # Arguments
409    /// * `fd_num` - The file descriptor number
410    ///
411    /// # Returns
412    /// * `true` if the fd is explicitly closed
413    /// * `false` otherwise
414    pub fn is_closed(&self, fd_num: i32) -> bool {
415        matches!(self.fds.get(&fd_num), Some(FileDescriptor::Closed))
416    }
417
418    /// Clear all file descriptors and saved state
419    pub fn clear(&mut self) {
420        self.fds.clear();
421        self.saved_fds.clear();
422    }
423}
424
425impl Default for FileDescriptorTable {
426    /// Creates the default FileDescriptorTable.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use rush_sh::state::FileDescriptorTable;
432    /// let table = FileDescriptorTable::default();
433    /// ```
434    fn default() -> Self {
435        Self::new()
436    }
437}