Skip to main content

yash_builtin/source/
semantics.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Implementation of the main behavior of the `.` built-in
18
19use 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    /// Executes the `.` built-in.
43    ///
44    /// If the file is not found or cannot be read, this method reports an error
45    /// to the standard error and returns `ExitStatus::FAILURE.into()`.
46    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        // TODO set positional parameters
58
59        // Parse and execute the command script
60        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
83/// Finds and opens the file to be executed.
84///
85/// If the name does not contain a slash, this function searches the file in the
86/// `$PATH` variable.
87async 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        // TODO If not in POSIX mode, search in the current working directory too
99    };
100
101    // Iterate over the directories trying to open the file in each directory
102    // and return the first successfully opened file descriptor.
103    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
116/// Opens the file to be executed.
117///
118/// The returned file descriptor is opened with the `O_CLOEXEC` flag and is at
119/// least [`MIN_INTERNAL_FD`](yash_env::io::MIN_INTERNAL_FD).
120async 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
135/// Handles the result of the `return` built-in possibly executed in the
136/// command script.
137///
138/// This function returns an optional exit status and a possibly modified
139/// divert. The exit status should override the current exit status if it is
140/// `Some`. The divert should be passed to the caller of the `.` built-in.
141fn 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
148/// Reports an error that occurred while preparing the file descriptor to read
149/// from.
150async 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        // The pathname parameter contains a slash, so the file is not searched
209        // in the $PATH variable.
210        let result = find_and_open_file(&mut env, "./file")
211            .now_or_never()
212            .unwrap();
213
214        // The expected file is "/file" since the default working directory is
215        // "/".
216        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        // The pathname parameter does not contain a slash, so the file is
243        // searched in the $PATH variable.
244        let result = find_and_open_file(&mut env, "file").now_or_never().unwrap();
245
246        // The expected file is "/bar/file".
247        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}