//! File access for the engine. Natively it is the disk. In the browser (wasm32) there is no //! file system to call synchronously, so files live in memory here: the web host loads them //! (from the browser's private storage, a file picker or a drop) before a command reads them, //! and takes what commands wrote ([`take_writes`]) to store it or hand it out as a download. use std::io::{Error, ErrorKind, Result}; #[cfg(not(target_arch = "wasm32"))] mod imp { use super::*; pub fn read(path: &str) -> Result> { std::fs::read(path) } pub fn write(path: &str, bytes: &[u8]) -> Result<()> { std::fs::write(path, bytes) } pub fn len(path: &str) -> Result { std::fs::metadata(path).map(|m| m.len()) } pub fn exists(path: &str) -> bool { std::path::Path::new(path).exists() } pub fn remove(path: &str) -> Result<()> { std::fs::remove_file(path) } } #[cfg(target_arch = "wasm32")] mod imp { use std::cell::RefCell; use std::collections::BTreeMap; use super::*; thread_local! { static FILES: RefCell>> = const { RefCell::new(BTreeMap::new()) }; static WRITES: RefCell> = const { RefCell::new(Vec::new()) }; } fn missing(path: &str) -> Error { Error::new(ErrorKind::NotFound, format!("{path}: no such file in this browser")) } pub fn read(path: &str) -> Result> { FILES.with(|f| f.borrow().get(path).cloned()).ok_or_else(|| missing(path)) } pub fn write(path: &str, bytes: &[u8]) -> Result<()> { FILES.with(|f| f.borrow_mut().insert(path.to_string(), bytes.to_vec())); WRITES.with(|w| { let mut w = w.borrow_mut(); w.retain(|p| p != path); w.push(path.to_string()); }); Ok(()) } pub fn len(path: &str) -> Result { FILES.with(|f| f.borrow().get(path).map(|b| b.len() as u64)).ok_or_else(|| missing(path)) } pub fn exists(path: &str) -> bool { FILES.with(|f| f.borrow().contains_key(path)) } pub fn remove(path: &str) -> Result<()> { FILES.with(|f| f.borrow_mut().remove(path)).map(|_| ()).ok_or_else(|| missing(path)) } pub fn insert(path: &str, bytes: Vec) { FILES.with(|f| f.borrow_mut().insert(path.to_string(), bytes)); } pub fn take_writes() -> Vec<(String, Vec)> { let paths = WRITES.with(|w| std::mem::take(&mut *w.borrow_mut())); paths.into_iter().filter_map(|p| FILES.with(|f| f.borrow().get(&p).cloned()).map(|b| (p, b))).collect() } pub fn list(prefix: &str) -> Vec<(String, u64)> { FILES.with(|f| f.borrow().iter().filter(|(p, _)| p.starts_with(prefix)).map(|(p, b)| (p.clone(), b.len() as u64)).collect()) } } pub use imp::*; /// A file's text. pub fn read_to_string(path: &str) -> Result { String::from_utf8(read(path)?).map_err(|e| Error::new(ErrorKind::InvalidData, e)) } #[cfg(test)] mod tests { #[test] fn files_round_trip() { let dir = std::env::temp_dir().join(format!("solvecraft-vfs-{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let p = dir.join("a.txt").to_string_lossy().to_string(); assert!(!super::exists(&p)); assert!(super::read(&p).is_err()); super::write(&p, b"hello").unwrap(); assert!(super::exists(&p)); assert_eq!(super::len(&p).unwrap(), 5); assert_eq!(super::read_to_string(&p).unwrap(), "hello"); super::remove(&p).unwrap(); assert!(!super::exists(&p)); let _ = std::fs::remove_dir_all(&dir); } }