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