Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / io / memory.rs
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}