Skip to main content

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, Uid};
20use super::FileBody;
21use crate::path::{Component, Path};
22use crate::str::UnixStr;
23use std::cell::RefCell;
24use std::collections::HashMap;
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/// This type alias exists only for historical reasons.
214/// Please use `yash_env::system::Mode` instead.
215#[deprecated = "use yash_env::system::Mode instead"]
216pub use super::super::Mode;
217
218/// File status
219///
220/// This type is a collection of file status information. It is similar to the
221/// `stat` structure defined in the POSIX standard, but it is simplified and
222/// does not include all fields of the `stat` structure.
223#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
224#[non_exhaustive]
225pub struct Stat {
226    /// Device ID
227    pub dev: u64,
228    /// Inode number
229    pub ino: u64,
230    /// Access permissions
231    ///
232    /// Note that this field does not include the file type bits.
233    /// The file type is stored in the `type` field.
234    pub mode: Mode,
235    /// File type
236    pub r#type: FileType,
237    /// Number of hard links
238    pub nlink: u64,
239    /// User ID of the file owner
240    pub uid: Uid,
241    /// Group ID of the file owner
242    pub gid: Gid,
243    /// Length of the file in bytes
244    pub size: u64,
245    // TODO: atime, mtime, ctime, (birthtime)
246}
247
248impl Stat {
249    /// Returns the device ID and inode number as a tuple
250    ///
251    /// This method is useful for testing whether two `Stat` objects refer to
252    /// the same file.
253    #[inline]
254    #[must_use]
255    pub const fn identity(&self) -> (u64, u64) {
256        (self.dev, self.ino)
257    }
258}
259
260impl super::super::Stat for Stat {
261    #[inline(always)]
262    fn dev(&self) -> u64 {
263        self.dev
264    }
265    #[inline(always)]
266    fn ino(&self) -> u64 {
267        self.ino
268    }
269    #[inline(always)]
270    fn mode(&self) -> Mode {
271        self.mode
272    }
273    #[inline(always)]
274    fn r#type(&self) -> FileType {
275        self.r#type
276    }
277    #[inline(always)]
278    fn nlink(&self) -> u64 {
279        self.nlink
280    }
281    #[inline(always)]
282    fn uid(&self) -> Uid {
283        self.uid
284    }
285    #[inline(always)]
286    fn gid(&self) -> Gid {
287        self.gid
288    }
289    #[inline(always)]
290    fn size(&self) -> u64 {
291        self.size
292    }
293}
294
295/// Implementor of [`Dir`] for virtual file system
296#[derive(Clone, Debug)]
297pub struct VirtualDir<I> {
298    iter: I,
299    current: Rc<UnixStr>,
300}
301
302impl<I> VirtualDir<I> {
303    /// Creates a `VirtualDir` that yields entries from an iterator.
304    #[must_use]
305    pub fn new<J>(iter: J) -> Self
306    where
307        J: IntoIterator<IntoIter = I, Item = Rc<UnixStr>>,
308    {
309        VirtualDir {
310            iter: iter.into_iter(),
311            current: Rc::from(UnixStr::new("")),
312        }
313    }
314}
315
316/// Creates a `VirtualDir` that yields entries of a directory.
317///
318/// This function will fail if the given file body is not a directory.
319impl TryFrom<&FileBody> for VirtualDir<std::vec::IntoIter<Rc<UnixStr>>> {
320    type Error = Errno;
321    fn try_from(file: &FileBody) -> Result<Self, Errno> {
322        let FileBody::Directory { files } = file else {
323            return Err(Errno::ENOTDIR);
324        };
325
326        let mut entries = Vec::with_capacity(files.len() + 2);
327        entries.push(Rc::from(UnixStr::new(".")));
328        entries.push(Rc::from(UnixStr::new("..")));
329        entries.extend(files.keys().cloned());
330
331        // You should not pose any assumption on the order of entries.
332        // Here, we deliberately disorder the entries.
333        let entry = entries.pop().unwrap();
334        let i = entries.len() / 2;
335        entries.insert(i, entry);
336
337        Ok(Self::new(entries))
338    }
339}
340
341impl<I> Dir for VirtualDir<I>
342where
343    I: Debug,
344    I: Iterator<Item = Rc<UnixStr>>,
345{
346    fn next(&mut self) -> Result<Option<DirEntry<'_>>, Errno> {
347        match self.iter.next() {
348            Some(name) => {
349                self.current = name;
350                let name = &self.current;
351                Ok(Some(DirEntry { name }))
352            }
353            None => {
354                self.current = Rc::from(UnixStr::new(""));
355                Ok(None)
356            }
357        }
358    }
359}
360
361// TODO impl Drop for VirtualDir: close backing file descriptor
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use assert_matches::assert_matches;
367
368    #[test]
369    fn file_system_get_root() {
370        let fs = FileSystem::default();
371        let result = fs.get("/");
372        assert_eq!(result, Ok(fs.root));
373    }
374
375    #[test]
376    fn file_system_save_and_get_file() {
377        let mut fs = FileSystem::default();
378        let file_1 = Rc::new(RefCell::new(Inode::new([12, 34, 56])));
379        let old = fs.save("/foo/bar", Rc::clone(&file_1));
380        assert_eq!(old, Ok(None));
381
382        let file_2 = Rc::new(RefCell::new(Inode::new([98, 76, 54])));
383        let old = fs.save("/foo/bar", Rc::clone(&file_2));
384        assert_eq!(old, Ok(Some(file_1)));
385
386        let result = fs.get("/foo/bar");
387        assert_eq!(result, Ok(file_2));
388    }
389
390    #[test]
391    fn file_system_save_and_get_directory() {
392        let mut fs = FileSystem::default();
393        let file = Rc::new(RefCell::new(Inode::new([12, 34, 56])));
394        let old = fs.save("/foo/bar", Rc::clone(&file));
395        assert_eq!(old, Ok(None));
396
397        let dir = fs.get("/foo").unwrap();
398        let dir = dir.borrow();
399        assert_eq!(dir.permissions, Mode::from_bits_retain(0o755));
400        assert_matches!(&dir.body, FileBody::Directory { files } => {
401            let mut i = files.iter();
402            let (name, content) = i.next().unwrap();
403            assert_eq!(name.as_bytes(), b"bar");
404            assert_eq!(content, &file);
405            assert_eq!(i.next(), None);
406        });
407    }
408
409    #[test]
410    fn file_system_save_invalid_name() {
411        let mut fs = FileSystem::default();
412        let old = fs.save("", Rc::default());
413        assert_eq!(old, Err(Errno::ENOENT));
414    }
415
416    #[test]
417    fn file_system_get_parents() {
418        let mut fs = FileSystem::default();
419        let file = Rc::new(RefCell::new(Inode::new([123])));
420        _ = fs.save("/dir/dir1/file", Rc::clone(&file));
421        _ = fs.save("/dir/dir2/dir3/file", Rc::default());
422        assert_eq!(fs.get("/dir/dir2/dir3/../../dir1/file").unwrap(), file);
423        assert_eq!(fs.get("/../dir/dir1/file").unwrap(), file);
424    }
425
426    #[test]
427    fn file_system_get_non_existent_file() {
428        let fs = FileSystem::default();
429        let result = fs.get("/no_such_file");
430        assert_eq!(result, Err(Errno::ENOENT));
431        let result = fs.get("/no_such_directory/foo");
432        assert_eq!(result, Err(Errno::ENOENT));
433    }
434
435    #[test]
436    fn file_system_get_not_directory() {
437        let mut fs = FileSystem::default();
438        let _ = fs.save("/file", Rc::default());
439        let result = fs.get("/file/");
440        assert_eq!(result, Err(Errno::ENOTDIR));
441        let result = fs.get("/file/foo");
442        assert_eq!(result, Err(Errno::ENOTDIR));
443    }
444
445    #[test]
446    fn file_system_get_no_search_permission() {
447        let mut fs = FileSystem::default();
448        let _ = fs.save("/dir/file", Rc::default());
449        {
450            let dir = fs.get("/dir").unwrap();
451            dir.borrow_mut().permissions = Mode::from_bits_retain(0o666);
452        }
453        let result = fs.get("/dir/file");
454        assert_eq!(result, Err(Errno::EACCES));
455    }
456
457    #[test]
458    fn empty_virtual_dir() {
459        let mut dir = VirtualDir::new(std::iter::empty());
460        assert_matches!(dir.next(), Ok(None));
461    }
462
463    #[test]
464    fn non_empty_virtual_dir() {
465        let iter = ["foo", "bar"]
466            .into_iter()
467            .map(|s| Rc::from(UnixStr::new(s)));
468        let mut dir = VirtualDir::new(iter);
469        assert_matches!(dir.next(), Ok(Some(entry)) => {
470            assert_eq!(entry.name, "foo");
471        });
472        assert_matches!(dir.next(), Ok(Some(entry)) => {
473            assert_eq!(entry.name, "bar");
474        });
475        assert_matches!(dir.next(), Ok(None));
476    }
477
478    #[test]
479    fn virtual_dir_try_from_file_body_directory() {
480        let files = ["one", "2", "three"]
481            .into_iter()
482            .map(|name| (Rc::from(UnixStr::new(name)), Rc::default()))
483            .collect();
484        let file = FileBody::Directory { files };
485        let mut dir = VirtualDir::try_from(&file).unwrap();
486
487        let mut files = Vec::new();
488        while let Some(entry) = dir.next().unwrap() {
489            files.push(entry.name.to_str().unwrap().to_string());
490        }
491        files.sort_unstable();
492        let files: Vec<&str> = files.iter().map(String::as_str).collect();
493        assert_eq!(files, [".", "..", "2", "one", "three"]);
494    }
495
496    #[test]
497    fn virtual_dir_try_from_file_body_non_directory() {
498        let file = FileBody::Regular {
499            content: Default::default(),
500            is_native_executable: false,
501        };
502        let result = VirtualDir::try_from(&file);
503        assert_eq!(result.unwrap_err(), Errno::ENOTDIR);
504    }
505}