mod buffered;
mod db_lock;
mod mem_env;
mod types;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub mod opfs;
mod std_env;
#[cfg(target_os = "wasi")]
mod wasi;
pub use mem_env::MemEnv;
pub use std_env::StdEnv;
pub use types::{Capabilities, DirEntry, FileMeta, WriteMode};
#[cfg(target_os = "wasi")]
pub use wasi::WasiEnv;
pub(crate) use buffered::BufferedWriter;
use std::io;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
pub trait Env: Send + Sync + std::fmt::Debug {
fn create_dir_all(&self, path: &Path) -> io::Result<()>;
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
fn open_read(&self, path: &Path) -> io::Result<Box<dyn ReadFile>>;
fn open_write(&self, path: &Path, mode: WriteMode) -> io::Result<Box<dyn WriteFile>>;
fn metadata(&self, path: &Path) -> io::Result<FileMeta>;
fn remove_file(&self, path: &Path) -> io::Result<()>;
fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
fn hard_link(&self, src: &Path, dst: &Path) -> io::Result<()>;
fn sync_dir(&self, path: &Path) -> io::Result<()>;
fn lock_file(&self, path: &Path, exclusive: bool) -> io::Result<Box<dyn FileLock>>;
fn capabilities(&self) -> Capabilities;
fn now_micros(&self) -> Option<u64>;
fn unix_secs(&self) -> Option<u64>;
fn spawn(
&self,
name: &str,
body: Box<dyn FnOnce() + Send + 'static>,
) -> io::Result<Box<dyn JoinHandle>>;
fn sleep(&self, dur: Duration);
fn exists(&self, path: &Path) -> bool {
self.metadata(path).is_ok()
}
fn is_dir(&self, path: &Path) -> bool {
self.metadata(path).map(|m| m.is_dir).unwrap_or(false)
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
let file = self.open_read(path)?;
let len = usize::try_from(file.len()?).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{} is too large to address", path.display()),
)
})?;
let mut buf = Vec::new();
buf.try_reserve_exact(len).map_err(|_| {
io::Error::new(
io::ErrorKind::OutOfMemory,
format!("cannot allocate {len} bytes to read {}", path.display()),
)
})?;
buf.resize(len, 0);
file.read_exact_at(0, &mut buf)?;
Ok(buf)
}
fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
let mut file = self.open_write(path, WriteMode::Truncate)?;
file.write_all(data)?;
file.flush()
}
fn drop_page_cache(&self, path: &Path) {
let _ = path;
}
}
pub trait ReadFile: Send + Sync {
fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
fn len(&self) -> io::Result<u64>;
fn is_empty(&self) -> io::Result<bool> {
Ok(self.len()? == 0)
}
}
pub trait WriteFile: Send {
fn write_all(&mut self, buf: &[u8]) -> io::Result<()>;
fn write_all_vectored(&mut self, slices: &[&[u8]]) -> io::Result<()> {
for slice in slices {
self.write_all(slice)?;
}
Ok(())
}
fn flush(&mut self) -> io::Result<()>;
fn sync_all(&mut self) -> io::Result<()>;
fn sync_data(&mut self) -> io::Result<()> {
self.sync_all()
}
fn set_len(&mut self, len: u64) -> io::Result<()>;
fn len(&self) -> io::Result<u64>;
fn is_empty(&self) -> io::Result<bool> {
Ok(self.len()? == 0)
}
}
pub trait FileLock: Send + Sync {}
pub trait JoinHandle: Send {
fn join(self: Box<Self>);
}
#[cfg(not(target_os = "wasi"))]
type DefaultEnv = StdEnv;
#[cfg(target_os = "wasi")]
type DefaultEnv = WasiEnv;
pub fn std_env() -> Arc<dyn Env> {
static ENV: std::sync::OnceLock<Arc<DefaultEnv>> = std::sync::OnceLock::new();
ENV.get_or_init(|| Arc::new(DefaultEnv::new())).clone()
}
pub(crate) fn platform_micros() -> Option<u64> {
platform_nanos().map(|nanos| nanos / 1_000)
}
pub(crate) fn platform_nanos() -> Option<u64> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
None
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
let origin = ORIGIN.get_or_init(std::time::Instant::now);
Some(origin.elapsed().as_nanos() as u64)
}
}
pub(crate) fn elapsed_micros(env: &dyn Env, start: Option<u64>) -> Option<u64> {
let start = start?;
Some(env.now_micros()?.saturating_sub(start))
}
pub(crate) fn sync_parent_dir(env: &dyn Env, path: &Path) -> io::Result<()> {
match path.parent() {
Some(parent) => env.sync_dir(parent),
None => Ok(()),
}
}
pub(crate) fn remove_file_and_sync_parent(env: &dyn Env, path: &Path) -> io::Result<()> {
env.remove_file(path)?;
sync_parent_dir(env, path)
}
pub(crate) struct ReadFileCursor<F> {
file: F,
offset: u64,
end: u64,
}
impl<F: std::ops::Deref<Target = dyn ReadFile>> ReadFileCursor<F> {
pub(crate) fn new(file: F) -> io::Result<Self> {
let end = file.len()?;
Ok(Self {
file,
offset: 0,
end,
})
}
pub(crate) fn len(&self) -> u64 {
self.end
}
}
impl<F: std::ops::Deref<Target = dyn ReadFile>> io::Read for ReadFileCursor<F> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.end.saturating_sub(self.offset);
if remaining == 0 || buf.is_empty() {
return Ok(0);
}
let want = remaining.min(buf.len() as u64) as usize;
self.file.read_exact_at(self.offset, &mut buf[..want])?;
self.offset += want as u64;
Ok(want)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn std_env_is_shared_not_reallocated() {
assert!(Arc::ptr_eq(&std_env(), &std_env()));
}
#[test]
fn platform_micros_is_monotonic_where_it_exists() {
if let (Some(a), Some(b)) = (platform_micros(), platform_micros()) {
assert!(b >= a);
}
}
#[test]
fn sync_parent_dir_accepts_existing_file() {
let dir = tempfile::TempDir::new().unwrap();
let env = std_env();
let path = dir.path().join("file");
env.write(&path, b"data").unwrap();
sync_parent_dir(&*env, &path).unwrap();
}
#[test]
fn remove_file_and_sync_parent_removes_file() {
let dir = tempfile::TempDir::new().unwrap();
let env = std_env();
let path = dir.path().join("file");
env.write(&path, b"data").unwrap();
remove_file_and_sync_parent(&*env, &path).unwrap();
assert!(!env.exists(&path));
}
#[test]
fn read_file_cursor_streams_the_whole_file() {
use std::io::Read;
let dir = tempfile::TempDir::new().unwrap();
let env = std_env();
let path = dir.path().join("streamed");
env.write(&path, b"0123456789").unwrap();
let file = env.open_read(&path).unwrap();
let mut cursor = ReadFileCursor::new(&*file).unwrap();
let mut out = Vec::new();
cursor.read_to_end(&mut out).unwrap();
assert_eq!(out, b"0123456789");
}
#[test]
fn elapsed_micros_is_none_without_a_clock() {
let env = MemEnv::new();
env.set_clocks(None, None);
assert_eq!(elapsed_micros(&env, Some(5)), None);
env.set_clocks(Some(1_000), Some(0));
assert_eq!(elapsed_micros(&env, Some(400)), Some(600));
assert_eq!(elapsed_micros(&env, None), None);
}
}