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::sys;
10
11/// Represents a file open in a shell context.
12pub enum OpenFile {
13    /// The original standard input this process was started with.
14    Stdin(std::io::Stdin),
15    /// The original standard output this process was started with.
16    Stdout(std::io::Stdout),
17    /// The original standard error this process was started with.
18    Stderr(std::io::Stderr),
19    /// A file open for reading or writing.
20    File(std::fs::File),
21    /// A read end of a pipe.
22    PipeReader(std::io::PipeReader),
23    /// A write end of a pipe.
24    PipeWriter(std::io::PipeWriter),
25}
26
27/// Returns an open file that will discard all I/O.
28pub fn null() -> Result<OpenFile, error::Error> {
29    let file = sys::fs::open_null_file()?;
30    Ok(OpenFile::File(file))
31}
32
33impl Clone for OpenFile {
34    fn clone(&self) -> Self {
35        self.try_clone().unwrap()
36    }
37}
38
39impl std::fmt::Display for OpenFile {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::Stdin(_) => write!(f, "stdin"),
43            Self::Stdout(_) => write!(f, "stdout"),
44            Self::Stderr(_) => write!(f, "stderr"),
45            Self::File(_) => write!(f, "file"),
46            Self::PipeReader(_) => write!(f, "pipe reader"),
47            Self::PipeWriter(_) => write!(f, "pipe writer"),
48        }
49    }
50}
51
52impl OpenFile {
53    /// Tries to duplicate the open file.
54    pub fn try_clone(&self) -> Result<Self, std::io::Error> {
55        let result = match self {
56            Self::Stdin(_) => Self::Stdin(std::io::stdin()),
57            Self::Stdout(_) => Self::Stdout(std::io::stdout()),
58            Self::Stderr(_) => Self::Stderr(std::io::stderr()),
59            Self::File(f) => Self::File(f.try_clone()?),
60            Self::PipeReader(f) => Self::PipeReader(f.try_clone()?),
61            Self::PipeWriter(f) => Self::PipeWriter(f.try_clone()?),
62        };
63
64        Ok(result)
65    }
66
67    /// Converts the open file into an `OwnedFd`.
68    #[cfg(unix)]
69    pub(crate) fn into_owned_fd(self) -> Result<std::os::fd::OwnedFd, error::Error> {
70        use std::os::fd::AsFd as _;
71
72        match self {
73            Self::Stdin(f) => Ok(f.as_fd().try_clone_to_owned()?),
74            Self::Stdout(f) => Ok(f.as_fd().try_clone_to_owned()?),
75            Self::Stderr(f) => Ok(f.as_fd().try_clone_to_owned()?),
76            Self::File(f) => Ok(f.into()),
77            Self::PipeReader(r) => Ok(std::os::fd::OwnedFd::from(r)),
78            Self::PipeWriter(w) => Ok(std::os::fd::OwnedFd::from(w)),
79        }
80    }
81
82    pub(crate) fn is_dir(&self) -> bool {
83        match self {
84            Self::Stdin(_) | Self::Stdout(_) | Self::Stderr(_) => false,
85            Self::File(file) => file.metadata().map(|m| m.is_dir()).unwrap_or(false),
86            Self::PipeReader(_) | Self::PipeWriter(_) => false,
87        }
88    }
89
90    pub(crate) fn is_term(&self) -> bool {
91        match self {
92            Self::Stdin(f) => f.is_terminal(),
93            Self::Stdout(f) => f.is_terminal(),
94            Self::Stderr(f) => f.is_terminal(),
95            Self::File(f) => f.is_terminal(),
96            Self::PipeReader(_) => false,
97            Self::PipeWriter(_) => false,
98        }
99    }
100}
101
102#[cfg(unix)]
103impl std::os::fd::AsFd for OpenFile {
104    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
105        match self {
106            Self::Stdin(f) => f.as_fd(),
107            Self::Stdout(f) => f.as_fd(),
108            Self::Stderr(f) => f.as_fd(),
109            Self::File(f) => f.as_fd(),
110            Self::PipeReader(r) => r.as_fd(),
111            Self::PipeWriter(w) => w.as_fd(),
112        }
113    }
114}
115
116impl From<std::fs::File> for OpenFile {
117    fn from(file: std::fs::File) -> Self {
118        Self::File(file)
119    }
120}
121
122impl From<std::io::PipeReader> for OpenFile {
123    fn from(reader: std::io::PipeReader) -> Self {
124        Self::PipeReader(reader)
125    }
126}
127
128impl From<std::io::PipeWriter> for OpenFile {
129    fn from(writer: std::io::PipeWriter) -> Self {
130        Self::PipeWriter(writer)
131    }
132}
133
134impl From<OpenFile> for Stdio {
135    fn from(open_file: OpenFile) -> Self {
136        match open_file {
137            OpenFile::Stdin(_) => Self::inherit(),
138            OpenFile::Stdout(_) => Self::inherit(),
139            OpenFile::Stderr(_) => Self::inherit(),
140            OpenFile::File(f) => f.into(),
141            OpenFile::PipeReader(f) => f.into(),
142            OpenFile::PipeWriter(f) => f.into(),
143        }
144    }
145}
146
147impl std::io::Read for OpenFile {
148    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
149        match self {
150            Self::Stdin(f) => f.read(buf),
151            Self::Stdout(_) => Err(std::io::Error::other(
152                error::ErrorKind::OpenFileNotReadable("stdout"),
153            )),
154            Self::Stderr(_) => Err(std::io::Error::other(
155                error::ErrorKind::OpenFileNotReadable("stderr"),
156            )),
157            Self::File(f) => f.read(buf),
158            Self::PipeReader(reader) => reader.read(buf),
159            Self::PipeWriter(_) => Err(std::io::Error::other(
160                error::ErrorKind::OpenFileNotReadable("pipe writer"),
161            )),
162        }
163    }
164}
165
166impl std::io::Write for OpenFile {
167    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
168        match self {
169            Self::Stdin(_) => Err(std::io::Error::other(
170                error::ErrorKind::OpenFileNotWritable("stdin"),
171            )),
172            Self::Stdout(f) => f.write(buf),
173            Self::Stderr(f) => f.write(buf),
174            Self::File(f) => f.write(buf),
175            Self::PipeReader(_) => Err(std::io::Error::other(
176                error::ErrorKind::OpenFileNotWritable("pipe reader"),
177            )),
178            Self::PipeWriter(writer) => writer.write(buf),
179        }
180    }
181
182    fn flush(&mut self) -> std::io::Result<()> {
183        match self {
184            Self::Stdin(_) => Ok(()),
185            Self::Stdout(f) => f.flush(),
186            Self::Stderr(f) => f.flush(),
187            Self::File(f) => f.flush(),
188            Self::PipeReader(_) => Ok(()),
189            Self::PipeWriter(writer) => writer.flush(),
190        }
191    }
192}
193
194/// Tristate representing the an `OpenFile` entry in an `OpenFiles` structure.
195pub enum OpenFileEntry<'a> {
196    /// File descriptor is present and has a valid associated `OpenFile`.
197    Open(&'a OpenFile),
198    /// File descriptor is explicitly marked as not being mapped to any `OpenFile`.
199    NotPresent,
200    /// File descriptor is not specified in any way; it may be provided by a
201    /// parent context of some kind.
202    NotSpecified,
203}
204
205/// Represents the open files in a shell context.
206#[derive(Clone, Default)]
207pub struct OpenFiles {
208    /// Maps shell file descriptors to open files.
209    files: HashMap<ShellFd, Option<OpenFile>>,
210}
211
212impl OpenFiles {
213    /// File descriptor used for standard input.
214    pub const STDIN_FD: ShellFd = 0;
215    /// File descriptor used for standard output.
216    pub const STDOUT_FD: ShellFd = 1;
217    /// File descriptor used for standard error.
218    pub const STDERR_FD: ShellFd = 2;
219
220    /// Creates a new `OpenFiles` instance populated with stdin, stdout, and stderr
221    /// from the host environment.
222    #[allow(unused)]
223    pub(crate) fn new() -> Self {
224        Self {
225            files: HashMap::from([
226                (Self::STDIN_FD, Some(OpenFile::Stdin(std::io::stdin()))),
227                (Self::STDOUT_FD, Some(OpenFile::Stdout(std::io::stdout()))),
228                (Self::STDERR_FD, Some(OpenFile::Stderr(std::io::stderr()))),
229            ]),
230        }
231    }
232
233    /// Updates the open files from the provided iterator of (fd number, `OpenFile`) pairs.
234    /// Any existing entries for the provided file descriptors will be overwritten.
235    ///
236    /// # Arguments
237    ///
238    /// * `files`: An iterator of (fd number, `OpenFile`) pairs to update the open files with.
239    pub fn update_from(&mut self, files: impl Iterator<Item = (ShellFd, OpenFile)>) {
240        for (fd, file) in files {
241            let _ = self.files.insert(fd, Some(file));
242        }
243    }
244
245    /// Retrieves the file backing standard input in this context.
246    pub fn try_stdin(&self) -> Option<&OpenFile> {
247        self.files.get(&Self::STDIN_FD).and_then(|f| f.as_ref())
248    }
249
250    /// Retrieves the file backing standard output in this context.
251    pub fn try_stdout(&self) -> Option<&OpenFile> {
252        self.files.get(&Self::STDOUT_FD).and_then(|f| f.as_ref())
253    }
254
255    /// Retrieves the file backing standard error in this context.
256    pub fn try_stderr(&self) -> Option<&OpenFile> {
257        self.files.get(&Self::STDERR_FD).and_then(|f| f.as_ref())
258    }
259
260    /// Tries to remove an open file by its file descriptor. If the file descriptor
261    /// is not used, `None` will be returned; otherwise, the removed file will
262    /// be returned.
263    ///
264    /// Arguments:
265    ///
266    /// * `fd`: The file descriptor to remove.
267    pub fn remove_fd(&mut self, fd: ShellFd) -> Option<OpenFile> {
268        self.files.insert(fd, None).and_then(|f| f)
269    }
270
271    /// Tries to lookup the `OpenFile` associated with a file descriptor.
272    /// Returns `None` if the file descriptor is not present.
273    ///
274    /// Arguments:
275    ///
276    /// * `fd`: The file descriptor to lookup.
277    pub fn try_fd(&self, fd: ShellFd) -> Option<&OpenFile> {
278        self.files.get(&fd).and_then(|f| f.as_ref())
279    }
280
281    /// Tries to lookup the `OpenFile` associated with a file descriptor. Returns
282    /// an `OpenFileEntry` representing the state of the file descriptor.
283    ///
284    /// Arguments:
285    ///
286    /// * `fd`: The file descriptor to lookup.
287    pub fn fd_entry(&self, fd: ShellFd) -> OpenFileEntry<'_> {
288        self.files
289            .get(&fd)
290            .map_or(OpenFileEntry::NotSpecified, |opt_file| match opt_file {
291                Some(f) => OpenFileEntry::Open(f),
292                None => OpenFileEntry::NotPresent,
293            })
294    }
295
296    /// Checks if the given file descriptor is in use.
297    pub fn contains_fd(&self, fd: ShellFd) -> bool {
298        self.files.contains_key(&fd)
299    }
300
301    /// Associates the given file descriptor with the provided file. If the file descriptor
302    /// is already in use, the previous file will be returned; otherwise, `None`
303    /// will be returned.
304    ///
305    /// Arguments:
306    ///
307    /// * `fd`: The file descriptor to associate with the file.
308    /// * `file`: The file to associate with the file descriptor.
309    pub fn set_fd(&mut self, fd: ShellFd, file: OpenFile) -> Option<OpenFile> {
310        self.files.insert(fd, Some(file)).and_then(|f| f)
311    }
312
313    /// Iterates over all file descriptors.
314    pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, &OpenFile)> {
315        self.files
316            .iter()
317            .filter_map(|(fd, file)| file.as_ref().map(|f| (*fd, f)))
318    }
319}
320
321impl<I> From<I> for OpenFiles
322where
323    I: Iterator<Item = (ShellFd, OpenFile)>,
324{
325    fn from(iter: I) -> Self {
326        let files = iter.map(|(fd, file)| (fd, Some(file))).collect();
327        Self { files }
328    }
329}