Skip to main content

yash_env/
pwd.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 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//! Working directory path handling
18
19use super::Env;
20use crate::path::Path;
21use crate::system::AT_FDCWD;
22use crate::system::Errno;
23use crate::system::Fstat;
24use crate::system::GetCwd;
25use crate::system::Stat as _;
26use crate::variable::AssignError;
27use crate::variable::PWD;
28use crate::variable::Scope::Global;
29use std::ffi::CString;
30use thiserror::Error;
31
32/// Tests whether a path contains a dot (`.`) or dot-dot (`..`) component.
33fn has_dot_or_dot_dot(path: &str) -> bool {
34    path.split('/').any(|c| c == "." || c == "..")
35}
36
37/// Error in [`Env::prepare_pwd`]
38#[derive(Clone, Debug, Eq, Error, PartialEq)]
39pub enum PreparePwdError {
40    /// Error assigning to the `$PWD` variable
41    #[error(transparent)]
42    AssignError(#[from] AssignError),
43
44    /// Error obtaining the current working directory path
45    #[error("cannot obtain the current working directory path: {0}")]
46    GetCwdError(#[from] Errno),
47}
48
49impl<S> Env<S> {
50    /// Returns the value of the `$PWD` variable if it is correct.
51    ///
52    /// The variable is correct if:
53    ///
54    /// - it is a scalar variable,
55    /// - its value is a pathname of the current working directory (possibly
56    ///   including symbolic link components), and
57    /// - there is no dot (`.`) or dot-dot (`..`) component in the pathname.
58    #[must_use]
59    pub fn get_pwd_if_correct(&self) -> Option<&str>
60    where
61        S: Fstat,
62    {
63        self.variables.get_scalar(PWD).filter(|pwd| {
64            if !Path::new(pwd).is_absolute() {
65                return false;
66            }
67            if has_dot_or_dot_dot(pwd) {
68                return false;
69            }
70            let Ok(cstr_pwd) = CString::new(pwd.as_bytes()) else {
71                return false;
72            };
73            let Ok(s1) = self.system.fstatat(AT_FDCWD, &cstr_pwd, true) else {
74                return false;
75            };
76            let Ok(s2) = self.system.fstatat(AT_FDCWD, c".", true) else {
77                return false;
78            };
79            s1.identity() == s2.identity()
80        })
81    }
82
83    /// Tests if the `$PWD` variable is correct.
84    #[inline]
85    #[must_use]
86    fn has_correct_pwd(&self) -> bool
87    where
88        S: Fstat,
89    {
90        self.get_pwd_if_correct().is_some()
91    }
92
93    /// Updates the `$PWD` variable with the current working directory.
94    ///
95    /// If the value of `$PWD` is [correct](Self::get_pwd_if_correct), this
96    /// function does not modify it. Otherwise, this function sets the value to
97    /// `self.system.getcwd()`.
98    ///
99    /// This function is meant for initializing the `$PWD` variable when the
100    /// shell starts.
101    pub fn prepare_pwd(&mut self) -> Result<(), PreparePwdError>
102    where
103        S: Fstat + GetCwd,
104    {
105        if !self.has_correct_pwd() {
106            let dir = self
107                .system
108                .getcwd()?
109                .into_unix_string()
110                .into_string()
111                .map_err(|_| Errno::EILSEQ)?;
112            let mut var = self.variables.get_or_new(PWD, Global);
113            var.assign(dir, None)?;
114            var.export(true);
115        }
116        Ok(())
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::VirtualSystem;
124    use crate::path::PathBuf;
125    use crate::system::r#virtual::FileBody;
126    use crate::system::r#virtual::Inode;
127    use crate::variable::Value;
128    use std::cell::RefCell;
129    use std::rc::Rc;
130
131    #[test]
132    fn has_dot_or_dot_dot_cases() {
133        assert!(!has_dot_or_dot_dot(""));
134        assert!(!has_dot_or_dot_dot("foo"));
135        assert!(!has_dot_or_dot_dot(".foo"));
136        assert!(!has_dot_or_dot_dot("foo.bar"));
137        assert!(!has_dot_or_dot_dot("..."));
138        assert!(!has_dot_or_dot_dot("/"));
139        assert!(!has_dot_or_dot_dot("/bar"));
140        assert!(!has_dot_or_dot_dot("/bar/baz"));
141
142        assert!(has_dot_or_dot_dot("."));
143        assert!(has_dot_or_dot_dot("/."));
144        assert!(has_dot_or_dot_dot("./"));
145        assert!(has_dot_or_dot_dot("/./"));
146        assert!(has_dot_or_dot_dot("foo/.//bar"));
147
148        assert!(has_dot_or_dot_dot(".."));
149        assert!(has_dot_or_dot_dot("/.."));
150        assert!(has_dot_or_dot_dot("../"));
151        assert!(has_dot_or_dot_dot("/../"));
152        assert!(has_dot_or_dot_dot("/foo//../bar"));
153    }
154
155    fn env_with_symlink_to_dir() -> Env<VirtualSystem> {
156        let system = VirtualSystem::new();
157        let mut state = system.state.borrow_mut();
158        state
159            .file_system
160            .save(
161                "/foo/bar/dir",
162                Rc::new(RefCell::new(Inode {
163                    body: FileBody::Directory {
164                        files: Default::default(),
165                    },
166                    permissions: Default::default(),
167                })),
168            )
169            .unwrap();
170        state
171            .file_system
172            .save(
173                "/foo/link",
174                Rc::new(RefCell::new(Inode {
175                    body: FileBody::Symlink {
176                        target: "bar/dir".into(),
177                    },
178                    permissions: Default::default(),
179                })),
180            )
181            .unwrap();
182        drop(state);
183        system.current_process_mut().cwd = PathBuf::from("/foo/bar/dir");
184        Env::with_system(system)
185    }
186
187    #[test]
188    fn prepare_pwd_no_value() {
189        let mut env = env_with_symlink_to_dir();
190
191        let result = env.prepare_pwd();
192        assert_eq!(result, Ok(()));
193        let pwd = env.variables.get(PWD).unwrap();
194        assert_eq!(pwd.value, Some(Value::scalar("/foo/bar/dir")));
195        assert!(pwd.is_exported);
196    }
197
198    #[test]
199    fn prepare_pwd_with_correct_path() {
200        let mut env = env_with_symlink_to_dir();
201        env.variables
202            .get_or_new(PWD, Global)
203            .assign("/foo/link", None)
204            .unwrap();
205
206        let result = env.prepare_pwd();
207        assert_eq!(result, Ok(()));
208        let pwd = env.variables.get(PWD).unwrap();
209        assert_eq!(pwd.value, Some(Value::scalar("/foo/link")));
210    }
211
212    #[test]
213    fn prepare_pwd_with_dot() {
214        let mut env = env_with_symlink_to_dir();
215        env.variables
216            .get_or_new(PWD, Global)
217            .assign("/foo/./link", None)
218            .unwrap();
219
220        let result = env.prepare_pwd();
221        assert_eq!(result, Ok(()));
222        let pwd = env.variables.get(PWD).unwrap();
223        assert_eq!(pwd.value, Some(Value::scalar("/foo/bar/dir")));
224        assert!(pwd.is_exported);
225    }
226
227    #[test]
228    fn prepare_pwd_with_dot_dot() {
229        let mut env = env_with_symlink_to_dir();
230        env.variables
231            .get_or_new(PWD, Global)
232            .assign("/foo/./link", None)
233            .unwrap();
234
235        let result = env.prepare_pwd();
236        assert_eq!(result, Ok(()));
237        let pwd = env.variables.get(PWD).unwrap();
238        assert_eq!(pwd.value, Some(Value::scalar("/foo/bar/dir")));
239        assert!(pwd.is_exported);
240    }
241
242    #[test]
243    fn prepare_pwd_with_wrong_path() {
244        let mut env = env_with_symlink_to_dir();
245        env.variables
246            .get_or_new(PWD, Global)
247            .assign("/foo/bar", None)
248            .unwrap();
249
250        let result = env.prepare_pwd();
251        assert_eq!(result, Ok(()));
252        let pwd = env.variables.get(PWD).unwrap();
253        assert_eq!(pwd.value, Some(Value::scalar("/foo/bar/dir")));
254        assert!(pwd.is_exported);
255    }
256
257    #[test]
258    fn prepare_pwd_with_non_absolute_path() {
259        let system = VirtualSystem::new();
260        let mut state = system.state.borrow_mut();
261        state
262            .file_system
263            .save(
264                "/link",
265                Rc::new(RefCell::new(Inode {
266                    body: FileBody::Symlink { target: ".".into() },
267                    permissions: Default::default(),
268                })),
269            )
270            .unwrap();
271        drop(state);
272        system.current_process_mut().cwd = PathBuf::from("/");
273
274        let mut env = Env::with_system(system);
275        env.variables
276            .get_or_new(PWD, Global)
277            .assign("link", None)
278            .unwrap();
279
280        let result = env.prepare_pwd();
281        assert_eq!(result, Ok(()));
282        let pwd = env.variables.get(PWD).unwrap();
283        assert_eq!(pwd.value, Some(Value::scalar("/")));
284        assert!(pwd.is_exported);
285    }
286}