Skip to main content

frost_exec/
redirect.rs

1//! Redirection handling — applies I/O redirections via `dup2(2)`.
2//!
3//! Called after `fork(2)` and before `exec(2)` in the child process.
4//! All system calls go through [`crate::sys`] for portability.
5
6use std::ffi::CString;
7use std::os::unix::ffi::OsStrExt;
8use std::path::Path;
9
10use nix::fcntl::OFlag;
11use nix::sys::stat::Mode;
12
13use frost_parser::ast::{Redirect, RedirectOp, WordPart};
14
15use crate::sys;
16
17/// Error type for redirection failures.
18#[derive(Debug, thiserror::Error)]
19pub enum RedirectError {
20    #[error("failed to open `{path}`: {source}")]
21    Open {
22        path: String,
23        source: nix::errno::Errno,
24    },
25
26    #[error("dup2 failed: {0}")]
27    Dup2(nix::errno::Errno),
28
29    #[error("close failed: {0}")]
30    Close(nix::errno::Errno),
31
32    #[error("bad file descriptor: {0}")]
33    BadFd(String),
34}
35
36/// Apply a list of redirections in the current process.
37///
38/// Typically called in the child after `fork()`. Each redirection
39/// opens/creates the target file and dups it onto the appropriate fd.
40///
41/// `expanded_targets` provides pre-expanded strings for redirect targets.
42/// If provided and non-empty, the i-th entry is used as the target text
43/// instead of resolving the word from the AST. This allows the executor
44/// to pass in variable-expanded herestring content.
45pub fn apply_redirects(redirects: &[Redirect]) -> Result<(), RedirectError> {
46    for redir in redirects {
47        apply_one(redir, None)?;
48    }
49    Ok(())
50}
51
52/// Apply redirections with pre-expanded target text for each redirect.
53pub fn apply_redirects_expanded(
54    redirects: &[Redirect],
55    expanded_targets: &[String],
56) -> Result<(), RedirectError> {
57    for (i, redir) in redirects.iter().enumerate() {
58        let expanded = expanded_targets.get(i).map(|s| s.as_str());
59        apply_one(redir, expanded)?;
60    }
61    Ok(())
62}
63
64fn apply_one(redir: &Redirect, expanded_target: Option<&str>) -> Result<(), RedirectError> {
65    let target_text = || expanded_target.map(|s| s.to_owned()).unwrap_or_else(|| resolve_word(&redir.target));
66
67    match redir.op {
68        // < file  (input)
69        RedirectOp::Less => {
70            let target_fd = redir.fd.unwrap_or(0) as i32;
71            let path = target_text();
72            let fd = open_file(&path, OFlag::O_RDONLY, Mode::empty())?;
73            dup2_and_close(fd, target_fd)?;
74        }
75
76        // > file  (output, truncate)
77        RedirectOp::Greater | RedirectOp::GreaterPipe | RedirectOp::GreaterBang => {
78            let target_fd = redir.fd.unwrap_or(1) as i32;
79            let path = target_text();
80            let fd = open_file(
81                &path,
82                OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
83                Mode::from_bits_truncate(0o666),
84            )?;
85            dup2_and_close(fd, target_fd)?;
86        }
87
88        // >> file  (append)
89        RedirectOp::DoubleGreater => {
90            let target_fd = redir.fd.unwrap_or(1) as i32;
91            let path = target_text();
92            let fd = open_file(
93                &path,
94                OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_APPEND,
95                Mode::from_bits_truncate(0o666),
96            )?;
97            dup2_and_close(fd, target_fd)?;
98        }
99
100        // &> file  (stdout + stderr)
101        RedirectOp::AmpGreater => {
102            let path = target_text();
103            let fd = open_file(
104                &path,
105                OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
106                Mode::from_bits_truncate(0o666),
107            )?;
108            dup2_and_close(fd, 1)?;
109            sys::dup2(1, 2).map_err(RedirectError::Dup2)?;
110        }
111
112        // &>> file  (append stdout + stderr)
113        RedirectOp::AmpDoubleGreater => {
114            let path = target_text();
115            let fd = open_file(
116                &path,
117                OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_APPEND,
118                Mode::from_bits_truncate(0o666),
119            )?;
120            dup2_and_close(fd, 1)?;
121            sys::dup2(1, 2).map_err(RedirectError::Dup2)?;
122        }
123
124        // <> file  (read-write)
125        RedirectOp::LessGreater => {
126            let target_fd = redir.fd.unwrap_or(0) as i32;
127            let path = target_text();
128            let fd = open_file(
129                &path,
130                OFlag::O_RDWR | OFlag::O_CREAT,
131                Mode::from_bits_truncate(0o666),
132            )?;
133            dup2_and_close(fd, target_fd)?;
134        }
135
136        // N>&M  (fd duplication)
137        RedirectOp::FdDup => {
138            let target_fd = redir.fd.unwrap_or(1) as i32;
139            let src_text = target_text();
140            if src_text == "-" {
141                sys::close(target_fd).map_err(RedirectError::Close)?;
142            } else {
143                let src_fd: i32 = src_text
144                    .parse()
145                    .map_err(|_| RedirectError::BadFd(src_text))?;
146                sys::dup2(src_fd, target_fd).map_err(RedirectError::Dup2)?;
147            }
148        }
149
150        // <<< herestring — feed the word as stdin
151        RedirectOp::TripleLess => {
152            let target_fd = redir.fd.unwrap_or(0) as i32;
153            let text = target_text();
154            let content = format!("{text}\n");
155            let fd = write_to_pipe(content.as_bytes())?;
156            dup2_and_close(fd, target_fd)?;
157        }
158
159        // << heredoc / <<- heredoc (strip tabs) — treat body as herestring for now
160        RedirectOp::DoubleLess | RedirectOp::DoubleLessDash => {
161            let target_fd = redir.fd.unwrap_or(0) as i32;
162            let text = target_text();
163            let content = format!("{text}\n");
164            let fd = write_to_pipe(content.as_bytes())?;
165            dup2_and_close(fd, target_fd)?;
166        }
167    }
168
169    Ok(())
170}
171
172/// Write data to a pipe and return the read end as a raw fd.
173fn write_to_pipe(data: &[u8]) -> Result<i32, RedirectError> {
174    let pipe = sys::pipe().map_err(|e| RedirectError::Open {
175        path: "<herestring>".to_owned(),
176        source: e,
177    })?;
178    // Write data to the write end.
179    nix::unistd::write(unsafe { std::os::fd::BorrowedFd::borrow_raw(pipe.write) }, data)
180        .map_err(|e| RedirectError::Open {
181            path: "<herestring>".to_owned(),
182            source: e,
183        })?;
184    // Close the write end so the reader gets EOF.
185    sys::close(pipe.write).ok();
186    Ok(pipe.read)
187}
188
189/// Open a file by path, returning a raw fd.
190fn open_file(path: &str, flags: OFlag, mode: Mode) -> Result<i32, RedirectError> {
191    let cpath = CString::new(Path::new(path).as_os_str().as_bytes())
192        .map_err(|_| RedirectError::BadFd(path.to_owned()))?;
193    sys::open(cpath.as_c_str(), flags, mode).map_err(|e| RedirectError::Open {
194        path: path.to_owned(),
195        source: e,
196    })
197}
198
199/// Dup `fd` onto `target`, then close the original if they differ.
200fn dup2_and_close(fd: i32, target: i32) -> Result<(), RedirectError> {
201    sys::dup2_and_close(fd, target).map_err(RedirectError::Dup2)
202}
203
204/// Get the target fd for a redirect (used by save/restore logic).
205pub fn target_fd_for(redir: &Redirect) -> i32 {
206    match redir.op {
207        RedirectOp::Less | RedirectOp::LessGreater => redir.fd.unwrap_or(0) as i32,
208        RedirectOp::Greater | RedirectOp::GreaterPipe | RedirectOp::GreaterBang
209        | RedirectOp::DoubleGreater | RedirectOp::FdDup => redir.fd.unwrap_or(1) as i32,
210        RedirectOp::AmpGreater | RedirectOp::AmpDoubleGreater => 1, // also affects 2
211        RedirectOp::DoubleLess | RedirectOp::TripleLess | RedirectOp::DoubleLessDash => {
212            redir.fd.unwrap_or(0) as i32
213        }
214    }
215}
216
217/// Extract a plain string from a [`Word`].
218fn resolve_word(word: &frost_parser::ast::Word) -> String {
219    let mut out = String::new();
220    for part in &word.parts {
221        match part {
222            WordPart::Literal(s) | WordPart::SingleQuoted(s) => out.push_str(s),
223            _ => {
224                tracing::warn!("unresolved word part in redirect target");
225            }
226        }
227    }
228    out
229}