1use 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#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum RedirectAction<T> {
21 Close(Fd),
23 Open(Fd, T, Permissions),
26 HereDoc(Fd, Vec<u8>),
30}
31
32impl<T> RedirectAction<T> {
33 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#[async_trait::async_trait]
60pub trait RedirectEval<E: ?Sized> {
61 type Handle;
63 type Error;
65
66 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 .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
155pub 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
179pub 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 redirect_clobber(fd, path, env).await
202}
203
204pub 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
228pub 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
253pub 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
322pub 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
341pub 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
360pub 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}