1use super::Command;
20use crate::common::report::report_failure;
21use std::cell::RefCell;
22use std::ffi::CStr;
23use std::ffi::CString;
24use std::ops::ControlFlow;
25use std::rc::Rc;
26use yash_env::Env;
27use yash_env::input::{Echo, FdReader2};
28use yash_env::io::Fd;
29use yash_env::io::move_fd_internal;
30use yash_env::parser::Config;
31use yash_env::path::PathBuf;
32use yash_env::semantics::{Divert, ExitStatus, Field, RunReadEvalLoop};
33use yash_env::source::Source;
34use yash_env::source::pretty::{Report, ReportType, Snippet};
35use yash_env::stack::Frame;
36use yash_env::system::{
37 Close, Dup, Errno, Fcntl, Isatty, Mode, OfdAccess, Open, OpenFlag, Read, Write,
38};
39use yash_env::variable::PATH;
40
41impl Command {
42 pub async fn execute<S>(self, env: &mut Env<S>) -> crate::Result
47 where
48 S: Close + Dup + Fcntl + Isatty + Open + Read + Write + 'static,
49 {
50 let env = &mut *env.push_frame(Frame::DotScript);
51
52 let fd = match find_and_open_file(env, &self.file.value).await {
53 Ok(fd) => fd,
54 Err(errno) => return report_find_and_open_file_failure(env, &self.file, errno).await,
55 };
56
57 let run_read_eval_loop = env
61 .any
62 .get::<RunReadEvalLoop<S>>()
63 .cloned()
64 .expect("`RunReadEvalLoop` should be in `env.any`");
65 let system = env.system.clone();
66 let ref_env = RefCell::new(&mut *env);
67 let input = Box::new(Echo::new(FdReader2::new(fd, system), &ref_env));
68 let mut config = Config::with_input(input);
69 config.source = Some(Rc::new(Source::DotScript {
70 name: self.file.value,
71 origin: self.file.origin,
72 }));
73 let divert = run_read_eval_loop.0(&ref_env, config).await;
74
75 _ = env.system.close(fd);
76
77 let (exit_status, divert) = consume_return(divert);
78 let exit_status = exit_status.unwrap_or(env.exit_status);
79 crate::Result::with_exit_status_and_divert(exit_status, divert)
80 }
81}
82
83async fn find_and_open_file<S>(env: &mut Env<S>, filename: &str) -> Result<Fd, Errno>
88where
89 S: Open + Close + Dup,
90{
91 let dirs: Box<dyn Iterator<Item = &str>> = if filename.contains('/') {
92 Box::new(std::iter::once("."))
93 } else {
94 env.variables
95 .get(PATH)
96 .and_then(|v| v.value.as_ref())
97 .map_or(Box::new(std::iter::empty()), |v| Box::new(v.split()))
98 };
100
101 for dir in dirs {
104 let path = PathBuf::from_iter([dir, filename])
105 .into_unix_string()
106 .into_vec();
107 if let Ok(c_path) = CString::new(path) {
108 if let Ok(fd) = open_file(&env.system, &c_path).await {
109 return Ok(fd);
110 }
111 }
112 }
113 Err(Errno::ENOENT)
114}
115
116async fn open_file<S>(system: &S, path: &CStr) -> Result<Fd, Errno>
121where
122 S: Open + Close + Dup + ?Sized,
123{
124 system
125 .open(
126 path,
127 OfdAccess::ReadOnly,
128 OpenFlag::CloseOnExec.into(),
129 Mode::empty(),
130 )
131 .await
132 .and_then(|fd| move_fd_internal(system, fd))
133}
134
135fn consume_return(divert: ControlFlow<Divert>) -> (Option<ExitStatus>, ControlFlow<Divert>) {
142 match divert {
143 ControlFlow::Break(Divert::Return(exit_status)) => (exit_status, ControlFlow::Continue(())),
144 other => (None, other),
145 }
146}
147
148async fn report_find_and_open_file_failure<S>(
151 env: &mut Env<S>,
152 name: &Field,
153 errno: Errno,
154) -> crate::Result
155where
156 S: Fcntl + Isatty + Write,
157{
158 let mut report = Report::new();
159 report.r#type = ReportType::Error;
160 report.title = "cannot open script file".into();
161 report.snippets = Snippet::with_primary_span(&name.origin, format!("`{name}`: {errno}").into());
162 report_failure(env, report).await
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use assert_matches::assert_matches;
169 use enumset::EnumSet;
170 use futures_util::FutureExt as _;
171 use std::cell::RefCell;
172 use std::rc::Rc;
173 use yash_env::VirtualSystem;
174 use yash_env::io::MIN_INTERNAL_FD;
175 use yash_env::path::Path;
176 use yash_env::system::FdFlag;
177 use yash_env::system::r#virtual::Inode;
178 use yash_env::variable::Scope;
179
180 fn system_with_file<P: AsRef<Path>, C: Into<Vec<u8>>>(path: P, content: C) -> VirtualSystem {
181 let system = VirtualSystem::new();
182 let mut state = system.state.borrow_mut();
183 let content = Rc::new(RefCell::new(Inode::new(content)));
184 state.file_system.save(path, content).unwrap();
185 drop(state);
186 system
187 }
188
189 #[test]
190 fn no_path_search_with_pathname_containing_slash() {
191 let system = VirtualSystem::new();
192 let inode = Rc::new(RefCell::new(Inode::new("")));
193 {
194 let mut state = system.state.borrow_mut();
195 let content = Rc::new(RefCell::new(Inode::new("")));
196 state.file_system.save("/bar/file", content).unwrap();
197 let content = Rc::new(RefCell::new(Inode::new("")));
198 state.file_system.save("/baz/file", content).unwrap();
199 let content = Rc::clone(&inode);
200 state.file_system.save("/file", content).unwrap();
201 }
202 let mut env = Env::with_system(system.clone());
203 env.variables
204 .get_or_new(PATH, Scope::Global)
205 .assign("/foo:/bar:/baz", None)
206 .unwrap();
207
208 let result = find_and_open_file(&mut env, "./file")
211 .now_or_never()
212 .unwrap();
213
214 let fd = result.unwrap();
217 _ = system.with_open_file_description(fd, |ofd| {
218 assert!(Rc::ptr_eq(ofd.inode(), &inode));
219 Ok(())
220 });
221 }
222
223 #[test]
224 fn file_found_in_path() {
225 let system = VirtualSystem::new();
226 let inode = Rc::new(RefCell::new(Inode::new("")));
227 {
228 let mut state = system.state.borrow_mut();
229 let content = Rc::clone(&inode);
230 state.file_system.save("/bar/file", content).unwrap();
231 let content = Rc::new(RefCell::new(Inode::new("")));
232 state.file_system.save("/baz/file", content).unwrap();
233 let content = Rc::new(RefCell::new(Inode::new("")));
234 state.file_system.save("/file", content).unwrap();
235 }
236 let mut env = Env::with_system(system.clone());
237 env.variables
238 .get_or_new(PATH, Scope::Global)
239 .assign("/foo:/bar:/baz", None)
240 .unwrap();
241
242 let result = find_and_open_file(&mut env, "file").now_or_never().unwrap();
245
246 let fd = result.unwrap();
248 _ = system.with_open_file_description(fd, |ofd| {
249 assert!(Rc::ptr_eq(ofd.inode(), &inode));
250 Ok(())
251 });
252 }
253
254 #[test]
255 fn open_file_result_lower_bound() {
256 let system = system_with_file("/foo/file", "");
257 let result = open_file(&system, c"/foo/file").now_or_never().unwrap();
258 assert_matches!(result, Ok(fd) if fd >= MIN_INTERNAL_FD);
259 }
260
261 #[test]
262 fn open_file_result_cloexec() {
263 let system = system_with_file("/foo/file", "");
264 let fd = open_file(&system, c"/foo/file")
265 .now_or_never()
266 .unwrap()
267 .unwrap();
268
269 let process = system.current_process();
270 let fd_body = process.get_fd(fd).unwrap();
271 assert_eq!(fd_body.flags, EnumSet::only(FdFlag::CloseOnExec));
272 }
273
274 #[test]
275 fn fd_is_closed_after_execute() {
276 let system = system_with_file("/foo/file", "");
277 let mut env = Env::with_system(system.clone());
278 env.any.insert(Box::new(RunReadEvalLoop::<VirtualSystem>(
279 |_env, _lexer| Box::pin(async { ControlFlow::Continue(()) }),
280 )));
281 let command = Command {
282 file: Field::dummy("/foo/file"),
283 params: vec![],
284 };
285
286 _ = command.execute(&mut env).now_or_never().unwrap();
287
288 let process = system.current_process();
289 for fd in 3..50 {
290 assert_matches!(process.get_fd(Fd(fd)), None, "fd={fd}");
291 }
292 }
293}