logix_vfs/
rel_fs.rs

1use std::{
2    fs::File,
3    path::{Path, PathBuf},
4};
5
6use crate::{utils::PathUtil, Error, LogixVfs};
7
8#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
9pub struct RelFs {
10    root: PathBuf,
11    cur_dir: PathBuf,
12}
13
14impl RelFs {
15    pub fn new(root: impl Into<PathBuf>) -> Self {
16        Self {
17            root: root.into(),
18            cur_dir: PathBuf::new(),
19        }
20    }
21
22    pub fn chdir(&mut self, path: impl AsRef<Path>) -> Result<&Path, Error> {
23        self.cur_dir = self.resolve_path(true, path)?;
24        Ok(&self.cur_dir)
25    }
26
27    fn resolve_path(&self, relative: bool, path: impl AsRef<Path>) -> Result<PathBuf, Error> {
28        PathUtil {
29            root: &self.root,
30            cur_dir: &self.cur_dir,
31        }
32        .resolve_path(relative, path.as_ref())
33    }
34}
35
36#[derive(Debug)]
37pub struct ReadDir {
38    path: PathBuf,
39    prefix: PathBuf,
40    it: std::fs::ReadDir,
41}
42
43impl Iterator for ReadDir {
44    type Item = Result<PathBuf, Error>;
45
46    fn next(&mut self) -> Option<Self::Item> {
47        Some(match self.it.next()? {
48            Ok(entry) => {
49                let full_path = entry.path();
50                full_path
51                    .strip_prefix(&self.prefix)
52                    .map_err(|e| {
53                        // NOTE(2024.02): This should not happen, at least I don't know how to trigger it
54                        Error::Other(format!(
55                            "Failed to strip prefix {:?} off {full_path:?}: {e}",
56                            self.prefix
57                        ))
58                    })
59                    .map(|p| p.to_path_buf())
60            }
61            Err(e) => Err(Error::from_io(self.path.clone(), e)),
62        })
63    }
64}
65
66impl LogixVfs for RelFs {
67    type RoFile = File;
68    type DirEntry = PathBuf;
69    type ReadDir = ReadDir;
70
71    fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, Error> {
72        self.resolve_path(true, path)
73    }
74
75    fn open_file(&self, path: &Path) -> Result<Self::RoFile, Error> {
76        let full_path = self.resolve_path(false, path)?;
77        File::open(full_path).map_err(|e| Error::from_io(path.to_path_buf(), e))
78    }
79
80    fn read_dir(&self, path: &Path) -> Result<Self::ReadDir, Error> {
81        let full_path = self.resolve_path(false, path)?;
82        let it = full_path
83            .read_dir()
84            .map_err(|e| Error::from_io(path.to_path_buf(), e))?;
85        Ok(ReadDir {
86            path: path.to_path_buf(),
87            prefix: full_path,
88            it,
89        })
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    type TestCase<'a> = &'a [(Option<(&'a str, &'a str)>, &'a [&'a str], &'a str, &'a str)];
98
99    static PATHS_TO_TEST: TestCase = &[
100        (
101            None,
102            &[
103                ".config/awesome-app/config.toml",
104                ".config/./awesome-app/./config.toml",
105            ],
106            "/home/zeldor/.config/awesome-app/config.toml",
107            ".config/awesome-app/config.toml",
108        ),
109        (
110            None,
111            &[".config/./awesome-app/../config.toml"],
112            "/home/zeldor/.config/config.toml",
113            ".config/config.toml",
114        ),
115        (
116            Some((".config", ".config")),
117            &["awesome-app"],
118            "/home/zeldor/.config/awesome-app",
119            ".config/awesome-app",
120        ),
121        (
122            None,
123            &["../awesome-app"],
124            "/home/zeldor/awesome-app",
125            "awesome-app",
126        ),
127        (
128            Some(("awesome-app", ".config/awesome-app")),
129            &[
130                "config.toml",
131                "./config.toml",
132                "/.config/awesome-app/config.toml",
133            ],
134            "/home/zeldor/.config/awesome-app/config.toml",
135            ".config/awesome-app/config.toml",
136        ),
137        (
138            None,
139            &["../config.toml"],
140            "/home/zeldor/.config/config.toml",
141            ".config/config.toml",
142        ),
143        (None, &["/.bashrc"], "/home/zeldor/.bashrc", ".bashrc"),
144    ];
145
146    #[test]
147    fn basics() {
148        let mut fs = RelFs::new("/home/zeldor");
149
150        for &(chdir, paths, if_full, if_relative) in PATHS_TO_TEST {
151            if let Some((chdir, rel_after)) = chdir {
152                assert_eq!(fs.chdir(chdir), Ok(Path::new(rel_after)), "{chdir:?}");
153            }
154            for path in paths {
155                assert_eq!(
156                    fs.resolve_path(false, path),
157                    Ok(PathBuf::from(if_full)),
158                    "{path:?}"
159                );
160                assert_eq!(
161                    fs.resolve_path(true, path),
162                    Ok(PathBuf::from(if_relative)),
163                    "{path:?}"
164                );
165            }
166        }
167    }
168
169    #[test]
170    fn errors() {
171        let mut fs = RelFs::new("src");
172
173        assert_eq!(
174            fs.canonicalize_path("../test".as_ref()),
175            Err(Error::PathOutsideBounds {
176                path: "../test".into()
177            })
178        );
179
180        assert_eq!(
181            fs.canonicalize_path("test/../test/../../test".as_ref()),
182            Err(Error::PathOutsideBounds {
183                path: "test/../test/../../test".into()
184            })
185        );
186
187        assert_eq!(fs.open_file("lib.rs".as_ref()).err(), None);
188        assert_eq!(
189            fs.open_file("not-lib.rs".as_ref()).err(),
190            Some(Error::NotFound {
191                path: "not-lib.rs".into()
192            })
193        );
194        assert_eq!(
195            fs.open_file("../outside.txt".as_ref()).err(),
196            Some(Error::PathOutsideBounds {
197                path: "../outside.txt".into()
198            })
199        );
200
201        assert_eq!(
202            fs.chdir("../outside").err(),
203            Some(Error::PathOutsideBounds {
204                path: "../outside".into()
205            })
206        );
207
208        assert_eq!(
209            fs.read_dir("../outside".as_ref()).err(),
210            Some(Error::PathOutsideBounds {
211                path: "../outside".into()
212            })
213        );
214
215        assert_eq!(
216            fs.read_dir("lib.rs".as_ref()).err(),
217            Some(Error::NotADirectory {
218                path: "lib.rs".into()
219            })
220        );
221        assert_eq!(
222            fs.read_dir("not-lib.rs".as_ref()).err(),
223            Some(Error::NotFound {
224                path: "not-lib.rs".into()
225            })
226        );
227    }
228}