Skip to main content

brush_core/
openfiles.rs

1//! Managing files open within a shell instance.
2
3use std::collections::HashMap;
4use std::io::IsTerminal;
5use std::process::Stdio;
6
7use crate::ShellFd;
8use crate::error;
9use crate::ioutils;
10use crate::sys;
11
12/// A trait representing a stream that can be read from and written to.
13/// This is used for custom stream implementations in `OpenFile`.
14///
15/// Types that implement this trait are expected to be cloneable via the
16/// `clone_box` function.
17pub trait Stream: std::io::Read + std::io::Write + Send + Sync {
18    /// Clones the stream into a boxed trait object.
19    fn clone_box(&self) -> Box<dyn Stream>;
20
21    /// Converts the stream into an `OwnedFd`. Returns an error if the operation
22    /// is not supported or if it fails.
23    #[cfg(unix)]
24    fn try_clone_to_owned(&self) -> Result<std::os::fd::OwnedFd, error::Error>;
25
26    /// Borrows the stream as a `BorrowedFd`. Returns an error if the operation
27    /// is not supported or if it fails.
28    #[cfg(unix)]
29    fn try_borrow_as_fd(&self) -> Result<std::os::fd::BorrowedFd<'_>, error::Error>;
30}
31
32/// Represents a file open in a shell context.
33pub enum OpenFile {
34    /// The original standard input this process was started with.
35    Stdin(std::io::Stdin),
36    /// The original standard output this process was started with.
37    Stdout(std::io::Stdout),
38    /// The original standard error this process was started with.
39    Stderr(std::io::Stderr),
40    /// A file open for reading or writing.
41    File(std::fs::File),
42    /// A read end of a pipe.
43    PipeReader(std::io::PipeReader),
44    /// A write end of a pipe.
45    PipeWriter(std::io::PipeWriter),
46    /// A custom stream.
47    Stream(Box<dyn Stream>),
48}
49
50#[cfg(feature = "serde")]
51impl serde::Serialize for OpenFile {
52    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: serde::Serializer,
55    {
56        match self {
57            Self::Stdin(_) => serializer.serialize_str("stdin"),
58            Self::Stdout(_) => serializer.serialize_str("stdout"),
59            Self::Stderr(_) => serializer.serialize_str("stderr"),
60            Self::File(_) => serializer.serialize_str("file"),
61            Self::PipeReader(_) => serializer.serialize_str("pipe_reader"),
62            Self::PipeWriter(_) => serializer.serialize_str("pipe_writer"),
63            Self::Stream(_) => serializer.serialize_str("stream"),
64        }
65    }
66}
67
68#[cfg(feature = "serde")]
69impl<'de> serde::Deserialize<'de> for OpenFile {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: serde::Deserializer<'de>,
73    {
74        match String::deserialize(deserializer)?.as_str() {
75            "stdin" => return Ok(std::io::stdin().into()),
76            "stdout" => return Ok(std::io::stdout().into()),
77            "stderr" => return Ok(std::io::stderr().into()),
78            "file" => (),
79            "pipe_reader" => (),
80            "pipe_writer" => (),
81            "stream" => (),
82            _ => return Err(serde::de::Error::custom("invalid open file")),
83        }
84
85        // TODO(serde): Figure out something better to do with open pipes and files.
86        null().map_err(serde::de::Error::custom)
87    }
88}
89
90/// Returns an open file that will discard all I/O.
91pub fn null() -> Result<OpenFile, error::Error> {
92    let file = sys::fs::open_null_file()?;
93    Ok(OpenFile::File(file))
94}
95
96impl Clone for OpenFile {
97    fn clone(&self) -> Self {
98        // If we fail to clone the open file for any reason, we return a special file
99        // that discards all I/O. This allows us to avoid fatally erroring out.
100        self.try_clone().unwrap_or_else(|_err| {
101            ioutils::FailingReaderWriter::new("failed to duplicate open file").into()
102        })
103    }
104}
105
106impl std::fmt::Display for OpenFile {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Self::Stdin(_) => write!(f, "stdin"),
110            Self::Stdout(_) => write!(f, "stdout"),
111            Self::Stderr(_) => write!(f, "stderr"),
112            Self::File(_) => write!(f, "file"),
113            Self::PipeReader(_) => write!(f, "pipe reader"),
114            Self::PipeWriter(_) => write!(f, "pipe writer"),
115            Self::Stream(_) => write!(f, "stream"),
116        }
117    }
118}
119
120impl OpenFile {
121    /// Tries to duplicate the open file.
122    pub fn try_clone(&self) -> Result<Self, std::io::Error> {
123        let result = match self {
124            Self::Stdin(_) => std::io::stdin().into(),
125            Self::Stdout(_) => std::io::stdout().into(),
126            Self::Stderr(_) => std::io::stderr().into(),
127            Self::File(f) => f.try_clone()?.into(),
128            Self::PipeReader(f) => f.try_clone()?.into(),
129            Self::PipeWriter(f) => f.try_clone()?.into(),
130            Self::Stream(s) => Self::Stream(s.clone_box()),
131        };
132
133        Ok(result)
134    }
135
136    /// Converts the open file into an `OwnedFd`.
137    #[cfg(unix)]
138    pub(crate) fn try_clone_to_owned(self) -> Result<std::os::fd::OwnedFd, error::Error> {
139        use std::os::fd::AsFd as _;
140
141        match self {
142            Self::Stdin(f) => Ok(f.as_fd().try_clone_to_owned()?),
143            Self::Stdout(f) => Ok(f.as_fd().try_clone_to_owned()?),
144            Self::Stderr(f) => Ok(f.as_fd().try_clone_to_owned()?),
145            Self::File(f) => Ok(f.into()),
146            Self::PipeReader(r) => Ok(std::os::fd::OwnedFd::from(r)),
147            Self::PipeWriter(w) => Ok(std::os::fd::OwnedFd::from(w)),
148            Self::Stream(s) => s.try_clone_to_owned(),
149        }
150    }
151
152    /// Borrows the open file as a `BorrowedFd`.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the operation is not supported for the underlying file type.
157    #[cfg(unix)]
158    pub fn try_borrow_as_fd(&self) -> Result<std::os::fd::BorrowedFd<'_>, error::Error> {
159        use std::os::fd::AsFd as _;
160
161        match self {
162            Self::Stdin(f) => Ok(f.as_fd()),
163            Self::Stdout(f) => Ok(f.as_fd()),
164            Self::Stderr(f) => Ok(f.as_fd()),
165            Self::File(f) => Ok(f.as_fd()),
166            Self::PipeReader(r) => Ok(r.as_fd()),
167            Self::PipeWriter(w) => Ok(w.as_fd()),
168            Self::Stream(s) => s.try_borrow_as_fd(),
169        }
170    }
171
172    pub(crate) fn is_dir(&self) -> bool {
173        match self {
174            Self::Stdin(_) | Self::Stdout(_) | Self::Stderr(_) => false,
175            Self::File(file) => file.metadata().is_ok_and(|m| m.is_dir()),
176            Self::PipeReader(_) | Self::PipeWriter(_) | Self::Stream(_) => false,
177        }
178    }
179
180    /// Checks if the open file is associated with a terminal.
181    pub fn is_terminal(&self) -> bool {
182        match self {
183            Self::Stdin(f) => f.is_terminal(),
184            Self::Stdout(f) => f.is_terminal(),
185            Self::Stderr(f) => f.is_terminal(),
186            Self::File(f) => f.is_terminal(),
187            Self::PipeReader(_) | Self::PipeWriter(_) | Self::Stream(_) => false,
188        }
189    }
190}
191
192impl From<std::io::Stdin> for OpenFile {
193    /// Creates an `OpenFile` from standard input.
194    fn from(stdin: std::io::Stdin) -> Self {
195        Self::Stdin(stdin)
196    }
197}
198
199impl From<std::io::Stdout> for OpenFile {
200    /// Creates an `OpenFile` from standard output.
201    fn from(stdout: std::io::Stdout) -> Self {
202        Self::Stdout(stdout)
203    }
204}
205
206impl From<std::io::Stderr> for OpenFile {
207    /// Creates an `OpenFile` from standard error.
208    fn from(stderr: std::io::Stderr) -> Self {
209        Self::Stderr(stderr)
210    }
211}
212
213impl From<std::fs::File> for OpenFile {
214    fn from(file: std::fs::File) -> Self {
215        Self::File(file)
216    }
217}
218
219impl From<std::io::PipeReader> for OpenFile {
220    fn from(reader: std::io::PipeReader) -> Self {
221        Self::PipeReader(reader)
222    }
223}
224
225impl From<std::io::PipeWriter> for OpenFile {
226    fn from(writer: std::io::PipeWriter) -> Self {
227        Self::PipeWriter(writer)
228    }
229}
230
231impl From<OpenFile> for Stdio {
232    fn from(open_file: OpenFile) -> Self {
233        match open_file {
234            OpenFile::Stdin(_) => Self::inherit(),
235            OpenFile::Stdout(_) => Self::inherit(),
236            OpenFile::Stderr(_) => Self::inherit(),
237            OpenFile::File(f) => f.into(),
238            OpenFile::PipeReader(f) => f.into(),
239            OpenFile::PipeWriter(f) => f.into(),
240            // NOTE: Custom streams cannot be converted to `Stdio`; we do our best here
241            // and return a null device instead.
242            OpenFile::Stream(_) => Self::null(),
243        }
244    }
245}
246
247impl std::io::Read for OpenFile {
248    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
249        match self {
250            Self::Stdin(f) => f.read(buf),
251            Self::Stdout(_) => Err(std::io::Error::other(
252                error::ErrorKind::OpenFileNotReadable("stdout"),
253            )),
254            Self::Stderr(_) => Err(std::io::Error::other(
255                error::ErrorKind::OpenFileNotReadable("stderr"),
256            )),
257            Self::File(f) => f.read(buf),
258            Self::PipeReader(reader) => reader.read(buf),
259            Self::PipeWriter(_) => Err(std::io::Error::other(
260                error::ErrorKind::OpenFileNotReadable("pipe writer"),
261            )),
262            Self::Stream(s) => s.read(buf),
263        }
264    }
265}
266
267impl std::io::Write for OpenFile {
268    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
269        match self {
270            Self::Stdin(_) => Err(std::io::Error::other(
271                error::ErrorKind::OpenFileNotWritable("stdin"),
272            )),
273            Self::Stdout(f) => f.write(buf),
274            Self::Stderr(f) => f.write(buf),
275            Self::File(f) => f.write(buf),
276            Self::PipeReader(_) => Err(std::io::Error::other(
277                error::ErrorKind::OpenFileNotWritable("pipe reader"),
278            )),
279            Self::PipeWriter(writer) => writer.write(buf),
280            Self::Stream(s) => s.write(buf),
281        }
282    }
283
284    fn flush(&mut self) -> std::io::Result<()> {
285        match self {
286            Self::Stdin(_) => Ok(()),
287            Self::Stdout(f) => f.flush(),
288            Self::Stderr(f) => f.flush(),
289            Self::File(f) => f.flush(),
290            Self::PipeReader(_) => Ok(()),
291            Self::PipeWriter(writer) => writer.flush(),
292            Self::Stream(s) => s.flush(),
293        }
294    }
295}
296
297/// Tristate representing the an `OpenFile` entry in an `OpenFiles` structure.
298pub enum OpenFileEntry<'a> {
299    /// File descriptor is present and has a valid associated `OpenFile`.
300    Open(&'a OpenFile),
301    /// File descriptor is explicitly marked as not being mapped to any `OpenFile`.
302    NotPresent,
303    /// File descriptor is not specified in any way; it may be provided by a
304    /// parent context of some kind.
305    NotSpecified,
306}
307
308/// Represents the open files in a shell context.
309#[derive(Clone, Default)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
311pub struct OpenFiles {
312    /// Maps shell file descriptors to open files.
313    files: HashMap<ShellFd, Option<OpenFile>>,
314}
315
316impl OpenFiles {
317    /// File descriptor used for standard input.
318    pub const STDIN_FD: ShellFd = 0;
319    /// File descriptor used for standard output.
320    pub const STDOUT_FD: ShellFd = 1;
321    /// File descriptor used for standard error.
322    pub const STDERR_FD: ShellFd = 2;
323
324    /// First file descriptor available for non-stdio files.
325    const FIRST_NON_STDIO_FD: ShellFd = 3;
326    /// Maximum file descriptor number allowed.
327    const MAX_FD: ShellFd = 1024;
328
329    /// Creates a new `OpenFiles` instance populated with stdin, stdout, and stderr
330    /// from the host environment.
331    pub(crate) fn new() -> Self {
332        Self {
333            files: HashMap::from([
334                (Self::STDIN_FD, Some(std::io::stdin().into())),
335                (Self::STDOUT_FD, Some(std::io::stdout().into())),
336                (Self::STDERR_FD, Some(std::io::stderr().into())),
337            ]),
338        }
339    }
340
341    /// Updates the open files from the provided iterator of (fd number, `OpenFile`) pairs.
342    /// Any existing entries for the provided file descriptors will be overwritten.
343    ///
344    /// # Arguments
345    ///
346    /// * `files`: An iterator of (fd number, `OpenFile`) pairs to update the open files with.
347    pub fn update_from(&mut self, files: impl Iterator<Item = (ShellFd, OpenFile)>) {
348        for (fd, file) in files {
349            let _ = self.files.insert(fd, Some(file));
350        }
351    }
352
353    /// Retrieves the file backing standard input in this context.
354    pub fn try_stdin(&self) -> Option<&OpenFile> {
355        self.files.get(&Self::STDIN_FD).and_then(|f| f.as_ref())
356    }
357
358    /// Retrieves the file backing standard output in this context.
359    pub fn try_stdout(&self) -> Option<&OpenFile> {
360        self.files.get(&Self::STDOUT_FD).and_then(|f| f.as_ref())
361    }
362
363    /// Retrieves the file backing standard error in this context.
364    pub fn try_stderr(&self) -> Option<&OpenFile> {
365        self.files.get(&Self::STDERR_FD).and_then(|f| f.as_ref())
366    }
367
368    /// Tries to remove an open file by its file descriptor. If the file descriptor
369    /// is not used, `None` will be returned; otherwise, the removed file will
370    /// be returned.
371    ///
372    /// Arguments:
373    ///
374    /// * `fd`: The file descriptor to remove.
375    pub fn remove_fd(&mut self, fd: ShellFd) -> Option<OpenFile> {
376        self.files.insert(fd, None).and_then(|f| f)
377    }
378
379    /// Tries to lookup the `OpenFile` associated with a file descriptor.
380    /// Returns `None` if the file descriptor is not present.
381    ///
382    /// Arguments:
383    ///
384    /// * `fd`: The file descriptor to lookup.
385    pub fn try_fd(&self, fd: ShellFd) -> Option<&OpenFile> {
386        self.files.get(&fd).and_then(|f| f.as_ref())
387    }
388
389    /// Tries to lookup the `OpenFile` associated with a file descriptor. Returns
390    /// an `OpenFileEntry` representing the state of the file descriptor.
391    ///
392    /// Arguments:
393    ///
394    /// * `fd`: The file descriptor to lookup.
395    pub fn fd_entry(&self, fd: ShellFd) -> OpenFileEntry<'_> {
396        self.files
397            .get(&fd)
398            .map_or(OpenFileEntry::NotSpecified, |opt_file| match opt_file {
399                Some(f) => OpenFileEntry::Open(f),
400                None => OpenFileEntry::NotPresent,
401            })
402    }
403
404    /// Checks if the given file descriptor is in use.
405    pub fn contains_fd(&self, fd: ShellFd) -> bool {
406        self.files.contains_key(&fd)
407    }
408
409    /// Associates the given file descriptor with the provided file. If the file descriptor
410    /// is already in use, the previous file will be returned; otherwise, `None`
411    /// will be returned.
412    ///
413    /// Arguments:
414    ///
415    /// * `fd`: The file descriptor to associate with the file.
416    /// * `file`: The file to associate with the file descriptor.
417    pub fn set_fd(&mut self, fd: ShellFd, file: OpenFile) -> Option<OpenFile> {
418        self.files.insert(fd, Some(file)).and_then(|f| f)
419    }
420
421    /// Iterates over all file descriptors.
422    pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, &OpenFile)> {
423        self.files
424            .iter()
425            .filter_map(|(fd, file)| file.as_ref().map(|f| (*fd, f)))
426    }
427
428    /// Adds a new open file, returning the assigned file descriptor.
429    ///
430    /// # Arguments
431    ///
432    /// * `file`: The open file to add.
433    pub fn add(&mut self, file: OpenFile) -> Result<ShellFd, error::Error> {
434        // Start searching for free file descriptors after the standard ones.
435        let mut fd = Self::FIRST_NON_STDIO_FD;
436        while self.files.contains_key(&fd) {
437            if fd >= Self::MAX_FD {
438                return Err(error::ErrorKind::TooManyOpenFiles.into());
439            }
440
441            fd += 1;
442        }
443
444        self.files.insert(fd, Some(file));
445        Ok(fd)
446    }
447}
448
449impl<I> From<I> for OpenFiles
450where
451    I: Iterator<Item = (ShellFd, OpenFile)>,
452{
453    fn from(iter: I) -> Self {
454        let files = iter.map(|(fd, file)| (fd, Some(file))).collect();
455        Self { files }
456    }
457}