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