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 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}