conch_runtime_pshaw/eval/
redirect.rs

1//! A module which defines evaluating any kind of redirection.
2
3use crate::env::{
4    AsyncIoEnvironment, FileDescEnvironment, FileDescOpener, IsInteractiveEnvironment,
5    StringWrapper, WorkingDirectoryEnvironment,
6};
7use crate::error::RedirectionError;
8use crate::eval::{Fields, TildeExpansion, WordEval, WordEvalConfig};
9use crate::io::Permissions;
10use crate::{Fd, STDIN_FILENO, STDOUT_FILENO};
11use futures_core::future::BoxFuture;
12use std::borrow::Cow;
13use std::fs::OpenOptions;
14use std::io;
15use std::path::Path;
16
17/// Indicates what changes should be made to the environment as a result
18/// of a successful `Redirect` evaluation.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum RedirectAction<T> {
21    /// Indicates that a descriptor should be closed.
22    Close(Fd),
23    /// Indicates that a descriptor should be opened with
24    /// a given file handle and permissions.
25    Open(Fd, T, Permissions),
26    /// Indicates that the body of a heredoc should be asynchronously written
27    /// to a file handle on a best effor basis (i.e. write as much of the body
28    /// as possible but give up on appropriate errors such as broken pipes).
29    HereDoc(Fd, Vec<u8>),
30}
31
32impl<T> RedirectAction<T> {
33    /// Applies changes to a given environment as appropriate.
34    pub fn apply<E>(self, env: &mut E) -> io::Result<()>
35    where
36        E: ?Sized + AsyncIoEnvironment + FileDescEnvironment + FileDescOpener,
37        E::FileHandle: From<T> + From<E::OpenedFileHandle>,
38        E::IoHandle: From<E::FileHandle>,
39    {
40        match self {
41            RedirectAction::Close(fd) => env.close_file_desc(fd),
42            RedirectAction::Open(fd, file_desc, perms) => {
43                env.set_file_desc(fd, file_desc.into(), perms)
44            }
45            RedirectAction::HereDoc(fd, body) => {
46                let pipe = env.open_pipe()?;
47                env.set_file_desc(fd, pipe.reader.into(), Permissions::Read);
48
49                let writer = E::FileHandle::from(pipe.writer);
50                env.write_all_best_effort(E::IoHandle::from(writer), body);
51            }
52        }
53
54        Ok(())
55    }
56}
57
58/// A trait for evaluating file descriptor redirections.
59#[async_trait::async_trait]
60pub trait RedirectEval<E: ?Sized> {
61    /// The type of handle that should be added to the environment.
62    type Handle;
63    /// An error that can arise during evaluation.
64    type Error;
65
66    /// Evaluates a redirection path and opens the appropriate redirect.
67    ///
68    /// Newly opened/closed/duplicated/heredoc file descriptors are NOT
69    /// updated in the environment, and thus it is up to the caller to
70    /// update the environment as appropriate.
71    async fn eval(&self, env: &mut E) -> Result<RedirectAction<Self::Handle>, Self::Error>;
72}
73
74impl<'a, T, E> RedirectEval<E> for &'a T
75where
76    T: RedirectEval<E>,
77    E: ?Sized,
78{
79    type Handle = T::Handle;
80    type Error = T::Error;
81
82    fn eval<'life0, 'life1, 'async_trait>(
83        &'life0 self,
84        env: &'life1 mut E,
85    ) -> BoxFuture<'async_trait, Result<RedirectAction<Self::Handle>, Self::Error>>
86    where
87        'life0: 'async_trait,
88        'life1: 'async_trait,
89        Self: 'async_trait,
90    {
91        (**self).eval(env)
92    }
93}
94
95async fn eval_path<W, E>(path: W, env: &mut E) -> Result<Fields<W::EvalResult>, W::Error>
96where
97    W: WordEval<E>,
98    E: ?Sized + IsInteractiveEnvironment,
99{
100    let cfg = WordEvalConfig {
101        tilde_expansion: TildeExpansion::First,
102        split_fields_further: env.is_interactive(),
103    };
104
105    Ok(path.eval_with_config(env, cfg).await?.await)
106}
107
108macro_rules! join_path {
109    ($path:expr) => {{
110        match $path {
111            Fields::Single(path) => path,
112            Fields::At(mut v) | Fields::Star(mut v) | Fields::Split(mut v) => {
113                if v.len() == 1 {
114                    v.pop().unwrap()
115                } else {
116                    let v = v.into_iter().map(StringWrapper::into_owned).collect();
117                    return Err(RedirectionError::Ambiguous(v).into());
118                }
119            }
120            Fields::Zero => return Err(RedirectionError::Ambiguous(Vec::new()).into()),
121        }
122    }};
123}
124
125async fn redirect<W, E>(
126    fd: Fd,
127    path: W,
128    opts: &OpenOptions,
129    perms: Permissions,
130    env: &mut E,
131) -> Result<RedirectAction<E::FileHandle>, W::Error>
132where
133    W: WordEval<E>,
134    W::Error: From<RedirectionError>,
135    E: ?Sized
136        + FileDescEnvironment
137        + FileDescOpener
138        + IsInteractiveEnvironment
139        + WorkingDirectoryEnvironment,
140    E::FileHandle: From<E::OpenedFileHandle>,
141{
142    let requested_path = join_path!(eval_path(path, env).await?);
143    let actual_path =
144        env.path_relative_to_working_dir(Cow::Borrowed(Path::new(requested_path.as_str())));
145
146    let ret = env
147        // FIXME: on unix set file permission bits based on umask
148        .open_path(&*actual_path, &opts)
149        .map(|fdesc| RedirectAction::Open(fd, E::FileHandle::from(fdesc), perms))
150        .map_err(|err| RedirectionError::Io(err, Some(requested_path.into_owned())));
151
152    Ok(ret?)
153}
154
155/// Evaluate a redirect which will open a file for reading.
156///
157/// If `fd` is not specified, then `STDIN_FILENO` will be used.
158pub async fn redirect_read<W, E>(
159    fd: Option<Fd>,
160    path: W,
161    env: &mut E,
162) -> Result<RedirectAction<E::FileHandle>, W::Error>
163where
164    W: WordEval<E>,
165    W::Error: From<RedirectionError>,
166    E: ?Sized
167        + FileDescEnvironment
168        + FileDescOpener
169        + IsInteractiveEnvironment
170        + WorkingDirectoryEnvironment,
171    E::FileHandle: From<E::OpenedFileHandle>,
172{
173    let fd = fd.unwrap_or(STDIN_FILENO);
174    let perms = Permissions::Read;
175
176    redirect(fd, path, &perms.into(), perms, env).await
177}
178
179/// Evaluate a redirect which will open a file for writing, failing if the
180/// `noclobber` option is set.
181///
182/// If `fd` is not specified, then `STDOUT_FILENO` will be used.
183///
184/// > *Note*: checks for `noclobber` are not yet implemented.
185pub async fn redirect_write<W, E>(
186    fd: Option<Fd>,
187    path: W,
188    env: &mut E,
189) -> Result<RedirectAction<E::FileHandle>, W::Error>
190where
191    W: WordEval<E>,
192    W::Error: From<RedirectionError>,
193    E: ?Sized
194        + FileDescEnvironment
195        + FileDescOpener
196        + IsInteractiveEnvironment
197        + WorkingDirectoryEnvironment,
198    E::FileHandle: From<E::OpenedFileHandle>,
199{
200    // FIXME: check for and fail if noclobber option is set
201    redirect_clobber(fd, path, env).await
202}
203
204/// Evaluate a redirect which will open a file for reading and writing.
205///
206/// If `fd` is not specified, then `STDIN_FILENO` will be used.
207pub async fn redirect_readwrite<W, E>(
208    fd: Option<Fd>,
209    path: W,
210    env: &mut E,
211) -> Result<RedirectAction<E::FileHandle>, W::Error>
212where
213    W: WordEval<E>,
214    W::Error: From<RedirectionError>,
215    E: ?Sized
216        + FileDescEnvironment
217        + FileDescOpener
218        + IsInteractiveEnvironment
219        + WorkingDirectoryEnvironment,
220    E::FileHandle: From<E::OpenedFileHandle>,
221{
222    let fd = fd.unwrap_or(STDIN_FILENO);
223    let perms = Permissions::ReadWrite;
224
225    redirect(fd, path, &perms.into(), perms, env).await
226}
227
228/// Evaluate a redirect which will open a file for writing, regardless if the
229/// `noclobber` option is set.
230///
231/// If `fd` is not specified, then `STDOUT_FILENO` will be used.
232pub async fn redirect_clobber<W, E>(
233    fd: Option<Fd>,
234    path: W,
235    env: &mut E,
236) -> Result<RedirectAction<E::FileHandle>, W::Error>
237where
238    W: WordEval<E>,
239    W::Error: From<RedirectionError>,
240    E: ?Sized
241        + FileDescEnvironment
242        + FileDescOpener
243        + IsInteractiveEnvironment
244        + WorkingDirectoryEnvironment,
245    E::FileHandle: From<E::OpenedFileHandle>,
246{
247    let fd = fd.unwrap_or(STDOUT_FILENO);
248    let perms = Permissions::Write;
249
250    redirect(fd, path, &perms.into(), perms, env).await
251}
252
253/// Evaluate a redirect which will open a file in append mode.
254///
255/// If `fd` is not specified, then `STDOUT_FILENO` will be used.
256pub async fn redirect_append<W, E>(
257    fd: Option<Fd>,
258    path: W,
259    env: &mut E,
260) -> Result<RedirectAction<E::FileHandle>, W::Error>
261where
262    W: WordEval<E>,
263    W::Error: From<RedirectionError>,
264    E: ?Sized
265        + FileDescEnvironment
266        + FileDescOpener
267        + IsInteractiveEnvironment
268        + WorkingDirectoryEnvironment,
269    E::FileHandle: From<E::OpenedFileHandle>,
270{
271    let fd = fd.unwrap_or(STDOUT_FILENO);
272    let mut opts = OpenOptions::new();
273    opts.append(true);
274
275    redirect(fd, path, &opts, Permissions::Write, env).await
276}
277
278async fn redirect_dup<W, E>(
279    dst_fd: Fd,
280    src_fd: W,
281    readable: bool,
282    env: &mut E,
283) -> Result<RedirectAction<E::FileHandle>, W::Error>
284where
285    W: WordEval<E>,
286    W::Error: From<RedirectionError>,
287    E: ?Sized + FileDescEnvironment + IsInteractiveEnvironment,
288    E::FileHandle: Clone,
289{
290    let src_fd = join_path!(eval_path(src_fd, env).await?);
291    let src_fd = src_fd.as_str();
292
293    if src_fd == "-" {
294        return Ok(RedirectAction::Close(dst_fd));
295    }
296
297    let fd_handle_perms = Fd::from_str_radix(src_fd, 10)
298        .ok()
299        .and_then(|fd| env.file_desc(fd).map(|(fdes, perms)| (fd, fdes, perms)));
300
301    let src_fdes = match fd_handle_perms {
302        Some((fd, fdes, perms)) => {
303            if (readable && perms.readable()) || (!readable && perms.writable()) {
304                fdes.clone()
305            } else {
306                return Err(RedirectionError::BadFdPerms(fd, perms).into());
307            }
308        }
309
310        None => return Err(RedirectionError::BadFdSrc(src_fd.to_owned()).into()),
311    };
312
313    let perms = if readable {
314        Permissions::Read
315    } else {
316        Permissions::Write
317    };
318
319    Ok(RedirectAction::Open(dst_fd, src_fdes, perms))
320}
321
322/// Evaluate a redirect which will either duplicate a readable file descriptor
323/// as specified by `src_fd` into `dst_fd`, or close `dst_fd` if `src_fd`
324/// evaluates to `-`.
325///
326/// If `fd` is not specified, then `STDIN_FILENO` will be used.
327pub async fn redirect_dup_read<W, E>(
328    dst_fd: Option<Fd>,
329    src_fd: W,
330    env: &mut E,
331) -> Result<RedirectAction<E::FileHandle>, W::Error>
332where
333    W: WordEval<E>,
334    W::Error: From<RedirectionError>,
335    E: ?Sized + FileDescEnvironment + IsInteractiveEnvironment,
336    E::FileHandle: Clone,
337{
338    redirect_dup(dst_fd.unwrap_or(STDIN_FILENO), src_fd, true, env).await
339}
340
341/// Evaluate a redirect which will either duplicate a writeable file descriptor
342/// as specified by `src_fd` into `dst_fd`, or close `dst_fd` if `src_fd`
343/// evaluates to `-`.
344///
345/// If `fd` is not specified, then `STDOUT_FILENO` will be used.
346pub async fn redirect_dup_write<W, E>(
347    dst_fd: Option<Fd>,
348    src_fd: W,
349    env: &mut E,
350) -> Result<RedirectAction<E::FileHandle>, W::Error>
351where
352    W: WordEval<E>,
353    W::Error: From<RedirectionError>,
354    E: ?Sized + FileDescEnvironment + IsInteractiveEnvironment,
355    E::FileHandle: Clone,
356{
357    redirect_dup(dst_fd.unwrap_or(STDOUT_FILENO), src_fd, false, env).await
358}
359
360/// Evaluate a redirect which write the body of a *here-document* into `fd`.
361///
362/// If `fd` is not specified, then `STDIN_FILENO` will be used.
363pub async fn redirect_heredoc<W, E>(
364    fd: Option<Fd>,
365    heredoc: W,
366    env: &mut E,
367) -> Result<RedirectAction<E::FileHandle>, W::Error>
368where
369    W: WordEval<E>,
370    E: ?Sized + FileDescEnvironment + IsInteractiveEnvironment,
371{
372    let cfg = WordEvalConfig {
373        tilde_expansion: TildeExpansion::None,
374        split_fields_further: false,
375    };
376
377    let body = match heredoc.eval_with_config(env, cfg).await?.await {
378        Fields::Zero => Vec::new(),
379        Fields::Single(path) => path.into_owned().into_bytes(),
380        Fields::At(mut v) | Fields::Star(mut v) | Fields::Split(mut v) => {
381            if v.len() == 1 {
382                v.pop().unwrap().into_owned().into_bytes()
383            } else {
384                let len = v.iter().map(|f| f.as_str().len()).sum();
385                let mut body = Vec::with_capacity(len);
386                for field in v {
387                    body.extend_from_slice(field.as_str().as_bytes());
388                }
389                body
390            }
391        }
392    };
393
394    Ok(RedirectAction::HereDoc(fd.unwrap_or(STDIN_FILENO), body))
395}