1use 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#[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
36pub fn apply_redirects(redirects: &[Redirect]) -> Result<(), RedirectError> {
46 for redir in redirects {
47 apply_one(redir, None)?;
48 }
49 Ok(())
50}
51
52pub 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 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 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 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 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 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 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 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 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 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
172fn 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 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 sys::close(pipe.write).ok();
186 Ok(pipe.read)
187}
188
189fn 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
199fn dup2_and_close(fd: i32, target: i32) -> Result<(), RedirectError> {
201 sys::dup2_and_close(fd, target).map_err(RedirectError::Dup2)
202}
203
204pub 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, RedirectOp::DoubleLess | RedirectOp::TripleLess | RedirectOp::DoubleLessDash => {
212 redir.fd.unwrap_or(0) as i32
213 }
214 }
215}
216
217fn 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}