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}