use crate::block::{BlockDevice, BlockRead};
use crate::error::{Error, Result};
use std::fs::{File, OpenOptions};
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use std::sync::Mutex;
pub struct FileDevice {
file: File,
write_lock: Mutex<()>,
size: u64,
writable: bool,
}
impl FileDevice {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
let size = file.metadata()?.len();
Ok(Self {
file,
write_lock: Mutex::new(()),
size,
writable: false,
})
}
pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let size = file.metadata()?.len();
Ok(Self {
file,
write_lock: Mutex::new(()),
size,
writable: true,
})
}
pub fn open_best_effort<P: AsRef<Path>>(path: P) -> Result<Self> {
let p = path.as_ref();
match Self::open_rw(p) {
Ok(d) => Ok(d),
Err(_) => Self::open(p),
}
}
}
impl FileDevice {
#[cfg(unix)]
fn read_once(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
use std::os::unix::fs::FileExt;
Ok(self.file.read_at(buf, offset)?)
}
#[cfg(windows)]
fn read_once(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
use std::os::windows::fs::FileExt;
let _guard = self.write_lock.lock().unwrap();
Ok(self.file.seek_read(buf, offset)?)
}
}
impl BlockRead for FileDevice {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
let mut total = 0usize;
while total < buf.len() {
let n = self.read_once(offset + total as u64, &mut buf[total..])?;
if n == 0 {
return Err(Error::ShortRead {
offset,
want: buf.len(),
got: total,
});
}
total += n;
}
Ok(())
}
fn size_bytes(&self) -> u64 {
self.size
}
}
impl BlockDevice for FileDevice {
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
if !self.writable {
return Err(Error::ReadOnly);
}
let _guard = self.write_lock.lock().unwrap();
let mut f = &self.file;
f.seek(SeekFrom::Start(offset))?;
f.write_all(buf)?;
Ok(())
}
fn flush(&self) -> Result<()> {
if !self.writable {
return Ok(());
}
let _guard = self.write_lock.lock().unwrap();
let mut f = &self.file;
f.flush()?;
self.file.sync_data()?;
Ok(())
}
fn is_writable(&self) -> bool {
self.writable
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn many_threads_reading_different_offsets_each_get_their_own_bytes() {
let path = temp_path("parallel_reads");
let _c = Cleanup(path.clone());
let mut bytes = Vec::with_capacity(64 * 256);
for page in 0..64u8 {
bytes.extend(std::iter::repeat_n(page, 256));
}
std::fs::write(&path, &bytes).expect("write the image");
let dev = std::sync::Arc::new(FileDevice::open(&path).expect("open"));
let mut handles = Vec::new();
for page in 0..64u8 {
let dev = dev.clone();
handles.push(std::thread::spawn(move || {
for _ in 0..50 {
let mut buf = [0u8; 256];
dev.read_at(u64::from(page) * 256, &mut buf).expect("read");
assert!(
buf.iter().all(|b| *b == page),
"page {page} came back holding another page's bytes"
);
}
}));
}
for h in handles {
h.join().expect("a reader panicked");
}
}
use std::sync::atomic::{AtomicU64, Ordering};
fn temp_path(tag: &str) -> std::path::PathBuf {
static N: AtomicU64 = AtomicU64::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("fs_core_{tag}_{pid}_{n}.bin"))
}
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn open_rw_round_trips_write_then_read() {
let path = temp_path("rw");
let _g = Cleanup(path.clone());
std::fs::write(&path, vec![0u8; 32]).unwrap();
let dev = FileDevice::open_rw(&path).unwrap();
assert!(dev.is_writable());
assert_eq!(dev.size_bytes(), 32);
dev.write_at(8, &[0xAA, 0xBB, 0xCC, 0xDD]).unwrap();
dev.flush().unwrap();
let mut buf = [0u8; 4];
dev.read_at(8, &mut buf).unwrap();
assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn open_rw_errors_on_missing_path() {
let path = temp_path("missing");
assert!(FileDevice::open_rw(&path).is_err());
}
#[test]
fn open_best_effort_uses_rw_when_writable() {
let path = temp_path("best_rw");
let _g = Cleanup(path.clone());
std::fs::write(&path, vec![0u8; 16]).unwrap();
let dev = FileDevice::open_best_effort(&path).unwrap();
assert!(dev.is_writable());
dev.write_at(0, &[0x11; 4]).unwrap();
}
#[test]
#[cfg(unix)]
fn open_best_effort_falls_back_to_read_only() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("best_ro");
let _g = Cleanup(path.clone());
std::fs::write(&path, vec![0xEFu8; 16]).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444)).unwrap();
let dev = FileDevice::open_best_effort(&path).unwrap();
assert!(!dev.is_writable());
assert!(matches!(dev.write_at(0, &[0u8; 4]), Err(Error::ReadOnly)));
let mut buf = [0u8; 4];
dev.read_at(0, &mut buf).unwrap();
assert_eq!(buf, [0xEF; 4]);
dev.flush().unwrap();
}
}