yash_env/system/virtual/
file_system.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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//! File system in a virtual system.
18
19use super::super::{Dir, DirEntry, Errno, FileType, Gid, Stat, Uid};
20use crate::path::{Component, Path, PathBuf};
21use crate::str::UnixStr;
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::collections::VecDeque;
25use std::fmt::Debug;
26use std::rc::Rc;
27
28const DEFAULT_DIRECTORY_MODE: Mode = Mode::USER_ALL.union(Mode::ALL_READ).union(Mode::ALL_EXEC);
29
30/// Collection of files.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct FileSystem {
33    /// Root directory
34    pub root: Rc<RefCell<Inode>>,
35}
36
37/// The default file system only contains an empty root directory.
38impl Default for FileSystem {
39    fn default() -> Self {
40        FileSystem {
41            root: Rc::new(RefCell::new(Inode {
42                body: FileBody::Directory {
43                    files: HashMap::new(),
44                },
45                permissions: DEFAULT_DIRECTORY_MODE,
46            })),
47        }
48    }
49}
50
51impl FileSystem {
52    /// Saves a file.
53    ///
54    /// If there is an existing file at the specified path, this function
55    /// replaces it the new file and returns the old one, regardless of
56    /// permissions.
57    ///
58    /// TODO Reject relative path
59    pub fn save<P: AsRef<Path>>(
60        &mut self,
61        path: P,
62        content: Rc<RefCell<Inode>>,
63    ) -> Result<Option<Rc<RefCell<Inode>>>, Errno> {
64        fn ensure_dir(body: &mut FileBody) -> &mut HashMap<Rc<UnixStr>, Rc<RefCell<Inode>>> {
65            match body {
66                FileBody::Directory { files } => files,
67                _ => {
68                    let files = HashMap::new();
69                    *body = FileBody::Directory { files };
70                    match body {
71                        FileBody::Directory { files } => files,
72                        _ => unreachable!(),
73                    }
74                }
75            }
76        }
77
78        fn main(
79            fs: &mut FileSystem,
80            path: &Path,
81            content: Rc<RefCell<Inode>>,
82        ) -> Result<Option<Rc<RefCell<Inode>>>, Errno> {
83            let mut components = path.components();
84            let file_name = match components.next_back().ok_or(Errno::ENOENT)? {
85                Component::Normal(name) => name,
86                _ => return Err(Errno::ENOENT),
87            };
88
89            // Create parent directories
90            let mut node = Rc::clone(&fs.root);
91            for component in components {
92                let name = match component {
93                    Component::Normal(name) => name,
94                    Component::RootDir => continue,
95                    _ => return Err(Errno::ENOENT),
96                };
97                let mut node_ref = node.borrow_mut();
98                let children = ensure_dir(&mut node_ref.body);
99                use std::collections::hash_map::Entry::*;
100                let child = match children.entry(Rc::from(name)) {
101                    Occupied(occupied) => Rc::clone(occupied.get()),
102                    Vacant(vacant) => {
103                        let child = Rc::new(RefCell::new(Inode {
104                            body: FileBody::Directory {
105                                files: HashMap::new(),
106                            },
107                            permissions: DEFAULT_DIRECTORY_MODE,
108                        }));
109                        Rc::clone(vacant.insert(child))
110                    }
111                };
112                drop(node_ref);
113                node = child;
114            }
115
116            let mut parent_ref = node.borrow_mut();
117            let children = ensure_dir(&mut parent_ref.body);
118            Ok(children.insert(Rc::from(file_name), content))
119        }
120
121        main(self, path.as_ref(), content)
122    }
123
124    /// Returns a reference to the existing file at the specified path.
125    ///
126    /// TODO Reject relative path
127    pub fn get<P: AsRef<Path>>(&self, path: P) -> Result<Rc<RefCell<Inode>>, Errno> {
128        fn main(fs: &FileSystem, path: &Path) -> Result<Rc<RefCell<Inode>>, Errno> {
129            let components = path.components();
130            let mut nodes = vec![Rc::clone(&fs.root)];
131            for component in components {
132                let name = match component {
133                    Component::Normal(name) => name,
134                    Component::RootDir | Component::CurDir => continue,
135                    Component::ParentDir => {
136                        if nodes.len() > 1 {
137                            nodes.pop();
138                        }
139                        continue;
140                    }
141                };
142
143                let node_ref = nodes.last().unwrap().borrow();
144                let children = match &node_ref.body {
145                    FileBody::Directory { files } => files,
146                    _ => return Err(Errno::ENOTDIR),
147                };
148
149                if !node_ref.permissions.contains(Mode::USER_EXEC) {
150                    return Err(Errno::EACCES);
151                }
152
153                let child = Rc::clone(children.get(name).ok_or(Errno::ENOENT)?);
154                drop(node_ref);
155                nodes.push(child);
156            }
157
158            let node = nodes.pop().unwrap();
159            if path.as_unix_str().as_bytes().ends_with(b"/")
160                && !matches!(&node.borrow().body, FileBody::Directory { .. })
161            {
162                return Err(Errno::ENOTDIR);
163            }
164            Ok(node)
165        }
166
167        main(self, path.as_ref())
168    }
169}
170
171/// File on the file system
172#[derive(Clone, Debug, Default, Eq, PartialEq)]
173pub struct Inode {
174    /// File content
175    pub body: FileBody,
176    /// Access permissions
177    pub permissions: Mode,
178    // TODO owner user and group, etc.
179}
180
181impl Inode {
182    /// Create a regular file with the given content.
183    pub fn new<T: Into<Vec<u8>>>(bytes: T) -> Self {
184        Inode {
185            body: FileBody::new(bytes),
186            permissions: Mode::default(),
187        }
188    }
189
190    /// Returns the metadata of the file.
191    ///
192    /// Currently, only the following fields are filled:
193    ///
194    /// - `ino`
195    /// - `mode`
196    /// - `type`
197    /// - `size`
198    #[must_use]
199    pub fn stat(&self) -> Stat {
200        Stat {
201            dev: 1,
202            ino: self as *const Self as u64,
203            mode: self.permissions,
204            r#type: self.body.r#type(),
205            nlink: 1,
206            uid: Uid(1),
207            gid: Gid(1),
208            size: self.body.size() as u64,
209        }
210    }
211}
212
213/// Filetype-specific content of a file
214#[derive(Clone, Debug, Eq, PartialEq)]
215#[non_exhaustive]
216pub enum FileBody {
217    /// Regular file
218    Regular {
219        /// File content
220        content: Vec<u8>,
221        /// Whether this file is a native binary that can be exec'ed
222        is_native_executable: bool,
223    },
224    /// Directory
225    Directory {
226        /// Files contained in this directory
227        ///
228        /// The keys of the hashmap are filenames without any parent directory
229        /// components. The hashmap does not contain "." or "..".
230        files: HashMap<Rc<UnixStr>, Rc<RefCell<Inode>>>,
231        // The hash map contents are reference-counted to allow making cheap
232        // copies of them, which is especially handy when traversing entries.
233    },
234    /// Named pipe
235    Fifo {
236        /// Content of the pipe
237        content: VecDeque<u8>,
238        /// Number of open file descriptions reading from this pipe
239        readers: usize,
240        /// Number of open file descriptions writing to this pipe
241        writers: usize,
242    },
243    /// Symbolic link
244    Symlink {
245        /// Path to the file referenced by this symlink
246        target: PathBuf,
247    },
248    /// Terminal device
249    ///
250    /// This is a dummy device that works like a regular file.
251    Terminal {
252        /// Virtual file content
253        content: Vec<u8>,
254    },
255    // TODO Other filetypes
256}
257
258/// The default file body is an empty regular file.
259impl Default for FileBody {
260    fn default() -> Self {
261        FileBody::Regular {
262            content: Vec::default(),
263            is_native_executable: bool::default(),
264        }
265    }
266}
267
268impl FileBody {
269    /// Creates a regular file body with the given content.
270    pub fn new<T: Into<Vec<u8>>>(bytes: T) -> Self {
271        FileBody::Regular {
272            content: bytes.into(),
273            is_native_executable: false,
274        }
275    }
276
277    /// Returns the type of the file.
278    #[must_use]
279    pub const fn r#type(&self) -> FileType {
280        match self {
281            Self::Regular { .. } => FileType::Regular,
282            Self::Directory { .. } => FileType::Directory,
283            Self::Fifo { .. } => FileType::Fifo,
284            Self::Symlink { .. } => FileType::Symlink,
285            Self::Terminal { .. } => FileType::CharacterDevice,
286        }
287    }
288
289    /// Returns the size of the file.
290    #[must_use]
291    pub fn size(&self) -> usize {
292        match self {
293            Self::Regular { content, .. } => content.len(),
294            Self::Directory { files } => files.len(),
295            Self::Fifo { content, .. } => content.len(),
296            Self::Symlink { target } => target.as_unix_str().len(),
297            Self::Terminal { .. } => 0,
298        }
299    }
300}
301
302/// This type alias exists only for historical reasons.
303/// Please use `yash_env::system::Mode` instead.
304#[deprecated = "use yash_env::system::Mode instead"]
305pub use super::super::Mode;
306
307/// Implementor of [`Dir`] for virtual file system
308#[derive(Clone, Debug)]
309pub struct VirtualDir<I> {
310    iter: I,
311    current: Rc<UnixStr>,
312}
313
314impl<I> VirtualDir<I> {
315    /// Creates a `VirtualDir` that yields entries from an iterator.
316    #[must_use]
317    pub fn new<J>(iter: J) -> Self
318    where
319        J: IntoIterator<IntoIter = I, Item = Rc<UnixStr>>,
320    {
321        VirtualDir {
322            iter: iter.into_iter(),
323            current: Rc::from(UnixStr::new("")),
324        }
325    }
326}
327
328/// Creates a `VirtualDir` that yields entries of a directory.
329///
330/// This function will fail if the given file body is not a directory.
331impl TryFrom<&FileBody> for VirtualDir<std::vec::IntoIter<Rc<UnixStr>>> {
332    type Error = Errno;
333    fn try_from(file: &FileBody) -> Result<Self, Errno> {
334        let FileBody::Directory { files } = file else {
335            return Err(Errno::ENOTDIR);
336        };
337
338        let mut entries = Vec::with_capacity(files.len() + 2);
339        entries.push(Rc::from(UnixStr::new(".")));
340        entries.push(Rc::from(UnixStr::new("..")));
341        entries.extend(files.keys().cloned());
342
343        // You should not pose any assumption on the order of entries.
344        // Here, we deliberately disorder the entries.
345        let entry = entries.pop().unwrap();
346        let i = entries.len() / 2;
347        entries.insert(i, entry);
348
349        Ok(Self::new(entries))
350    }
351}
352
353impl<I> Dir for VirtualDir<I>
354where
355    I: Debug,
356    I: Iterator<Item = Rc<UnixStr>>,
357{
358    fn next(&mut self) -> Result<Option<DirEntry<'_>>, Errno> {
359        match self.iter.next() {
360            Some(name) => {
361                self.current = name;
362                let name = &self.current;
363                Ok(Some(DirEntry { name }))
364            }
365            None => {
366                self.current = Rc::from(UnixStr::new(""));
367                Ok(None)
368            }
369        }
370    }
371}
372
373// TODO impl Drop for VirtualDir: close backing file descriptor
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use assert_matches::assert_matches;
379
380    #[test]
381    fn file_system_get_root() {
382        let fs = FileSystem::default();
383        let result = fs.get("/");
384        assert_eq!(result, Ok(fs.root));
385    }
386
387    #[test]
388    fn file_system_save_and_get_file() {
389        let mut fs = FileSystem::default();
390        let file_1 = Rc::new(RefCell::new(Inode::new([12, 34, 56])));
391        let old = fs.save("/foo/bar", Rc::clone(&file_1));
392        assert_eq!(old, Ok(None));
393
394        let file_2 = Rc::new(RefCell::new(Inode::new([98, 76, 54])));
395        let old = fs.save("/foo/bar", Rc::clone(&file_2));
396        assert_eq!(old, Ok(Some(file_1)));
397
398        let result = fs.get("/foo/bar");
399        assert_eq!(result, Ok(file_2));
400    }
401
402    #[test]
403    fn file_system_save_and_get_directory() {
404        let mut fs = FileSystem::default();
405        let file = Rc::new(RefCell::new(Inode::new([12, 34, 56])));
406        let old = fs.save("/foo/bar", Rc::clone(&file));
407        assert_eq!(old, Ok(None));
408
409        let dir = fs.get("/foo").unwrap();
410        let dir = dir.borrow();
411        assert_eq!(dir.permissions, Mode::from_bits_retain(0o755));
412        assert_matches!(&dir.body, FileBody::Directory { files } => {
413            let mut i = files.iter();
414            let (name, content) = i.next().unwrap();
415            assert_eq!(name.as_bytes(), b"bar");
416            assert_eq!(content, &file);
417            assert_eq!(i.next(), None);
418        });
419    }
420
421    #[test]
422    fn file_system_save_invalid_name() {
423        let mut fs = FileSystem::default();
424        let old = fs.save("", Rc::default());
425        assert_eq!(old, Err(Errno::ENOENT));
426    }
427
428    #[test]
429    fn file_system_get_parents() {
430        let mut fs = FileSystem::default();
431        let file = Rc::new(RefCell::new(Inode::new([123])));
432        _ = fs.save("/dir/dir1/file", Rc::clone(&file));
433        _ = fs.save("/dir/dir2/dir3/file", Rc::default());
434        assert_eq!(fs.get("/dir/dir2/dir3/../../dir1/file").unwrap(), file);
435        assert_eq!(fs.get("/../dir/dir1/file").unwrap(), file);
436    }
437
438    #[test]
439    fn file_system_get_non_existent_file() {
440        let fs = FileSystem::default();
441        let result = fs.get("/no_such_file");
442        assert_eq!(result, Err(Errno::ENOENT));
443        let result = fs.get("/no_such_directory/foo");
444        assert_eq!(result, Err(Errno::ENOENT));
445    }
446
447    #[test]
448    fn file_system_get_not_directory() {
449        let mut fs = FileSystem::default();
450        let _ = fs.save("/file", Rc::default());
451        let result = fs.get("/file/");
452        assert_eq!(result, Err(Errno::ENOTDIR));
453        let result = fs.get("/file/foo");
454        assert_eq!(result, Err(Errno::ENOTDIR));
455    }
456
457    #[test]
458    fn file_system_get_no_search_permission() {
459        let mut fs = FileSystem::default();
460        let _ = fs.save("/dir/file", Rc::default());
461        {
462            let dir = fs.get("/dir").unwrap();
463            dir.borrow_mut().permissions = Mode::from_bits_retain(0o666);
464        }
465        let result = fs.get("/dir/file");
466        assert_eq!(result, Err(Errno::EACCES));
467    }
468
469    #[test]
470    fn empty_virtual_dir() {
471        let mut dir = VirtualDir::new(std::iter::empty());
472        assert_matches!(dir.next(), Ok(None));
473    }
474
475    #[test]
476    fn non_empty_virtual_dir() {
477        let iter = ["foo", "bar"]
478            .into_iter()
479            .map(|s| Rc::from(UnixStr::new(s)));
480        let mut dir = VirtualDir::new(iter);
481        assert_matches!(dir.next(), Ok(Some(entry)) => {
482            assert_eq!(entry.name, "foo");
483        });
484        assert_matches!(dir.next(), Ok(Some(entry)) => {
485            assert_eq!(entry.name, "bar");
486        });
487        assert_matches!(dir.next(), Ok(None));
488    }
489
490    #[test]
491    fn virtual_dir_try_from_file_body_directory() {
492        let files = ["one", "2", "three"]
493            .into_iter()
494            .map(|name| (Rc::from(UnixStr::new(name)), Rc::default()))
495            .collect();
496        let file = FileBody::Directory { files };
497        let mut dir = VirtualDir::try_from(&file).unwrap();
498
499        let mut files = Vec::new();
500        while let Some(entry) = dir.next().unwrap() {
501            files.push(entry.name.to_str().unwrap().to_string());
502        }
503        files.sort_unstable();
504        let files: Vec<&str> = files.iter().map(String::as_str).collect();
505        assert_eq!(files, [".", "..", "2", "one", "three"]);
506    }
507
508    #[test]
509    fn virtual_dir_try_from_file_body_non_directory() {
510        let file = FileBody::Regular {
511            content: Default::default(),
512            is_native_executable: false,
513        };
514        let result = VirtualDir::try_from(&file);
515        assert_eq!(result.unwrap_err(), Errno::ENOTDIR);
516    }
517}