Skip to main content

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