use std::fs::{File, OpenOptions};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "mmap")]
use std::sync::Arc;
use std::sync::Mutex;
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,
source: ReadSource,
accum: Mutex<Accum>,
accum_dirty: AtomicBool,
writable: bool,
lock_policy: FileLocking,
lock_held: bool,
base: u64,
}
enum ReadSource {
Pread,
#[cfg(feature = "mmap")]
Mapped(Arc<LockedMap>),
}
#[cfg(feature = "mmap")]
pub(crate) struct LockedMap {
map: memmap2::Mmap,
_lock: File,
}
#[cfg(feature = "mmap")]
impl std::ops::Deref for LockedMap {
type Target = memmap2::Mmap;
fn deref(&self) -> &memmap2::Mmap {
&self.map
}
}
#[cfg(feature = "mmap")]
const MAP_MAX_READ: usize = 8 << 10;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ReadDst {
Fresh,
Reused,
}
impl ReadSource {
fn for_read_only(file: &File, lock_held: bool) -> Self {
#[cfg(feature = "mmap")]
{
let len = file.metadata().map(|m| m.len()).unwrap_or(0);
if lock_held && len > 0 && usize::try_from(len).is_ok() {
if let (Ok(lock), Ok(map)) = (file.try_clone(), unsafe { memmap2::Mmap::map(file) })
{
return ReadSource::Mapped(Arc::new(LockedMap { map, _lock: lock }));
}
}
}
let _ = (file, lock_held);
ReadSource::Pread
}
#[cfg(feature = "mmap")]
fn map_for(&self, len: usize, dst: ReadDst) -> Option<&LockedMap> {
match self {
ReadSource::Mapped(map) if dst == ReadDst::Reused || len <= MAP_MAX_READ => Some(map),
_ => None,
}
}
fn len(&self, file: &File) -> std::io::Result<u64> {
match self {
#[cfg(feature = "mmap")]
ReadSource::Mapped(map) => Ok(map.len() as u64),
ReadSource::Pread => Ok(file.metadata()?.len()),
}
}
fn read_vec(&self, file: &File, at: u64, len: usize) -> std::io::Result<Vec<u8>> {
#[cfg(feature = "mmap")]
if let Some(map) = self.map_for(len, ReadDst::Fresh) {
return Ok(mapped_range(map, at, len)?.to_vec());
}
let mut buf = vec![0u8; len];
pread_exact(file, at, &mut buf)?;
Ok(buf)
}
fn read_vec_upto(&self, file: &File, at: u64, max_len: usize) -> std::io::Result<Vec<u8>> {
#[cfg(feature = "mmap")]
if let Some(map) = self.map_for(max_len, ReadDst::Fresh) {
let avail = (map.len() as u64).saturating_sub(at) as usize;
return Ok(mapped_range(map, at, max_len.min(avail))?.to_vec());
}
let mut buf = vec![0u8; max_len];
let mut total = 0;
while total < buf.len() {
match pread(file, at + 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)
}
fn read_exact_into(
&self,
file: &File,
at: u64,
buf: &mut [u8],
dst: ReadDst,
) -> std::io::Result<()> {
#[cfg(feature = "mmap")]
if let Some(map) = self.map_for(buf.len(), dst) {
buf.copy_from_slice(mapped_range(map, at, buf.len())?);
return Ok(());
}
let _ = dst;
pread_exact(file, at, buf)
}
}
#[cfg(feature = "mmap")]
fn mapped_range(map: &memmap2::Mmap, at: u64, len: usize) -> std::io::Result<&[u8]> {
if len == 0 {
return Ok(&[]);
}
let end = at.checked_add(len as u64);
let range = usize::try_from(at)
.ok()
.zip(end.and_then(|e| usize::try_from(e).ok()));
match range.and_then(|(s, e)| map.get(s..e)) {
Some(slice) => Ok(slice),
None => Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
)),
}
}
const ACCUM_MAX: usize = 1 << 20;
#[cfg(not(feature = "threadsafe"))]
const ACCUM_PASSTHROUGH: usize = 8 << 10;
#[cfg(not(feature = "threadsafe"))]
fn stageable(len: usize) -> bool {
len < ACCUM_PASSTHROUGH
}
#[cfg(feature = "threadsafe")]
fn stageable(_len: usize) -> bool {
false
}
const ACCUM_MAX_GAP: u64 = 4096;
struct Accum {
start: u64,
buf: Vec<u8>,
gaps: Vec<std::ops::Range<usize>>,
}
impl Accum {
fn new() -> Self {
Accum {
start: 0,
buf: Vec::new(),
gaps: Vec::new(),
}
}
fn end(&self) -> u64 {
self.start + self.buf.len() as u64
}
fn flush(&mut self, file: &File) -> std::io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let result = self.write_out(file);
self.buf.clear();
self.gaps.clear();
result
}
fn write_out(&mut self, file: &File) -> std::io::Result<()> {
if !self.gaps.is_empty() {
let mut on_disk = vec![0u8; self.buf.len()];
pread_upto(file, self.start, &mut on_disk)?;
for gap in std::mem::take(&mut self.gaps) {
self.buf[gap.clone()].copy_from_slice(&on_disk[gap]);
}
}
pwrite_all(file, self.start, &self.buf)
}
fn stage(&mut self, file: &File, at: u64, end: u64, data: &[u8]) -> std::io::Result<()> {
if !self.buf.is_empty() && end.saturating_sub(self.start) as usize <= ACCUM_MAX {
if at >= self.start && at <= self.end() {
let off = (at - self.start) as usize;
if off + data.len() > self.buf.len() {
self.buf.resize(off + data.len(), 0);
}
self.buf[off..off + data.len()].copy_from_slice(data);
self.fill_gaps(off..off + data.len());
return Ok(());
}
if at > self.end() && at - self.end() <= ACCUM_MAX_GAP {
let gap = self.buf.len()..(at - self.start) as usize;
self.buf.resize(gap.end, 0);
self.buf.extend_from_slice(data);
self.gaps.push(gap);
return Ok(());
}
}
self.flush(file)?;
self.buf.extend_from_slice(data);
self.start = at;
Ok(())
}
fn fill_gaps(&mut self, written: std::ops::Range<usize>) {
match self.gaps.last() {
Some(last) if written.start < last.end => {}
_ => return,
}
let mut kept = Vec::with_capacity(self.gaps.len() + 1);
for gap in self.gaps.drain(..) {
if written.end <= gap.start || written.start >= gap.end {
kept.push(gap);
continue;
}
if gap.start < written.start {
kept.push(gap.start..written.start);
}
if written.end < gap.end {
kept.push(written.end..gap.end);
}
}
self.gaps = kept;
}
}
impl FileHandle {
fn new(file: File, writable: bool, lock_policy: FileLocking, lock_held: bool) -> Self {
Self {
file,
source: ReadSource::Pread,
accum: Mutex::new(Accum::new()),
accum_dirty: AtomicBool::new(false),
writable,
lock_policy,
lock_held,
base: 0,
}
}
fn new_read_only(file: File, lock_policy: FileLocking, lock_held: bool) -> Self {
let source = ReadSource::for_read_only(&file, lock_held);
let mut handle = Self::new(file, false, lock_policy, lock_held);
handle.source = source;
handle
}
#[cfg(test)]
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::new(file, true, policy, lock_held))
}
pub fn open_or_create_readwrite_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)?;
Ok(Self::new(file, true, policy, lock_held))
}
#[cfg(test)]
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::new_read_only(file, policy, lock_held))
}
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::new(file, true, policy, lock_held))
}
pub fn base(&self) -> u64 {
self.base
}
pub fn set_base(&mut self, base: u64) {
self.base = base;
}
pub fn locate_signature(&self) -> std::io::Result<Option<u64>> {
use crate::format::superblock::HDF5_SIGNATURE;
self.flush()?;
let file_len = self.source.len(&self.file)?;
let mut buf = [0u8; HDF5_SIGNATURE.len()];
let mut addr = 0u64;
loop {
if addr + HDF5_SIGNATURE.len() as u64 <= file_len {
self.source
.read_exact_into(&self.file, addr, &mut buf, ReadDst::Fresh)?;
if buf == HDF5_SIGNATURE {
return Ok(Some(addr));
}
}
addr = if addr == 0 { 512 } else { addr * 2 };
if addr >= file_len {
return Ok(None);
}
}
}
pub fn release_lock(&mut self) -> std::io::Result<()> {
if !self.writable {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"a read-only handle keeps its shared lock: its map, and every view of it, \
were taken under it",
));
}
if !self.lock_held || matches!(self.lock_policy, FileLocking::Disabled) {
return Ok(());
}
self.flush()?;
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",
));
}
let at = self.abs(offset)?;
let end = at.checked_add(data.len() as u64).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"write of {} bytes at {offset} overflows the address space",
data.len()
),
)
})?;
if !stageable(data.len()) {
if self.accum_dirty.load(Ordering::Acquire) {
let mut accum = self.accum.lock().unwrap();
if at < accum.end() && end > accum.start {
let result = accum.flush(&self.file);
self.accum_dirty.store(false, Ordering::Release);
result?;
}
}
return pwrite_all(&self.file, at, data);
}
let mut accum = self.accum.lock().unwrap();
let result = accum.stage(&self.file, at, end, data);
self.accum_dirty
.store(!accum.buf.is_empty(), Ordering::Release);
result
}
pub fn flush(&self) -> std::io::Result<()> {
if !self.accum_dirty.load(Ordering::Acquire) {
return Ok(());
}
let mut accum = self.accum.lock().unwrap();
let result = accum.flush(&self.file);
self.accum_dirty.store(false, Ordering::Release);
result
}
pub fn read_at(&self, offset: u64, len: usize) -> std::io::Result<Vec<u8>> {
self.flush()?;
let file_len = self.source.len(&self.file)?;
let start = self.abs(offset)?;
let end = start.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}"),
));
}
self.source.read_vec(&self.file, start, len)
}
pub fn read_exact_at_into(
&self,
offset: u64,
buf: &mut [u8],
dst: ReadDst,
) -> std::io::Result<()> {
self.flush()?;
self.source
.read_exact_into(&self.file, self.abs(offset)?, buf, dst)
}
pub fn read_at_most(&self, offset: u64, max_len: usize) -> std::io::Result<Vec<u8>> {
self.flush()?;
let file_len = self.source.len(&self.file)?;
let start = self.abs(offset)?;
let avail = file_len.saturating_sub(start);
let max_len = (max_len as u64).min(avail) as usize;
self.source.read_vec_upto(&self.file, start, max_len)
}
pub fn refresh_read_source(&mut self) {
#[cfg(feature = "mmap")]
if matches!(self.source, ReadSource::Mapped(_)) {
self.source = ReadSource::Pread;
self.source = ReadSource::for_read_only(&self.file, self.lock_held);
}
}
#[cfg(feature = "mmap")]
pub fn map_snapshot(&self) -> Option<Arc<LockedMap>> {
match &self.source {
ReadSource::Mapped(map) => Some(Arc::clone(map)),
ReadSource::Pread => None,
}
}
#[cfg(test)]
fn is_mapped(&self) -> bool {
!matches!(self.source, ReadSource::Pread)
}
fn abs(&self, offset: u64) -> std::io::Result<u64> {
offset.checked_add(self.base).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"address {offset} overflows past the userblock at {}",
self.base
),
)
})
}
pub fn sync_data(&self) -> std::io::Result<()> {
self.flush()?;
self.file.sync_data()
}
pub fn sync_all(&self) -> std::io::Result<()> {
self.flush()?;
self.file.sync_all()
}
pub fn file_size(&self) -> std::io::Result<u64> {
self.flush()?;
Ok(self.source.len(&self.file)?.saturating_sub(self.base))
}
pub fn set_eof(&self, eof: u64) -> std::io::Result<()> {
self.flush()?;
let want = eof + self.base;
if self.file.metadata()?.len() != want {
self.file.set_len(want)?;
}
Ok(())
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
if let Err(e) = self.flush() {
eprintln!(
"rust-hdf5: failed to flush buffered file writes on drop: {e}. \
The file may be incomplete or corrupt; call H5File::close() \
to handle this error explicitly."
);
}
}
}
#[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)
}
fn pread_upto(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
let mut total = 0;
while total < buf.len() {
match pread(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),
}
}
Ok(())
}
#[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(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
fn tmp(label: &str) -> std::path::PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rust_hdf5_accum_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir.join("f.bin")
}
fn stage(accum: &mut Accum, file: &File, at: u64, data: &[u8]) {
accum.stage(file, at, at + data.len() as u64, data).unwrap();
}
fn scratch() -> (std::path::PathBuf, File) {
let path = tmp("accum_unit");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
(path, file)
}
#[test]
fn adjoining_writes_join_one_run() {
let (_p, file) = scratch();
let mut accum = Accum::new();
stage(&mut accum, &file, 100, &[1u8; 10]);
stage(&mut accum, &file, 110, &[2u8; 10]);
stage(&mut accum, &file, 120, &[3u8; 10]);
assert_eq!(accum.start, 100);
assert_eq!(accum.buf.len(), 30);
assert!(accum.gaps.is_empty());
assert_eq!(file.metadata().unwrap().len(), 0, "nothing written yet");
accum.flush(&file).unwrap();
assert_eq!(file.metadata().unwrap().len(), 130);
}
#[test]
fn a_short_hop_is_bridged_and_recorded() {
let (_p, file) = scratch();
let mut accum = Accum::new();
stage(&mut accum, &file, 100, &[1u8; 10]);
stage(&mut accum, &file, 116, &[2u8; 10]);
assert_eq!(accum.start, 100);
assert_eq!(accum.gaps, vec![10..16]);
stage(&mut accum, &file, 116 + 10 + ACCUM_MAX_GAP + 1, &[3u8; 10]);
assert_eq!(accum.start, 116 + 10 + ACCUM_MAX_GAP + 1);
assert!(accum.gaps.is_empty());
}
#[test]
fn a_bridge_keeps_the_bytes_already_under_it() {
let path = tmp("bridge_keeps");
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[0xABu8; 64]).unwrap();
handle.flush().unwrap();
handle.write_at(0, &[1, 2, 3, 4]).unwrap();
handle.write_at(60, &[5, 6, 7, 8]).unwrap();
handle.flush().unwrap();
let got = handle.read_at(0, 64).unwrap();
assert_eq!(&got[0..4], &[1, 2, 3, 4]);
assert_eq!(&got[4..60], &[0xABu8; 56]);
assert_eq!(&got[60..64], &[5, 6, 7, 8]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn a_write_into_a_bridged_gap_wins_over_the_read_back() {
let path = tmp("gap_overwrite");
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[0xABu8; 64]).unwrap();
handle.flush().unwrap();
handle.write_at(0, &[1, 2, 3, 4]).unwrap();
handle.write_at(60, &[5, 6, 7, 8]).unwrap();
handle.write_at(30, &[9, 9]).unwrap();
handle.flush().unwrap();
let got = handle.read_at(0, 64).unwrap();
assert_eq!(&got[28..34], &[0xAB, 0xAB, 9, 9, 0xAB, 0xAB]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn a_read_sees_writes_still_in_the_accumulator() {
let path = tmp("read_through");
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[7u8; 32]).unwrap();
assert_eq!(handle.read_at(0, 32).unwrap(), vec![7u8; 32]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn the_file_length_accounts_for_buffered_writes() {
let path = tmp("file_len");
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[7u8; 300]).unwrap();
assert_eq!(handle.file_size().unwrap(), 300);
handle.write_at(300, &[8u8; 100]).unwrap();
handle.set_eof(400).unwrap();
assert_eq!(handle.file_size().unwrap(), 400);
assert_eq!(handle.read_at(299, 2).unwrap(), vec![7, 8]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[cfg(not(feature = "threadsafe"))]
#[test]
fn a_pass_through_write_lands_after_the_bytes_it_covers() {
let path = tmp("pass_through");
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[1u8; 64]).unwrap();
handle.write_at(0, &vec![2u8; ACCUM_PASSTHROUGH]).unwrap();
handle.flush().unwrap();
assert_eq!(handle.read_at(0, 64).unwrap(), vec![2u8; 64]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn drop_flushes_what_is_left() {
let path = tmp("drop_flush");
{
let handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[42u8; 16]).unwrap();
}
assert_eq!(std::fs::read(&path).unwrap(), vec![42u8; 16]);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn a_userblock_does_not_break_the_run() {
let path = tmp("userblock");
let mut handle = FileHandle::create(&path).unwrap();
handle.write_at(0, &[0u8; 512]).unwrap();
handle.set_base(512);
handle.write_at(0, &[3u8; 8]).unwrap();
handle.write_at(8, &[4u8; 8]).unwrap();
handle.flush().unwrap();
assert_eq!(handle.file_size().unwrap(), 16);
drop(handle);
assert_eq!(std::fs::read(&path).unwrap().len(), 528);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
}
#[cfg(test)]
mod read_source_tests {
use super::*;
fn dir(label: &str) -> std::path::PathBuf {
use std::sync::atomic::AtomicU64;
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rust_hdf5_source_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn only_a_read_only_open_maps() {
let d = dir("only_read_only");
let path = d.join("f.bin");
std::fs::write(&path, vec![7u8; 4096]).unwrap();
let writable =
FileHandle::open_readwrite_with_locking(&path, FileLocking::Disabled).unwrap();
assert!(!writable.is_mapped());
drop(writable);
let created = FileHandle::create(&d.join("created.bin")).unwrap();
assert!(!created.is_mapped());
drop(created);
let read_only = FileHandle::open_read(&path).unwrap();
assert_eq!(read_only.is_mapped(), cfg!(feature = "mmap"));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_map_needs_the_shared_lock() {
let d = dir("map_needs_lock");
let path = d.join("f.bin");
std::fs::write(&path, vec![7u8; 4096]).unwrap();
let waived = FileHandle::open_read_with_locking(&path, FileLocking::Disabled).unwrap();
assert!(!waived.is_mapped());
assert_eq!(waived.read_at(0, 4).unwrap(), vec![7u8; 4]);
drop(waived);
let mut locked = FileHandle::open_read_with_locking(&path, FileLocking::Enabled).unwrap();
assert_eq!(locked.is_mapped(), cfg!(feature = "mmap"));
assert!(locked.release_lock().is_err());
assert_eq!(locked.is_mapped(), cfg!(feature = "mmap"));
assert_eq!(locked.read_at(0, 4).unwrap(), vec![7u8; 4]);
let _ = std::fs::remove_dir_all(&d);
}
#[cfg(feature = "mmap")]
#[test]
fn a_map_share_keeps_the_shared_lock_after_its_handle() {
let d = dir("map_keeps_lock");
let path = d.join("f.bin");
std::fs::write(&path, vec![7u8; 4096]).unwrap();
let handle = FileHandle::open_read_with_locking(&path, FileLocking::Enabled).unwrap();
let share = handle
.map_snapshot()
.expect("a locked read-only handle maps");
drop(handle);
assert!(
FileHandle::open_readwrite_with_locking(&path, FileLocking::Enabled).is_err(),
"a writer took the lock under a live map share"
);
assert_eq!(&share[..4], &[7u8; 4]);
drop(share);
FileHandle::open_readwrite_with_locking(&path, FileLocking::Enabled).unwrap();
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_read_ending_at_eof_succeeds() {
let d = dir("at_eof");
let path = d.join("f.bin");
let bytes: Vec<u8> = (0..1000u32).map(|i| i as u8).collect();
std::fs::write(&path, &bytes).unwrap();
let handle = FileHandle::open_read(&path).unwrap();
assert_eq!(handle.file_size().unwrap(), 1000);
assert_eq!(handle.read_at(990, 10).unwrap(), bytes[990..].to_vec());
assert_eq!(handle.read_at(1000, 0).unwrap(), Vec::<u8>::new());
assert_eq!(handle.read_at_most(1000, 64).unwrap(), Vec::<u8>::new());
assert_eq!(handle.read_at_most(990, 64).unwrap(), bytes[990..].to_vec());
let mut out = [0u8; 10];
handle
.read_exact_at_into(990, &mut out, ReadDst::Fresh)
.unwrap();
assert_eq!(&out, &bytes[990..]);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_read_past_eof_fails_as_unexpected_eof() {
let d = dir("past_eof");
let path = d.join("f.bin");
std::fs::write(&path, vec![3u8; 1000]).unwrap();
let handle = FileHandle::open_read(&path).unwrap();
for (offset, len) in [(1000u64, 1usize), (999, 2), (0, 1001)] {
let err = handle.read_at(offset, len).unwrap_err();
assert_eq!(
err.kind(),
std::io::ErrorKind::UnexpectedEof,
"read_at({offset}, {len})"
);
let mut out = vec![0u8; len];
let err = handle
.read_exact_at_into(offset, &mut out, ReadDst::Fresh)
.unwrap_err();
assert_eq!(
err.kind(),
std::io::ErrorKind::UnexpectedEof,
"read_exact_at_into({offset}, {len})"
);
}
assert!(handle.read_at(u64::MAX, 8).is_err());
assert!(handle
.read_exact_at_into(u64::MAX, &mut [0u8; 8], ReadDst::Fresh)
.is_err());
assert!(handle.read_at_most(1000, 64).unwrap().is_empty());
assert!(handle.read_at_most(u64::MAX, 64).unwrap().is_empty());
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_userblock_shifts_the_source_the_same_way() {
let d = dir("userblock");
let path = d.join("f.bin");
let mut bytes = vec![0xEEu8; 512];
bytes.extend((0..500u32).map(|i| i as u8));
std::fs::write(&path, &bytes).unwrap();
let mut handle = FileHandle::open_read(&path).unwrap();
handle.set_base(512);
assert_eq!(handle.file_size().unwrap(), 500);
assert_eq!(handle.read_at(0, 4).unwrap(), vec![0, 1, 2, 3]);
assert_eq!(handle.read_at(499, 1).unwrap(), vec![243]);
assert_eq!(
handle.read_at(500, 1).unwrap_err().kind(),
std::io::ErrorKind::UnexpectedEof
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn growth_is_picked_up_when_the_source_is_retaken() {
use std::io::Write;
let d = dir("growth");
let path = d.join("f.bin");
std::fs::write(&path, vec![1u8; 4096]).unwrap();
let policy = if cfg!(windows) {
FileLocking::Disabled
} else {
FileLocking::Enabled
};
let mut handle = FileHandle::open_read_with_locking(&path, policy).unwrap();
assert_eq!(handle.file_size().unwrap(), 4096);
let mut appender = OpenOptions::new().append(true).open(&path).unwrap();
appender.write_all(&vec![2u8; 4096]).unwrap();
appender.flush().unwrap();
#[cfg(all(feature = "mmap", not(windows)))]
{
assert!(handle.is_mapped());
assert_eq!(handle.file_size().unwrap(), 4096);
assert_eq!(
handle.read_at(4096, 4096).unwrap_err().kind(),
std::io::ErrorKind::UnexpectedEof
);
}
handle.refresh_read_source();
assert_eq!(handle.file_size().unwrap(), 8192);
assert_eq!(handle.read_at(4096, 4096).unwrap(), vec![2u8; 4096]);
assert_eq!(handle.read_at(0, 4096).unwrap(), vec![1u8; 4096]);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn an_empty_file_falls_back_to_the_descriptor() {
let d = dir("empty");
let path = d.join("f.bin");
std::fs::write(&path, []).unwrap();
let mut handle = FileHandle::open_read(&path).unwrap();
assert!(!handle.is_mapped());
assert_eq!(handle.file_size().unwrap(), 0);
assert!(handle.read_at_most(0, 64).unwrap().is_empty());
assert_eq!(
handle.read_at(0, 1).unwrap_err().kind(),
std::io::ErrorKind::UnexpectedEof
);
assert!(handle.locate_signature().unwrap().is_none());
handle.refresh_read_source();
assert!(!handle.is_mapped());
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_map_and_a_descriptor_read_the_same_bytes() {
let d = dir("differential");
let path = d.join("f.bin");
let bytes: Vec<u8> = (0..70_000u32).map(|i| (i * 31) as u8).collect();
std::fs::write(&path, &bytes).unwrap();
let handle = FileHandle::open_read(&path).unwrap();
for (offset, len) in [
(0u64, 8usize),
(1, 1),
(4095, 4098),
(100, 8 * 1024 - 1),
(100, 8 * 1024),
(100, 8 * 1024 + 1),
(65_536, 4464),
(69_999, 1),
(0, 70_000),
] {
let want = &bytes[offset as usize..offset as usize + len];
assert_eq!(handle.read_at(offset, len).unwrap(), want, "read_at");
assert_eq!(
handle.read_at_most(offset, len + 100).unwrap(),
&bytes[offset as usize..(offset as usize + len + 100).min(bytes.len())],
"read_at_most"
);
let mut out = vec![0u8; len];
handle
.read_exact_at_into(offset, &mut out, ReadDst::Fresh)
.unwrap();
assert_eq!(out, want, "read_exact_at_into");
}
let _ = std::fs::remove_dir_all(&d);
}
}