use std::fs;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenMode {
MustExist,
CreateNew,
OpenOrCreate,
}
pub trait VfsFile: Send {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()>;
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<()>;
fn sync(&self) -> io::Result<()>;
fn truncate(&self, len: u64) -> io::Result<()>;
fn len(&self) -> io::Result<u64>;
fn is_empty(&self) -> io::Result<bool> {
Ok(self.len()? == 0)
}
fn try_lock_exclusive(&self) -> io::Result<bool>;
}
pub trait Vfs: Send + Sync {
fn open(&self, path: &Path, mode: OpenMode) -> io::Result<Box<dyn VfsFile>>;
fn delete(&self, path: &Path) -> io::Result<()>;
fn exists(&self, path: &Path) -> bool;
fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RealVfs;
impl Vfs for RealVfs {
fn open(&self, path: &Path, mode: OpenMode) -> io::Result<Box<dyn VfsFile>> {
let mut opts = fs::OpenOptions::new();
opts.read(true).write(true);
match mode {
OpenMode::MustExist => {}
OpenMode::CreateNew => {
opts.create_new(true);
}
OpenMode::OpenOrCreate => {
opts.create(true);
}
}
Ok(Box::new(RealFile {
file: opts.open(path)?,
}))
}
fn delete(&self, path: &Path) -> io::Result<()> {
fs::remove_file(path)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
fs::rename(from, to)
}
}
struct RealFile {
file: fs::File,
}
impl VfsFile for RealFile {
#[cfg(unix)]
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
use std::os::unix::fs::FileExt;
self.file.read_exact_at(buf, offset)
}
#[cfg(windows)]
fn read_at(&self, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> {
use std::os::windows::fs::FileExt;
while !buf.is_empty() {
match self.file.seek_read(buf, offset) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"read past end of file",
));
}
Ok(n) => {
buf = &mut buf[n..];
offset += n as u64;
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(unix)]
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
use std::os::unix::fs::FileExt;
self.file.write_all_at(buf, offset)
}
#[cfg(windows)]
fn write_at(&self, mut buf: &[u8], mut offset: u64) -> io::Result<()> {
use std::os::windows::fs::FileExt;
while !buf.is_empty() {
match self.file.seek_write(buf, offset) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"failed to write page",
));
}
Ok(n) => {
buf = &buf[n..];
offset += n as u64;
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
fn sync(&self) -> io::Result<()> {
self.file.sync_all()
}
fn truncate(&self, len: u64) -> io::Result<()> {
self.file.set_len(len)
}
fn len(&self) -> io::Result<u64> {
Ok(self.file.metadata()?.len())
}
fn try_lock_exclusive(&self) -> io::Result<bool> {
match self.file.try_lock() {
Ok(()) => Ok(true),
Err(fs::TryLockError::WouldBlock) => Ok(false),
Err(fs::TryLockError::Error(e)) => Err(e),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use std::path::PathBuf;
fn temp_path(tag: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"embedmind-vfs-test-{}-{}-{tag}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
p
}
#[test]
fn real_vfs_roundtrip_and_lock() {
let vfs = RealVfs;
let path = temp_path("roundtrip");
let f = vfs.open(&path, OpenMode::CreateNew).unwrap();
assert!(f.try_lock_exclusive().unwrap());
f.write_at(b"hello", 3).unwrap();
assert_eq!(f.len().unwrap(), 8);
let mut buf = [0u8; 5];
f.read_at(&mut buf, 3).unwrap();
assert_eq!(&buf, b"hello");
let g = vfs.open(&path, OpenMode::MustExist).unwrap();
assert!(!g.try_lock_exclusive().unwrap());
drop(g);
f.truncate(4).unwrap();
assert_eq!(f.len().unwrap(), 4);
f.sync().unwrap();
assert!(f.read_at(&mut buf, 3).is_err());
drop(f);
vfs.delete(&path).unwrap();
assert!(!vfs.exists(&path));
}
#[test]
fn real_vfs_open_modes() {
let vfs = RealVfs;
let path = temp_path("modes");
assert!(vfs.open(&path, OpenMode::MustExist).is_err());
let f = vfs.open(&path, OpenMode::OpenOrCreate).unwrap();
assert!(vfs.open(&path, OpenMode::CreateNew).is_err());
drop(f);
vfs.delete(&path).unwrap();
}
}