Fork of daniellemaywood.uk/gleam — Wasm codegen work
16 kB
543 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4use super::*;
5use std::ops::Deref;
6use std::{
7 cell::RefCell,
8 collections::{HashMap, HashSet},
9 rc::Rc,
10 time::Duration,
11};
12
13use camino::{Utf8Path, Utf8PathBuf};
14
15/// An in memory sharable collection of pretend files that can be used in place
16/// of a real file system. It is a shared reference to a set of buffer than can
17/// be cheaply cloned, all resulting copies pointing to the same internal
18/// buffers.
19///
20/// Useful in tests and in environments like the browser where there is no file
21/// system.
22///
23/// Not thread safe. The compiler is single threaded, so that's OK.
24///
25/// Only supports absolute paths. The root directory ("/") is always guaranteed
26/// to exist.
27///
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct InMemoryFileSystem {
30 files: Rc<RefCell<HashMap<Utf8PathBuf, InMemoryFile>>>,
31}
32
33impl Default for InMemoryFileSystem {
34 fn default() -> Self {
35 let mut files = HashMap::new();
36
37 // Ensure root directory always exists.
38 let _ = files.insert(Utf8PathBuf::from("/"), InMemoryFile::directory());
39
40 Self {
41 files: Rc::new(RefCell::new(files)),
42 }
43 }
44}
45
46impl InMemoryFileSystem {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn reset(&self) {
52 self.files.deref().borrow_mut().clear();
53 }
54
55 /// Returns the contents of each file, excluding directories.
56 ///
57 /// # Panics
58 ///
59 /// Panics if this is not the only reference to the underlying files.
60 ///
61 pub fn into_contents(self) -> HashMap<Utf8PathBuf, Content> {
62 Rc::try_unwrap(self.files)
63 .expect("InMemoryFileSystem::into_files called on a clone")
64 .into_inner()
65 .into_iter()
66 .filter_map(|(path, file)| file.into_content().map(|content| (path, content)))
67 .collect()
68 }
69
70 /// All files currently in the filesystem (directories are not included).
71 pub fn files(&self) -> Vec<Utf8PathBuf> {
72 self.files
73 .borrow()
74 .iter()
75 .filter(|(_, f)| !f.is_directory())
76 .map(|(path, _)| path)
77 .cloned()
78 .collect()
79 }
80
81 #[cfg(test)]
82 /// Set the modification time of a file.
83 ///
84 /// Panics if the file does not exist.
85 ///
86 pub fn set_modification_time(&self, path: &Utf8Path, time: SystemTime) {
87 self.files
88 .deref()
89 .borrow_mut()
90 .get_mut(path)
91 .unwrap()
92 .modification_time = time;
93 }
94
95 pub fn try_set_modification_time(
96 &self,
97 path: &Utf8Path,
98 time: SystemTime,
99 ) -> Result<(), Error> {
100 self.files
101 .deref()
102 .borrow_mut()
103 .get_mut(path)
104 .ok_or_else(|| Error::FileIo {
105 kind: FileKind::File,
106 action: FileIoAction::Open,
107 path: path.to_path_buf(),
108 err: None,
109 })?
110 .modification_time = time;
111 Ok(())
112 }
113}
114
115impl FileSystemWriter for InMemoryFileSystem {
116 fn delete_directory(&self, path: &Utf8Path) -> Result<(), Error> {
117 let mut files = self.files.deref().borrow_mut();
118
119 if files.get(path).is_some_and(|f| !f.is_directory()) {
120 return Err(Error::FileIo {
121 kind: FileKind::Directory,
122 action: FileIoAction::Delete,
123 path: path.to_path_buf(),
124 err: None,
125 });
126 }
127
128 let root = Utf8Path::new("/");
129 if path != root {
130 // Ensure the root path always exists.
131 // Deleting other files is fine.
132 let _ = files.remove(path);
133 }
134
135 // Remove any files in the directory
136 while let Some(file) = files
137 .keys()
138 .find(|file| file.as_path() != root && file.starts_with(path))
139 {
140 let file = file.clone();
141 let _ = files.remove(&file);
142 }
143
144 Ok(())
145 }
146
147 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> {
148 self.write_bytes(to, &self.read_bytes(from)?)
149 }
150
151 fn copy_dir(&self, _: &Utf8Path, _: &Utf8Path) -> Result<(), Error> {
152 panic!("unimplemented") // TODO
153 }
154
155 fn mkdir(&self, path: &Utf8Path) -> Result<(), Error> {
156 // Traverse ancestors from parent to root.
157 // Create each missing ancestor.
158 for ancestor in path.ancestors() {
159 if ancestor == "" {
160 // Ignore the final ancestor of a relative path.
161 continue;
162 }
163 // Ensure we don't overwrite an existing file.
164 // We can ignore existing directories though.
165 let mut files = self.files.deref().borrow_mut();
166 if files.get(ancestor).is_some_and(|f| !f.is_directory()) {
167 return Err(Error::FileIo {
168 kind: FileKind::Directory,
169 action: FileIoAction::Create,
170 path: ancestor.to_path_buf(),
171 err: None,
172 });
173 }
174 let dir = InMemoryFile::directory();
175 _ = files.insert(ancestor.to_path_buf(), dir);
176 }
177
178 Ok(())
179 }
180
181 fn hardlink(&self, left: &Utf8Path, right: &Utf8Path) -> Result<(), Error> {
182 let mut files = self.files.deref().borrow_mut();
183 let Some(file) = files.get(left).cloned() else {
184 return Err(Error::FileIo {
185 kind: FileKind::File,
186 action: FileIoAction::ReadMetadata,
187 path: left.to_path_buf(),
188 err: None,
189 });
190 };
191 let _ = files.insert(right.into(), file);
192 Ok(())
193 }
194
195 fn symlink_dir(&self, _: &Utf8Path, _: &Utf8Path) -> Result<(), Error> {
196 panic!("unimplemented") // TODO
197 }
198
199 fn delete_file(&self, path: &Utf8Path) -> Result<(), Error> {
200 let mut files = self.files.deref().borrow_mut();
201 if files.get(path).is_some_and(|f| f.is_directory()) {
202 return Err(Error::FileIo {
203 kind: FileKind::File,
204 action: FileIoAction::Delete,
205 path: path.to_path_buf(),
206 err: None,
207 });
208 }
209 let _ = files.remove(path);
210 Ok(())
211 }
212
213 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error> {
214 self.write_bytes(path, content.as_bytes())
215 }
216
217 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error> {
218 // Ensure directories exist
219 if let Some(parent) = path.parent() {
220 self.mkdir(parent)?;
221 }
222
223 let mut file = InMemoryFile::default();
224 _ = io::Write::write(&mut file, content).expect("channel buffer write");
225 _ = self
226 .files
227 .deref()
228 .borrow_mut()
229 .insert(path.to_path_buf(), file);
230 Ok(())
231 }
232
233 fn exists(&self, path: &Utf8Path) -> bool {
234 self.files.deref().borrow().contains_key(path)
235 }
236}
237
238impl FileSystemReader for InMemoryFileSystem {
239 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, Error> {
240 Ok(path.to_path_buf())
241 }
242
243 fn read(&self, path: &Utf8Path) -> Result<String, Error> {
244 let path = path.to_path_buf();
245 let files = self.files.deref().borrow();
246 let buffer = files
247 .get(&path)
248 .and_then(|file| file.node.as_file_buffer())
249 .ok_or_else(|| Error::FileIo {
250 kind: FileKind::File,
251 action: FileIoAction::Open,
252 path: path.clone(),
253 err: None,
254 })?;
255 let bytes = buffer.borrow();
256 let unicode = String::from_utf8(bytes.clone()).map_err(|err| Error::FileIo {
257 kind: FileKind::File,
258 action: FileIoAction::Read,
259 path: path.clone(),
260 err: Some(err.to_string()),
261 })?;
262 Ok(unicode)
263 }
264
265 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>, Error> {
266 let path = path.to_path_buf();
267 let files = self.files.deref().borrow();
268 let buffer = files
269 .get(&path)
270 .and_then(|file| file.node.as_file_buffer())
271 .ok_or_else(|| Error::FileIo {
272 kind: FileKind::File,
273 action: FileIoAction::Open,
274 path: path.clone(),
275 err: None,
276 })?;
277 let bytes = buffer.borrow().clone();
278 Ok(bytes)
279 }
280
281 fn is_file(&self, path: &Utf8Path) -> bool {
282 self.files
283 .deref()
284 .borrow()
285 .get(path)
286 .is_some_and(|file| !file.is_directory())
287 }
288
289 fn is_directory(&self, path: &Utf8Path) -> bool {
290 self.files
291 .deref()
292 .borrow()
293 .get(path)
294 .is_some_and(|file| file.is_directory())
295 }
296
297 fn reader(&self, _path: &Utf8Path) -> Result<WrappedReader, Error> {
298 unreachable!("Memory reader unimplemented")
299 }
300
301 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir> {
302 let read_dir = ReadDir::from_iter(
303 self.files
304 .deref()
305 .borrow()
306 .keys()
307 .map(|file_path| file_path.to_path_buf())
308 .filter(|file_path| file_path.parent().is_some_and(|parent| path == parent))
309 .map(DirEntry::from_pathbuf)
310 .map(Ok),
311 );
312
313 Ok(read_dir)
314 }
315
316 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime, Error> {
317 let files = self.files.deref().borrow();
318 let file = files.get(path).ok_or_else(|| Error::FileIo {
319 kind: FileKind::File,
320 action: FileIoAction::ReadMetadata,
321 path: path.to_path_buf(),
322 err: None,
323 })?;
324 Ok(file.modification_time)
325 }
326
327 fn is_same_file(&self, _left: &Utf8Path, _right: &Utf8Path) -> Result<bool, Error> {
328 unreachable!("is_same_file unimplemented")
329 }
330}
331
332/// The representation of a file or directory in the in-memory filesystem.
333///
334/// Stores a file's buffer of contents.
335///
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum InMemoryFileNode {
338 File(Rc<RefCell<Vec<u8>>>),
339 Directory,
340}
341
342impl InMemoryFileNode {
343 /// Returns this file's file buffer if this isn't a directory.
344 fn as_file_buffer(&self) -> Option<&Rc<RefCell<Vec<u8>>>> {
345 match self {
346 Self::File(buffer) => Some(buffer),
347 Self::Directory => None,
348 }
349 }
350
351 /// Returns this file's file buffer if this isn't a directory.
352 fn into_file_buffer(self) -> Option<Rc<RefCell<Vec<u8>>>> {
353 match self {
354 Self::File(buffer) => Some(buffer),
355 Self::Directory => None,
356 }
357 }
358}
359
360/// An in memory sharable that can be used in place of a real file. It is a
361/// shared reference to a buffer than can be cheaply cloned, all resulting copies
362/// pointing to the same internal buffer.
363///
364/// Useful in tests and in environments like the browser where there is no file
365/// system.
366///
367/// This struct holds common properties of different types of filesystem nodes
368/// (files and directories). The `node` field contains the file's content
369/// buffer, if this is not a directory.
370///
371/// Not thread safe. The compiler is single threaded, so that's OK.
372///
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct InMemoryFile {
375 node: InMemoryFileNode,
376 modification_time: SystemTime,
377}
378
379impl InMemoryFile {
380 /// Creates a directory.
381 pub fn directory() -> Self {
382 Self {
383 node: InMemoryFileNode::Directory,
384 ..Self::default()
385 }
386 }
387
388 /// Checks whether this is a directory's entry.
389 pub fn is_directory(&self) -> bool {
390 matches!(self.node, InMemoryFileNode::Directory)
391 }
392
393 /// Returns this file's contents if this is not a directory.
394 ///
395 /// # Panics
396 ///
397 /// Panics if this is not the only reference to the underlying files.
398 ///
399 pub fn into_content(self) -> Option<Content> {
400 let buffer = self.node.into_file_buffer()?;
401 let contents = Rc::try_unwrap(buffer)
402 .expect("InMemoryFile::into_content called with multiple references")
403 .into_inner();
404
405 // All null bytes are usually from when a binary file is empty, and
406 // aren't particularly useful as text, so we treat them as binary.
407 if contents.iter().all(|byte| *byte == 0) {
408 return Some(Content::Binary(contents));
409 }
410
411 match String::from_utf8(contents) {
412 Ok(s) => Some(Content::Text(s)),
413 Err(e) => Some(Content::Binary(e.into_bytes())),
414 }
415 }
416}
417
418impl Default for InMemoryFile {
419 fn default() -> Self {
420 Self {
421 node: InMemoryFileNode::File(Rc::new(RefCell::new(vec![]))),
422 // We use a fixed time here so that the tests are deterministic. In
423 // future we may want to inject this in some fashion.
424 modification_time: SystemTime::UNIX_EPOCH + Duration::from_secs(663112800),
425 }
426 }
427}
428
429impl io::Write for InMemoryFile {
430 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
431 let Some(buffer) = self.node.as_file_buffer() else {
432 // Not a file
433 return Err(io::Error::from(io::ErrorKind::NotFound));
434 };
435 let mut reference = (*buffer).borrow_mut();
436 reference.write(buf)
437 }
438
439 fn flush(&mut self) -> io::Result<()> {
440 let Some(buffer) = self.node.as_file_buffer() else {
441 // Not a file
442 return Err(io::Error::from(io::ErrorKind::NotFound));
443 };
444 let mut reference = (*buffer).borrow_mut();
445 reference.flush()
446 }
447}
448
449impl CommandExecutor for InMemoryFileSystem {
450 fn exec(&self, _command: Command) -> Result<i32, Error> {
451 Ok(0) // Always succeed.
452 }
453}
454
455impl BeamCompilerIO for InMemoryFileSystem {
456 fn compile_beam(
457 &self,
458 _out: &Utf8Path,
459 _lib: &Utf8Path,
460 _modules: &HashSet<Utf8PathBuf>,
461 _stdio: Stdio,
462 ) -> Result<Vec<String>, Error> {
463 Ok(Vec::new()) // Always succeed.
464 }
465}
466
467#[test]
468fn test_empty_in_memory_fs_has_root() {
469 let imfs = InMemoryFileSystem::new();
470
471 assert!(imfs.exists(Utf8Path::new("/")));
472}
473
474#[test]
475fn test_cannot_remove_root_from_in_memory_fs() -> Result<(), Error> {
476 let imfs = InMemoryFileSystem::new();
477 imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?;
478 imfs.delete_directory(Utf8Path::new("/"))?;
479
480 assert!(!imfs.exists(Utf8Path::new("/a/b/c.txt")));
481 assert!(imfs.exists(Utf8Path::new("/")));
482
483 Ok(())
484}
485
486#[test]
487fn test_files() -> Result<(), Error> {
488 let imfs = InMemoryFileSystem::new();
489 imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?;
490 imfs.write(&Utf8PathBuf::from("/d/e.txt"), "a")?;
491
492 let mut files = imfs.files();
493
494 // Sort for test determinism due to hash map usage.
495 files.sort_unstable();
496
497 assert_eq!(
498 vec![
499 Utf8PathBuf::from("/a/b/c.txt"),
500 Utf8PathBuf::from("/d/e.txt"),
501 ],
502 files
503 );
504
505 Ok(())
506}
507
508#[test]
509fn test_in_memory_dir_walking() -> Result<(), Error> {
510 use itertools::Itertools;
511 let imfs = InMemoryFileSystem::new();
512 imfs.write(&Utf8PathBuf::from("/a/b/a.txt"), "a")?;
513 imfs.write(&Utf8PathBuf::from("/a/b/b.txt"), "a")?;
514 imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?;
515 imfs.write(&Utf8PathBuf::from("/b/d.txt"), "a")?;
516 imfs.write(&Utf8PathBuf::from("/a/c/e.txt"), "a")?;
517 imfs.write(&Utf8PathBuf::from("/a/c/d/f.txt"), "a")?;
518 imfs.write(&Utf8PathBuf::from("/a/g.txt"), "a")?;
519 imfs.write(&Utf8PathBuf::from("/h.txt"), "a")?;
520 imfs.mkdir(&Utf8PathBuf::from("/a/e"))?;
521
522 let mut walked_entries: Vec<String> = DirWalker::new(Utf8PathBuf::from("/a/"))
523 .into_file_iter(&imfs)
524 .map_ok(Utf8PathBuf::into_string)
525 .try_collect()?;
526
527 // Keep test deterministic due to hash map usage
528 walked_entries.sort_unstable();
529
530 assert_eq!(
531 vec![
532 "/a/b/a.txt".to_owned(),
533 "/a/b/b.txt".to_owned(),
534 "/a/b/c.txt".to_owned(),
535 "/a/c/d/f.txt".to_owned(),
536 "/a/c/e.txt".to_owned(),
537 "/a/g.txt".to_owned(),
538 ],
539 walked_entries,
540 );
541
542 Ok(())
543}