use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::io::locking::{self, FileLocking, LockMode};
#[cfg(all(feature = "threadsafe", not(any(unix, windows))))]
compile_error!(
"the `threadsafe` feature requires positioned file I/O (pread/pwrite), \
which is only available on Unix and Windows targets; on this target the \
seek-based fallback would race the shared file cursor across threads"
);
pub struct FileHandle {
file: File,
writable: bool,
lock_policy: FileLocking,
lock_held: bool,
}
impl FileHandle {
pub fn create(path: &Path) -> std::io::Result<Self> {
Self::create_with_locking(path, FileLocking::from_env_or(FileLocking::default()))
}
pub fn create_with_locking(path: &Path, policy: FileLocking) -> std::io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
let lock_held = locking::try_acquire(&file, LockMode::Exclusive, policy)?;
if file.metadata()?.len() > 0 {
file.set_len(0)?;
}
Ok(Self {
file,
writable: true,
lock_policy: policy,
lock_held,
})
}
pub fn open_read(path: &Path) -> std::io::Result<Self> {
Self::open_read_with_locking(path, FileLocking::from_env_or(FileLocking::default()))
}
pub fn open_read_with_locking(path: &Path, policy: FileLocking) -> std::io::Result<Self> {
let file = OpenOptions::new().read(true).open(path)?;
let lock_held = locking::try_acquire(&file, LockMode::Shared, policy)?;
Ok(Self {
file,
writable: false,
lock_policy: policy,
lock_held,
})
}
pub fn open_readwrite(path: &Path) -> std::io::Result<Self> {
Self::open_readwrite_with_locking(path, FileLocking::from_env_or(FileLocking::default()))
}
pub fn open_readwrite_with_locking(path: &Path, policy: FileLocking) -> std::io::Result<Self> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let lock_held = locking::try_acquire(&file, LockMode::Exclusive, policy)?;
Ok(Self {
file,
writable: true,
lock_policy: policy,
lock_held,
})
}
pub fn lock_policy(&self) -> FileLocking {
self.lock_policy
}
pub fn lock_held(&self) -> bool {
self.lock_held
}
pub fn release_lock(&mut self) -> std::io::Result<()> {
if !self.lock_held || matches!(self.lock_policy, FileLocking::Disabled) {
return Ok(());
}
locking::release(&self.file)?;
self.lock_held = false;
Ok(())
}
pub fn write_at(&self, offset: u64, data: &[u8]) -> std::io::Result<()> {
if !self.writable {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"file opened read-only",
));
}
pwrite_all(&self.file, offset, data)
}
pub fn read_at(&self, offset: u64, len: usize) -> std::io::Result<Vec<u8>> {
let file_len = self.file.metadata()?.len();
let end = offset.checked_add(len as u64);
if end.is_none_or(|e| e > file_len) {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("read past end: offset={offset} len={len} file_size={file_len}"),
));
}
let mut buf = vec![0u8; len];
pread_exact(&self.file, offset, &mut buf)?;
Ok(buf)
}
pub fn read_exact_at_into(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
pread_exact(&self.file, offset, buf)
}
pub fn read_at_most(&self, offset: u64, max_len: usize) -> std::io::Result<Vec<u8>> {
let file_len = self.file.metadata()?.len();
let avail = file_len.saturating_sub(offset);
let max_len = (max_len as u64).min(avail) as usize;
let mut buf = vec![0u8; max_len];
let mut total = 0;
while total < buf.len() {
match pread(&self.file, offset + total as u64, &mut buf[total..]) {
Ok(0) => break,
Ok(n) => total += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
buf.truncate(total);
Ok(buf)
}
pub fn sync_data(&self) -> std::io::Result<()> {
self.file.sync_data()
}
pub fn sync_all(&self) -> std::io::Result<()> {
self.file.sync_all()
}
pub fn file_size(&self) -> std::io::Result<u64> {
Ok(self.file.metadata()?.len())
}
}
#[cfg(unix)]
fn pwrite_all(file: &File, offset: u64, data: &[u8]) -> std::io::Result<()> {
use std::os::unix::fs::FileExt;
file.write_all_at(data, offset)
}
#[cfg(windows)]
fn pwrite_all(file: &File, mut offset: u64, mut data: &[u8]) -> std::io::Result<()> {
use std::os::windows::fs::FileExt;
while !data.is_empty() {
match file.seek_write(data, offset) {
Ok(0) => {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write whole buffer",
));
}
Ok(n) => {
data = &data[n..];
offset += n as u64;
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn pwrite_all(file: &File, offset: u64, data: &[u8]) -> std::io::Result<()> {
use std::io::{Seek, SeekFrom, Write};
let mut f = file;
f.seek(SeekFrom::Start(offset))?;
f.write_all(data)
}
#[cfg(unix)]
fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
use std::os::unix::fs::FileExt;
file.read_at(buf, offset)
}
#[cfg(windows)]
fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
use std::os::windows::fs::FileExt;
file.seek_read(buf, offset)
}
#[cfg(not(any(unix, windows)))]
fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
use std::io::{Read, Seek, SeekFrom};
let mut f = file;
f.seek(SeekFrom::Start(offset))?;
f.read(buf)
}
#[cfg(unix)]
fn pread_exact(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
use std::os::unix::fs::FileExt;
file.read_exact_at(buf, offset)
}
#[cfg(windows)]
fn pread_exact(file: &File, mut offset: u64, mut buf: &mut [u8]) -> std::io::Result<()> {
use std::os::windows::fs::FileExt;
while !buf.is_empty() {
match file.seek_read(buf, offset) {
Ok(0) => {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
));
}
Ok(n) => {
let tmp = buf;
buf = &mut tmp[n..];
offset += n as u64;
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn pread_exact(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
use std::io::{Read, Seek, SeekFrom};
let mut f = file;
f.seek(SeekFrom::Start(offset))?;
f.read_exact(buf)
}
#[cfg(feature = "mmap")]
pub struct MmapFileHandle {
mmap: memmap2::Mmap,
_file: File,
}
#[cfg(feature = "mmap")]
impl MmapFileHandle {
pub fn open(path: &Path) -> std::io::Result<Self> {
Self::open_with_locking(path, FileLocking::from_env_or(FileLocking::default()))
}
pub fn open_with_locking(path: &Path, policy: FileLocking) -> std::io::Result<Self> {
let file = File::open(path)?;
let _ = locking::try_acquire(&file, LockMode::Shared, policy)?;
let mmap = unsafe { memmap2::Mmap::map(&file)? };
Ok(Self { mmap, _file: file })
}
pub fn len(&self) -> usize {
self.mmap.len()
}
pub fn is_empty(&self) -> bool {
self.mmap.is_empty()
}
pub fn read_at(&self, offset: u64, len: usize) -> std::io::Result<&[u8]> {
let end = offset
.checked_add(len as u64)
.filter(|&e| e <= self.mmap.len() as u64);
match end {
Some(end) => Ok(&self.mmap[offset as usize..end as usize]),
None => Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"mmap read past end: offset={} len={} file_size={}",
offset,
len,
self.mmap.len()
),
)),
}
}
pub fn read_at_most(&self, offset: u64, max_len: usize) -> &[u8] {
if offset >= self.mmap.len() as u64 {
return &[];
}
let start = offset as usize;
let end = (start as u64)
.saturating_add(max_len as u64)
.min(self.mmap.len() as u64) as usize;
&self.mmap[start..end]
}
}