1use std::collections::BTreeMap;
18use std::path::{Path, PathBuf};
19
20pub trait ReadFs {
26 type Error: std::error::Error;
27
28 fn read_to_string(&self, path: &Path) -> Result<String, Self::Error>;
29 fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, Self::Error>;
30 fn exists(&self, path: &Path) -> bool;
31 fn is_dir(&self, path: &Path) -> bool;
32 fn is_file(&self, path: &Path) -> bool;
33}
34
35#[derive(Debug, Clone, Copy)]
41pub struct HostFs;
42
43impl ReadFs for HostFs {
44 type Error = std::io::Error;
45
46 fn read_to_string(&self, path: &Path) -> Result<String, Self::Error> {
47 std::fs::read_to_string(path)
48 }
49
50 fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, Self::Error> {
51 std::fs::read(path)
52 }
53
54 fn exists(&self, path: &Path) -> bool {
55 path.exists()
56 }
57
58 fn is_dir(&self, path: &Path) -> bool {
59 path.is_dir()
60 }
61
62 fn is_file(&self, path: &Path) -> bool {
63 path.is_file()
64 }
65}
66
67#[derive(Debug, Clone, Default)]
77pub struct MemFs {
78 files: BTreeMap<PathBuf, Vec<u8>>,
79}
80
81#[derive(Debug)]
83pub struct MemFsError {
84 kind: MemFsErrorKind,
85 path: PathBuf,
86}
87
88#[derive(Debug)]
89enum MemFsErrorKind {
90 NotFound,
91 InvalidUtf8,
92}
93
94impl std::fmt::Display for MemFsError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self.kind {
97 MemFsErrorKind::NotFound => write!(f, "not found: {}", self.path.display()),
98 MemFsErrorKind::InvalidUtf8 => {
99 write!(f, "invalid UTF-8 in: {}", self.path.display())
100 }
101 }
102 }
103}
104
105impl std::error::Error for MemFsError {}
106
107impl MemFs {
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn add_file(&mut self, path: impl Into<PathBuf>, contents: impl Into<String>) {
114 self.files.insert(path.into(), contents.into().into_bytes());
115 }
116
117 pub fn add_bytes(&mut self, path: impl Into<PathBuf>, bytes: impl Into<Vec<u8>>) {
119 self.files.insert(path.into(), bytes.into());
120 }
121
122 pub fn file_paths(&self) -> impl Iterator<Item = &Path> {
124 self.files.keys().map(PathBuf::as_path)
125 }
126
127 pub fn file_size(&self, path: &Path) -> Result<u64, MemFsError> {
129 self.files
130 .get(path)
131 .map(|bytes| bytes.len() as u64)
132 .ok_or_else(|| self.not_found(path))
133 }
134
135 fn not_found(&self, path: &Path) -> MemFsError {
136 MemFsError {
137 kind: MemFsErrorKind::NotFound,
138 path: path.to_path_buf(),
139 }
140 }
141}
142
143impl ReadFs for MemFs {
144 type Error = MemFsError;
145
146 fn read_to_string(&self, path: &Path) -> Result<String, Self::Error> {
147 let bytes = self.files.get(path).ok_or_else(|| self.not_found(path))?;
148 String::from_utf8(bytes.clone()).map_err(|_| MemFsError {
149 kind: MemFsErrorKind::InvalidUtf8,
150 path: path.to_path_buf(),
151 })
152 }
153
154 fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, Self::Error> {
155 self.files
156 .get(path)
157 .cloned()
158 .ok_or_else(|| self.not_found(path))
159 }
160
161 fn exists(&self, path: &Path) -> bool {
162 self.is_file(path) || self.is_dir(path)
163 }
164
165 fn is_dir(&self, path: &Path) -> bool {
166 self.files.keys().any(|k| k.starts_with(path) && k != path)
168 }
169
170 fn is_file(&self, path: &Path) -> bool {
171 self.files.contains_key(path)
172 }
173}
174
175#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
186 fn host_fs_read_to_string() {
187 let dir = tempfile::tempdir().unwrap();
188 let file = dir.path().join("hello.txt");
189 std::fs::write(&file, "hello world").unwrap();
190
191 let fs = HostFs;
192 assert_eq!(fs.read_to_string(&file).unwrap(), "hello world");
193 }
194
195 #[test]
196 fn host_fs_read_bytes() {
197 let dir = tempfile::tempdir().unwrap();
198 let file = dir.path().join("data.bin");
199 std::fs::write(&file, b"\x00\x01\x02").unwrap();
200
201 let fs = HostFs;
202 assert_eq!(fs.read_bytes(&file).unwrap(), vec![0, 1, 2]);
203 }
204
205 #[test]
206 fn host_fs_exists() {
207 let dir = tempfile::tempdir().unwrap();
208 let file = dir.path().join("exists.txt");
209 std::fs::write(&file, "").unwrap();
210
211 let fs = HostFs;
212 assert!(fs.exists(&file));
213 assert!(!fs.exists(&dir.path().join("nope.txt")));
214 }
215
216 #[test]
217 fn host_fs_is_dir() {
218 let dir = tempfile::tempdir().unwrap();
219 let fs = HostFs;
220 assert!(fs.is_dir(dir.path()));
221 assert!(!fs.is_dir(&dir.path().join("nope")));
222 }
223
224 #[test]
225 fn host_fs_is_file() {
226 let dir = tempfile::tempdir().unwrap();
227 let file = dir.path().join("f.txt");
228 std::fs::write(&file, "x").unwrap();
229
230 let fs = HostFs;
231 assert!(fs.is_file(&file));
232 assert!(!fs.is_file(dir.path()));
233 }
234
235 #[test]
236 fn host_fs_read_missing_file_errors() {
237 let fs = HostFs;
238 let result = fs.read_to_string(Path::new("/definitely/not/here.txt"));
239 assert!(result.is_err());
240 }
241
242 #[test]
245 fn mem_fs_read_to_string() {
246 let mut fs = MemFs::new();
247 fs.add_file(PathBuf::from("a.txt"), "contents");
248 assert_eq!(fs.read_to_string(Path::new("a.txt")).unwrap(), "contents");
249 }
250
251 #[test]
252 fn mem_fs_read_bytes() {
253 let mut fs = MemFs::new();
254 fs.add_bytes(PathBuf::from("b.bin"), vec![0xDE, 0xAD]);
255 assert_eq!(fs.read_bytes(Path::new("b.bin")).unwrap(), vec![0xDE, 0xAD]);
256 }
257
258 #[test]
259 fn mem_fs_not_found() {
260 let fs = MemFs::new();
261 let err = fs.read_to_string(Path::new("missing.txt")).unwrap_err();
262 assert!(err.to_string().contains("not found"));
263 }
264
265 #[test]
266 fn mem_fs_invalid_utf8() {
267 let mut fs = MemFs::new();
268 fs.add_bytes(PathBuf::from("bad.txt"), vec![0xFF, 0xFE]);
269 let err = fs.read_to_string(Path::new("bad.txt")).unwrap_err();
270 assert!(err.to_string().contains("invalid UTF-8"));
271 }
272
273 #[test]
274 fn mem_fs_exists() {
275 let mut fs = MemFs::new();
276 fs.add_file(PathBuf::from("src/lib.rs"), "fn main() {}");
277 assert!(fs.exists(Path::new("src/lib.rs")));
278 assert!(fs.exists(Path::new("src"))); assert!(!fs.exists(Path::new("nope")));
280 }
281
282 #[test]
283 fn mem_fs_is_dir() {
284 let mut fs = MemFs::new();
285 fs.add_file(PathBuf::from("src/lib.rs"), "");
286 assert!(fs.is_dir(Path::new("src")));
287 assert!(!fs.is_dir(Path::new("src/lib.rs"))); assert!(!fs.is_dir(Path::new("other")));
289 }
290
291 #[test]
292 fn mem_fs_is_file() {
293 let mut fs = MemFs::new();
294 fs.add_file(PathBuf::from("src/lib.rs"), "");
295 assert!(fs.is_file(Path::new("src/lib.rs")));
296 assert!(!fs.is_file(Path::new("src")));
297 }
298
299 #[test]
300 fn mem_fs_default_is_empty() {
301 let fs = MemFs::default();
302 assert!(!fs.exists(Path::new("anything")));
303 }
304
305 #[test]
306 fn mem_fs_file_paths_are_sorted() {
307 let mut fs = MemFs::new();
308 fs.add_file(PathBuf::from("z/file.txt"), "z");
309 fs.add_file(PathBuf::from("a/file.txt"), "a");
310 fs.add_file(PathBuf::from("m/file.txt"), "m");
311
312 let paths: Vec<_> = fs
313 .file_paths()
314 .map(|path| path.to_string_lossy().into_owned())
315 .collect();
316
317 assert_eq!(paths, vec!["a/file.txt", "m/file.txt", "z/file.txt"]);
318 }
319
320 #[test]
321 fn mem_fs_file_size_reads_inserted_length() {
322 let mut fs = MemFs::new();
323 fs.add_bytes(PathBuf::from("blob.bin"), vec![1, 2, 3, 4, 5]);
324
325 assert_eq!(fs.file_size(Path::new("blob.bin")).unwrap(), 5);
326 }
327
328 #[test]
329 fn mem_fs_file_size_missing_errors() {
330 let fs = MemFs::new();
331 let err = fs.file_size(Path::new("missing.bin")).unwrap_err();
332 assert!(err.to_string().contains("not found"));
333 }
334}